For your IT team

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 https://updates.hawkmason.com · JSON · Bearer operator code or API key

Basics

Base URL, format and sign-in

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"
API keys

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.

MethodPathWhat it does
GET/portal/keysLists the keys: { airline, keys: [{ id, name, createdAt, lastUsed, revokedAt, exp }] }. Operator code only.
POST/portal/keysMakes 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.
Errors

What a refusal means

An error answers JSON with an error field that says what was wrong, for example { "error": "revisionDate YYYY-MM-DD" }.

StatusMeaningTypical cause
400Bad requestA required field is missing or has the wrong format, or the body is not valid JSON.
401No valid codeNo Authorization header, an unknown or revoked key, or an expired code.
403Not allowedA 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.
404Not foundNo such record in your airline, or no such path. Another airline's record answers 404 too.
409ConflictThe revision is already published, a report already has 4 photos, or 20 keys are already active.
413Too largeA JSON body or a file over its limit (limits are given with each endpoint).
415Wrong content typeA JSON endpoint called without Content-Type: application/json, or a file of a type the endpoint does not take.
Operator portal

Today board and webhook settings

MethodPathWhat it does
GET/portal/todayWhat needs action now, across the products your airline holds. Up to 200 items, the most urgent first, then by date.
GET/portal/webhookThe 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/webhookSets 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/testSends a ping event now and answers the delivery: { id, event, at, status, ok, error? }. Operator code only.
POST/portal/webhook/rotateMakes a new secret and answers it once. The old secret stops signing at once. Operator code only.
DELETE/portal/webhookRemoves 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"] }'
Crewops Safety

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.

MethodPathWhat it does
GET/safety/reportsYour 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.csvEvery report as CSV, one row per report, with columns named after the Annex I fields.
GET/safety/reports/<id>/e5xOne report as an ECCAIRS E5X file.
GET/safety/export.e5x?ids=a,bThe listed reports in one E5X file (up to 200).
GET/safety/export.e5x?reportable=1The 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/weblinkYour web report link, made on the first call: { airline, key, url }.
POST/safety/weblink/rotateA 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.

FieldValueEffect
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.
actionDone0Marks 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

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.

Crewops Academy

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.

MethodPathWhat it does
GET/academy/recordsEvery crew member's qualification records, with paired (whether a phone holds the code).
POST/academy/recordsImports 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/requirementsWhat each role must hold: { requirements: { cockpit, cabin }, isDefault, kinds }.
PUT/academy/requirementsSets them from { cockpit: [kinds], cabin: [kinds] }. The matrix and the Today board read them.
GET/academy/export.csvThe records as CSV: crew_code, role, name, kind, label, issued, expires, note, updated.
GET/academy/noticesNotices, each with its count of signatures (acks).
POST/academy/noticesPublishes a read-and-sign notice: { type, title, body?, url?, dueDate?, minutes?, audience?, by? }.
GET/academy/notices/<id>/acksWho signed it and when: { notice, acks: [{ crewCode, at }] }.
DELETE/academy/notices/<id>Withdraws the notice. It stays on record.
GET/academy/coursesCourses, each with its lesson count, exam pass mark and number of completions.
POST/academy/coursesPublishes a course (fields below). The answer never includes the exam's answers.
GET/academy/courses/<id>/doneWho 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/scormStarts 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/unbindReleases 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.

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.

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.

Crewops Library

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.

MethodPathWhat it does
GET/library/docsThe current revision of every document, with its number of revisions and confirmations: { airline, crew, documents }.
POST/library/docsPublishes 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>/acksWho 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\" }"
Crewops Insight

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.

MethodPathWhat 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"
Webhooks

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.

EventWhen it firesdata
safety.report.createdA 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.updatedThe 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.acknowledgedA crew member signs a notice.{ noticeId, title, type, crewCode, at }
academy.course.completedA 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.confirmedA crew member confirms a revision.{ code, title, revision, crewCode, at }
pingYou 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
  }
}

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
Talk to us

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.

hello@hawkmason.com

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.