Crewops API reference
Your SMS, HR, training and document systems read and write Crewops data directly, so a product you buy on its own still fits the way you work.
Every endpoint below takes and returns JSON over HTTPS and opens with your airline's operator code or an API key made from it. Webhooks tell your systems when a report arrives, a notice is signed, a course is completed or a revision is confirmed. Each section covers one product; the endpoints of a product your airline does not hold answer 403.
Base URL, format and sign-in
- Base URL:
https://updates.hawkmason.com. Every path on this page is relative to it. - Format: JSON in and out. Send
Content-Type: application/jsonwith every JSON body; without it the request answers 415. File uploads send the file itself as the body with its own content type. - Sign-in:
Authorization: Bearer <operator code or API key>on every request. - Scope: the code says which airline it belongs to, so there is no airline parameter. Every list and record is your airline's own.
- Products: the code also says which products your airline holds (Insight, Safety, Academy, Library). A product's endpoints open only if the code includes it.
- From a server: call the API from your own systems. Browsers on other websites are refused by the CORS policy, which allows only hawkmason.com.
The crew code your crews enter in Crewops Crew never opens these endpoints. A crew code answers 403: it is in every crew member's hands and must not read colleagues' data.
export CREWOPS="https://updates.hawkmason.com"
export CREWOPS_KEY="cwk_…" # an API key, or the operator code
curl -s "$CREWOPS/portal/today" \
-H "Authorization: Bearer $CREWOPS_KEY"
One key per connected system
Make a key in the operator portal, under Connections, for each system that calls the API: your SMS, your HR system, your training system, your document system. Give each one a name so you know which system it belongs to.
- A key looks like
cwk_followed by 40 hexadecimal characters. It is shown once, when it is made. Crewops stores only its SHA-256 hash. - A key acts as the operator code it was made with: the same airline, the same products and the same expiry date.
- A key cannot make or revoke keys, and cannot set, rotate, test or delete the webhook. It can read the webhook settings.
- Revoke a key on its own, in the portal, and every other key keeps working. Up to 20 keys can be active at once.
- The portal shows the day each key was last used.
| Method | Path | What it does |
|---|---|---|
| GET | /portal/keys | Lists the keys: { airline, keys: [{ id, name, createdAt, lastUsed, revokedAt, exp }] }. Operator code only. |
| POST | /portal/keys | Makes a key from { name } (up to 60 characters). Answers 201 with { id, name, key, products, exp }; the key is shown only here. Operator code only. |
| DELETE | /portal/keys/<id> | Revokes the key with this id (K- and 8 hex characters). Operator code only. |
What a refusal means
An error answers JSON with an error field that says what was wrong, for example { "error": "revisionDate YYYY-MM-DD" }.
| Status | Meaning | Typical cause |
|---|---|---|
| 400 | Bad request | A required field is missing or has the wrong format, or the body is not valid JSON. |
| 401 | No valid code | No Authorization header, an unknown or revoked key, or an expired code. |
| 403 | Not allowed | A crew code instead of the operator code, a code that does not include this product, or an API key used to manage keys or the webhook. |
| 404 | Not found | No such record in your airline, or no such path. Another airline's record answers 404 too. |
| 409 | Conflict | The revision is already published, a report already has 4 photos, or 20 keys are already active. |
| 413 | Too large | A JSON body or a file over its limit (limits are given with each endpoint). |
| 415 | Wrong content type | A JSON endpoint called without Content-Type: application/json, or a file of a type the endpoint does not take. |
Today board and webhook settings
| Method | Path | What it does |
|---|---|---|
| GET | /portal/today | What needs action now, across the products your airline holds. Up to 200 items, the most urgent first, then by date. |
| GET | /portal/webhook | The webhook: { url, events, createdAt, rotatedAt, available, log }. available lists the events your products allow; log holds the last 20 deliveries. The secret is never returned here. |
| PUT | /portal/webhook | Sets the endpoint and events from { url, events }. The URL must be https. The first time, it answers 201 with the secret, once. Operator code only. |
| POST | /portal/webhook/test | Sends a ping event now and answers the delivery: { id, event, at, status, ok, error? }. Operator code only. |
| POST | /portal/webhook/rotate | Makes a new secret and answers it once. The old secret stops signing at once. Operator code only. |
| DELETE | /portal/webhook | Removes the webhook. Operator code only. |
The Today board
Each item has a level (bad, warn or info), a kind, a title, a detail, an optional due date and a ref that points to the record: report, risk, audit, change, crew, notice, course, doc or flight.
curl -s "$CREWOPS/portal/today" -H "Authorization: Bearer $CREWOPS_KEY"
{
"airline": "DEMO",
"products": ["insight", "safety", "academy", "library"],
"at": "2026-10-14T07:00:00.000Z",
"counts": { "bad": 1, "warn": 1, "info": 0 },
"total": 2,
"items": [
{ "product": "safety", "level": "bad", "kind": "report_clock",
"title": "72-hour clock passed", "detail": "DEMO-261010-7KQ2M · Unstable approach",
"due": "2026-10-13T08:41:00.000Z", "ref": { "report": "DEMO-261010-7KQ2M" } },
{ "product": "academy", "level": "warn", "kind": "qual_expiring",
"title": "LPC expires within 30 days", "detail": "A1234 · cockpit",
"due": "2026-11-02", "ref": { "crew": "A1234" } }
]
}
Set the webhook
curl -s -X PUT "$CREWOPS/portal/webhook" \
-H "Authorization: Bearer $OPERATOR_CODE" \
-H "Content-Type: application/json" \
-d '{ "url": "https://sms.example.com/hooks/crewops",
"events": ["safety.report.created", "safety.report.updated"] }'
Reports, office records and exports
Report ids look like DEMO-261014-7KQ2M: your airline, the date received (YYMMDD) and five characters. A report's status is one of new, triage, investigation, action or closed.
| Method | Path | What it does |
|---|---|---|
| GET | /safety/reports | Your reports, newest first, as summaries: { airline, count, reports }. |
| GET | /safety/reports/<id> | One report in full, with its flight, answers, classification, actions, reply, photos and timeline. |
| PATCH | /safety/reports/<id> | Changes status, classification, actions, investigation, reply or note (fields below). Answers the report. |
| GET | /safety/export.csv | Every report as CSV, one row per report, with columns named after the Annex I fields. |
| GET | /safety/reports/<id>/e5x | One report as an ECCAIRS E5X file. |
| GET | /safety/export.e5x?ids=a,b | The listed reports in one E5X file (up to 200). |
| GET | /safety/export.e5x?reportable=1 | The reports classified as mandatory reportable, in one E5X file (up to 200, newest first). |
| GET | /safety/photo/<airline>/<id>/<file> | One photo of a report. The file names are in the report's photos list. |
| GET | /safety/office/<kind> | The office records of one kind, newest first: risks, audits or changes. |
| POST | /safety/office/<kind> | Makes a record. title is required; a risk also needs likelihood (1–5) and severity (A–E). |
| GET | /safety/office/<kind>/<id> | One record. |
| PATCH | /safety/office/<kind>/<id> | Changes fields, or adds a mitigation, finding or decision (below). |
| GET | /safety/weblink | Your web report link, made on the first call: { airline, key, url }. |
| POST | /safety/weblink/rotate | A new web report link. The old one stops working at once. |
What a report summary holds
id, type, family (safety or operational), urgent, status, anonymous, crewCode (absent when anonymous), title, date, flightNo, dep, arr, station, receivedAt, clockDue (the end of the 72-hour clock, or null), reportable, photos, rootCause, riskId, sp (the Samn-Perelli score of a fatigue report), source (web for a report sent through the web link) and actionDue (the earliest open action's due date).
What PATCH changes
Send only the fields you change. Add by (up to 60 characters) to name who did it on the report's timeline.
| Field | Value | Effect |
|---|---|---|
| status | "triage" | Moves the report. Moving it out of new records that the 72-hour clock was met. |
| classification | { occurrenceClass, category, eventType, eventTop, risk, reportable, mandatoryClass } | Replaces the classification as a whole. category is the ADREP mnemonic (for example "CABIN"); occurrenceClass is one of accident, serious_incident, major_incident, significant_incident, incident, no_safety_effect, not_determined, observation; eventTop is one of equipment, operational, personnel, organisational, consequential, other, unknown. reportable: false clears the 72-hour clock. |
| action | { text, owner?, due? } | Adds a corrective action. due is YYYY-MM-DD. |
| actionDone | 0 | Marks the action at this index as done. |
| investigation | { rootCause?, factors?, whys?, findings?, riskId? } | Only the keys you send change. riskId links a risk from the register. Never shown to the reporter. |
| reply | "…" | The reply the reporter reads on the phone or the follow-up page. |
| askMore | "…" | A question to the reporter, who can answer by adding information. |
| note | "…" | An internal note on the timeline. Never shown to the reporter. |
rootCause is one of human_skill, human_decision, human_perception, human_violation, communication_crm, procedure, training, organisation, equipment_technical, environment, external, undetermined. JSON bodies are limited to 64 KB.
curl -s -X PATCH "$CREWOPS/safety/reports/DEMO-261014-7KQ2M" \
-H "Authorization: Bearer $CREWOPS_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "action",
"classification": { "occurrenceClass": "incident", "category": "CABIN",
"eventTop": "operational", "reportable": true },
"action": { "text": "Brief cabin crew on the revised galley stowage check",
"owner": "Cabin safety", "due": "2026-10-31" },
"by": "Safety office" }'
E5X and CSV exports
The E5X file is built and checked against the public ECCAIRS data-bridge schema, so your office can upload it to the ECCAIRS 2 reporting portal or to an authority's ECCAIRS-based system. It carries the date and time, headline, occurrence class, ADREP occurrence category, location, flight number, phase, registration, event type (top level), narrative and reporting history. The operator, aircraft model and aerodrome codes are completed in the ECCAIRS portal. Your office makes the submission; Crewops does not submit. The taxonomy version in the file header is 9.0.0.1 unless you pass ?taxonomy= with another version.
# every report marked mandatory reportable, as one E5X file
curl -s -o reportable.e5x "$CREWOPS/safety/export.e5x?reportable=1" \
-H "Authorization: Bearer $CREWOPS_KEY"
# the register as CSV
curl -s -o safety.csv "$CREWOPS/safety/export.csv" -H "Authorization: Bearer $CREWOPS_KEY"
Risks, audits and changes
- risks (ids
R-…):title,hazard,area,owner,likelihood1–5,severityA–E,residualLikelihood,residualSeverity,reviewDate,status(open,mitigating,monitoring,closed),sources(report ids). The record comes back with itsindexandtolerabilityon the ICAO Doc 9859 5×5 matrix. PATCH also takesaddMitigation: { text, owner?, due? },mitigationDone: indexandaddSource: reportId. - audits (ids
A-…):title,area,kind(internal,external,authority,supplier),date,auditor,scope,status. PATCH also takesaddFinding: { text, level, requirement?, owner?, due? }with levellevel1,level2orobservation, andupdateFinding: { index, rootCause?, cap?, owner?, due?, evidence?, status? }. - changes (ids
C-…):title,description,kind,hazards,owner,plannedDate,reviewDate,status,risks(risk ids). PATCH also takesdecision: "approved"or"rejected"with an optionalnote.
curl -s -X POST "$CREWOPS/safety/office/risks" \
-H "Authorization: Bearer $CREWOPS_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Unstable approaches at night", "likelihood": 3, "severity": "C",
"owner": "Flight safety", "reviewDate": "2027-01-15",
"sources": ["DEMO-261014-7KQ2M"] }'
Photos and the web report link
Reporters add up to 4 photos to a report (JPEG, PNG or HEIC, up to 10 MB each) from the app or the web form. Your systems read them with the photo path above; each entry in a report's photos list has its file, size and at. The web report link opens the form for crews without the app; share it on your own channels, and rotate it when it has gone further than it should.
Records, notices and courses
crewCode on this page is the crew member's own code in your airline (2–12 capital letters or digits). An audience is { "all": true }, or { "roles": ["cockpit", "cabin"], "codes": ["A1234"] }. JSON bodies are limited to 256 KB.
| Method | Path | What it does |
|---|---|---|
| GET | /academy/records | Every crew member's qualification records, with paired (whether a phone holds the code). |
| POST | /academy/records | Imports records, up to 500 crew members per call. Each crew member's items replace the ones held. Answers { ok, written }. |
| GET | /academy/overview | { crew, expired, expiring, overdueNotices, notices }: expired items, items expiring within 30 days and notices past their date. |
| GET | /academy/requirements | What each role must hold: { requirements: { cockpit, cabin }, isDefault, kinds }. |
| PUT | /academy/requirements | Sets them from { cockpit: [kinds], cabin: [kinds] }. The matrix and the Today board read them. |
| GET | /academy/export.csv | The records as CSV: crew_code, role, name, kind, label, issued, expires, note, updated. |
| GET | /academy/notices | Notices, each with its count of signatures (acks). |
| POST | /academy/notices | Publishes a read-and-sign notice: { type, title, body?, url?, dueDate?, minutes?, audience?, by? }. |
| GET | /academy/notices/<id>/acks | Who signed it and when: { notice, acks: [{ crewCode, at }] }. |
| DELETE | /academy/notices/<id> | Withdraws the notice. It stays on record. |
| GET | /academy/courses | Courses, each with its lesson count, exam pass mark and number of completions. |
| POST | /academy/courses | Publishes a course (fields below). The answer never includes the exam's answers. |
| GET | /academy/courses/<id>/done | Who completed it (done) and every crew member's latest exam attempt (attempts). |
| DELETE | /academy/courses/<id> | Withdraws the course. It stays on record. |
| POST | /academy/files?name= | Uploads a lesson file, MP4 or PDF, as the raw body, up to 95 MB. Answers { fileUrl, kind, size }. |
| POST | /academy/scorm | Starts a SCORM package upload. Answers { pkg }. |
| PUT | /academy/scorm/<pkg>/<path> | Uploads one file of the unzipped package, as the raw body, up to 95 MB per file. Answers { url }. |
| POST | /academy/unbind | Releases a crew member's code from their old phone, from { crewCode }, so a new phone can pair. |
Import records from your HR or training system
Each record is a crew member with a list of items. kind is one of opc, lpc, line_check, sep, dg, crm, first_aid, route_competence, aerodrome_competence, recurrent_cabin, triennial_cabin, medical, licence, other. Dates are YYYY-MM-DD. label (up to 80 characters) names an item, and note takes up to 200.
curl -s -X POST "$CREWOPS/academy/records" \
-H "Authorization: Bearer $CREWOPS_KEY" \
-H "Content-Type: application/json" \
-d '{ "records": [
{ "crewCode": "A1234", "role": "cockpit", "name": "A. Pilot",
"items": [
{ "kind": "lpc", "issued": "2026-05-02", "expires": "2027-05-31" },
{ "kind": "medical", "label": "Class 1", "expires": "2027-02-28" },
{ "kind": "dg", "issued": "2025-11-10", "expires": "2027-11-30" } ] },
{ "crewCode": "C5678", "role": "cabin",
"items": [ { "kind": "recurrent_cabin", "expires": "2027-03-31" } ] } ] }'
Notices
type is one of crew_notice, safety_bulletin, safety_memo, coordination_sheet, security_notice, information, training_document, revision. url must be https.
curl -s -X POST "$CREWOPS/academy/notices" \
-H "Authorization: Bearer $CREWOPS_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "safety_bulletin", "title": "Lithium battery fires in the cabin",
"body": "Read the revised procedure before your next duty.",
"dueDate": "2026-10-31", "audience": { "roles": ["cabin"] } }'
Courses
A course takes { title, kind?, mode, dueDate?, minutes?, lessons, quiz?, audience?, by? }. mode is required (with a due date) or self for self-study. kind takes the qualification kinds above.
- lessons, up to 40:
{ title, kind, url, minutes? }.kindisvideo(MP4),pdf,linkorscorm;urlmust be https. Upload video and PDF to/academy/filesand use thefileUrl, so crews can download the lesson and study offline. - quiz, optional:
{ passMark, questions: [{ q, options, answer }] }, with 2–6 options,answeras the index of the right option, up to 60 questions and a pass mark of 50–100 (75 if not given). The exam is graded on the server; the answers never reach the phone. Attempts are kept.
curl -s -X POST "$CREWOPS/academy/courses" \
-H "Authorization: Bearer $CREWOPS_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Dangerous goods recurrent", "kind": "dg", "mode": "required",
"dueDate": "2026-12-15", "audience": { "all": true },
"lessons": [
{ "title": "Recognising hidden dangerous goods", "kind": "video", "minutes": 18,
"url": "https://updates.hawkmason.com/academy/file/DEMO/<file>.mp4" } ],
"quiz": { "passMark": 80, "questions": [
{ "q": "Who must be told first about a spare lithium battery in checked baggage?",
"options": ["The commander", "The passenger", "Ground handling"], "answer": 0 } ] } }'
SCORM packages
SCORM 1.2 and 2004 packages are uploaded unzipped, one file at a time, and then used as a lesson. The phone keeps the package offline and plays it in the app; the module's own result (passed, completed or failed, with its score) completes the course.
- 1.
POST /academy/scormanswers a package id,pkg. - 2.
PUT /academy/scorm/<pkg>/<path>for every file in the package, keeping its path inside the package (no..). Each answers the file'surl. - 3.
POST /academy/courseswith a lesson{ "kind": "scorm", "url": <the launch file's url>, "files": [every path], "version": "1.2" or "2004" }. The launch file is the one the package's manifest names.
PKG=$(curl -s -X POST "$CREWOPS/academy/scorm" -H "Authorization: Bearer $CREWOPS_KEY" | jq -r .pkg)
cd crm-module/ # the unzipped package
find . -type f | sed 's|^\./||' | while read -r f; do
curl -s -X PUT "$CREWOPS/academy/scorm/$PKG/$f" \
-H "Authorization: Bearer $CREWOPS_KEY" --data-binary "@$f" > /dev/null
done
# then publish a course whose lesson is
# { "title": "CRM module", "kind": "scorm", "version": "2004",
# "url": "https://updates.hawkmason.com/academy/sco/DEMO/$PKG/index.html",
# "files": ["index.html", "imsmanifest.xml", …] }
Paths with spaces or other special characters must be percent-encoded in the URL.
Controlled documents and revisions
A document has a stable code, such as OM-A (capital letters, digits, dots and dashes). Publishing a revision under an existing code replaces the current revision and keeps the history. JSON bodies are limited to 128 KB.
| Method | Path | What it does |
|---|---|---|
| GET | /library/docs | The current revision of every document, with its number of revisions and confirmations: { airline, crew, documents }. |
| POST | /library/docs | Publishes a document (201) or a new revision of one (200). A revision number already published answers 409. |
| GET | /library/docs/<code> | The document with its full revision history. |
| GET | /library/docs/<code>/acks | Who confirmed the current revision: { code, revision, acks: [{ crewCode, at }] }. |
| DELETE | /library/docs/<code> | Withdraws the document. It stays in the history; a new revision publishes it again. |
| POST | /library/files?name= | Uploads a PDF as the raw body, up to 60 MB. Answers { fileUrl, name, size }. The file is served only to your paired crews and to your operator code and keys. |
Publish from your document system
Required: code, revision and revisionDate; a new document also needs title and type (manual, procedure, policy, bulletin, form, checklist, regulation, certificate, other). Optional: category, owner, audience, effectiveDate, changeLog, fileUrl (https), pages, approved, requiresAck, ackDue (the read-by date) and by.
# 1. upload the PDF
FILE_URL=$(curl -s -X POST "$CREWOPS/library/files?name=OM-A%20rev%2014.pdf" \
-H "Authorization: Bearer $CREWOPS_KEY" -H "Content-Type: application/pdf" \
--data-binary @om-a-rev14.pdf | jq -r .fileUrl)
# 2. publish the revision with a read-and-confirm
curl -s -X POST "$CREWOPS/library/docs" \
-H "Authorization: Bearer $CREWOPS_KEY" -H "Content-Type: application/json" \
-d "{ \"code\": \"OM-A\", \"revision\": \"14\", \"revisionDate\": \"2026-10-14\",
\"effectiveDate\": \"2026-11-01\", \"fileUrl\": \"$FILE_URL\",
\"changeLog\": \"8.3.2 revised: stabilised approach criteria\",
\"requiresAck\": true, \"ackDue\": \"2026-10-31\" }"
Crew post-flight ratings
The ratings crews have chosen to share after their flights, for the dates you ask. A crew member shares only if they switch sharing on in the app.
| Method | Path | What it does |
|---|---|---|
| GET | /insight/ratings?from=&to= | Ratings for flights between the two dates (YYYY-MM-DD, both included), by date and flight: { airline, count, ratings }. |
Each rating has date, flightNo, dep, arr, scale (samn-perelli or kss), value, ratedAt, afterMin, sectors, role, crewCode when the crew member gave it, crewId (a pseudonym of the phone) and receivedAt. Nothing else about the crew member is sent: no sleep, no health entries and no roster beyond the sector rated.
curl -s "$CREWOPS/insight/ratings?from=2026-10-01&to=2026-10-14" \
-H "Authorization: Bearer $CREWOPS_KEY"
Hear about events as they happen
Set one https endpoint in the operator portal, under Connections, and pick the events it should receive. You can pick only the events of products your airline holds.
| Event | When it fires | data |
|---|---|---|
| safety.report.created | A report arrives, from the app or the web report link. | The report, without the reporter's reply token, the phone's pseudonym or the timeline, with family and the number of photos. |
| safety.report.updated | The office changes status, classification, actions, investigation, reply or asks for more. An internal note alone does not fire it. | The same view of the report, with changed: the fields that changed. |
| academy.notice.acknowledged | A crew member signs a notice. | { noticeId, title, type, crewCode, at } |
| academy.course.completed | A crew member completes a course. | { courseId, title, kind, crewCode, at, verified }; with score and attempts when a server-graded exam was passed (verified: true), or scorm: { status, score } from a SCORM module. |
| library.revision.confirmed | A crew member confirms a revision. | { code, title, revision, crewCode, at } |
| ping | You press the test button, or call /portal/webhook/test. | { message, events } |
The delivery
Each delivery is a POST with a JSON body:
POST /hooks/crewops HTTP/1.1
Content-Type: application/json
User-Agent: Crewops-Webhooks/1
X-Crewops-Event: academy.course.completed
X-Crewops-Signature: t=1791969164,v1=5d0c…e19a
{
"id": "evt_3f9c0a1b2c4d5e6f7a8b",
"event": "academy.course.completed",
"airline": "DEMO",
"at": "2026-10-14T09:12:44.512Z",
"data": {
"courseId": "C-261014-K7QD", "title": "Dangerous goods recurrent", "kind": "dg",
"crewCode": "A1234", "at": "2026-10-14T09:12:44.501Z",
"verified": true, "score": 90, "attempts": 1
}
}
- One try: each event is sent once, with a 5-second timeout and no retries. Answer with a 2xx status quickly and do the slow work afterwards.
- The log: the portal lists the last 20 deliveries with their status, so you can see a failed one and fetch the record through the API.
- Duplicates: keep the
idof each event you have handled and ignore one you have seen. - The secret: it starts with
whsec_and is shown once, when the webhook is made or the secret rotated.
Check the signature
The header x-crewops-signature is t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256, keyed with your secret, of the string <t>.<raw body>. Compute it over the body exactly as received, before any JSON parsing, compare it in constant time, and reject a t more than five minutes old.
Node.js (no dependencies)
import http from 'node:http';
import crypto from 'node:crypto';
const SECRET = process.env.CREWOPS_WEBHOOK_SECRET; // whsec_…
function verify(rawBody, header, toleranceS = 300) {
const parts = Object.fromEntries(String(header).split(',').map((p) => p.split('=', 2)));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceS) return false;
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex');
const given = String(parts.v1 ?? '');
return given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
http.createServer((req, res) => {
let raw = '';
req.setEncoding('utf8');
req.on('data', (chunk) => (raw += chunk));
req.on('end', () => {
if (!verify(raw, req.headers['x-crewops-signature'] ?? '')) return res.writeHead(400).end();
const { id, event, airline, at, data } = JSON.parse(raw);
// remember id, queue the work, answer within 5 s
res.writeHead(204).end();
});
}).listen(8080);
Python (Flask)
import hashlib, hmac, json, os, time
from flask import Flask, request
SECRET = os.environ["CREWOPS_WEBHOOK_SECRET"].encode() # whsec_...
app = Flask(__name__)
def verify(raw_body: bytes, header: str, tolerance_s: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance_s:
return False
expected = hmac.new(SECRET, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
@app.post("/hooks/crewops")
def crewops_hook():
raw = request.get_data() # the body exactly as received
if not verify(raw, request.headers.get("x-crewops-signature", "")):
return "", 400
event = json.loads(raw) # id, event, airline, at, data
# remember event["id"], queue the work, answer within 5 s
return "", 204
Connecting a system we have not listed?
Tell us which system it is and what it should send or receive. The developer who wrote this API reads it and answers within a working day, in English or Turkish.
Crewops is independent software by Hawk & Mason Studios. Regulation references say which rules a feature is built around; they are not a statement of approval by any authority. Your procedures, records and approved systems prevail.