# CrewPass Partner API — full documentation Concatenation of every published documentation page, in canonical reading order. Generated by `docs/site/scripts/build_llms_full.py`. Spec source of truth: https://docs.crewpass.co.uk/partners/api-reference/openapi.json --- # CrewPass Partner API Source: https://docs.crewpass.co.uk/partners/introduction The CrewPass Partner API gives a **management company** a secure, read-only window onto the crew across its fleet: the vessels on your account, the people on those vessels, their documents and compliance, and a real-time feed of changes. It is a front door onto data that already lives in CrewPass — not a second database — so what you read here always matches what your CrewPass dashboard shows. ## What you can do Every vessel on your CrewPass account. Everyone on your vessels, with verification and background-check status. Contact details, experience, photo, and (with consent) identity fields. List certificates with issuer + verification status, and download the files. Is a crew member fully certified for their role? STCW, medical, and more. Be notified when a document, compliance, status, or profile changes. New here? Start with the [Quickstart](/quickstart) — credentials to your first crew read in about 15 minutes. ## How access works {#access} Every call has to pass three checks before it returns data: 1. **You are who you say you are.** Each read authenticates with your API key as a Bearer token over TLS; reads are not signed (see [Authentication](/authentication)). 2. **You're allowed to see this kind of data.** Your CrewPass plan decides which capabilities you have — listing crew, reading documents, reading compliance, and so on. 3. **The crew member has agreed, and they're on your fleet.** A crew member consents to share their data with you when they accept a place on one of your vessels. Someone outside your fleet comes back as `404` (never `403`) — so you can't probe for people who exist elsewhere in CrewPass. Sensitive identity fields (date of birth, passport, address) need an extra, explicit consent. Your **fleet** is simply the vessels on your CrewPass employer account — the same list as your dashboard. There's nothing to set up or attach. ## Base URLs | Environment | Base URL | |---|---| | Production | `https://partners.crewpass.co.uk` | | Dev / sandbox | `https://crewpass-api-v2-dev-258178043395.europe-west4.run.app` | Everything lives under `/api/v2/`. ## Not in this version This release is **read-only plus webhooks**. Onboarding crew, writing vessel placements, and ordering background checks through the API are coming in a later version. Background-check **status** is readable today; the verification provider behind it is never named. ## For AI coding agents These docs are built to be fed to coding agents (Claude, Cursor, GPT): [`/llms.txt`](/llms.txt) is a manifest of every page, [`/llms-full.txt`](/llms-full.txt) is the whole site as one file, every page also resolves as `.md`, and the OpenAPI contract is served raw at [`openapi.json`](/api-reference/openapi.json). --- # Quickstart Source: https://docs.crewpass.co.uk/partners/quickstart This walks the management-company flow end to end: **list your vessels → list your fleet → look up a crew member → read their profile, documents, and compliance, and download a document**. Every v1 read authenticates with your API key as a Bearer token over TLS and is not signed, so each call carries the same `Authorization` header and nothing more (see [Authentication](/authentication)). CrewPass issues you a partner API key (`cpk_live_*` and `cpk_test_*`). Keep it server-side. ```bash export CPK_KEY="cpk_live_xxxxxxxxxxxxxxxxxxxxxxxx" export BASE="https://partners.crewpass.co.uk" ``` `GET /api/v2/partners/me` returns your identity and granted scopes. It's the first call any partner makes. ```bash cURL curl -sS "$BASE/api/v2/partners/me" -H "Authorization: Bearer $CPK_KEY" ``` ```json Response { "partner_id": "prt_123", "display_name": "Example Fleet Management", "partner_type": "customer", "status": "active", "mode": "live", "subscription_plan": "management", "granted_scopes": ["crew:compliance:read", "crew:documents:read", "crew:documents:download", "crew:profile:read", "crew:profile:full:read", "crew:status:read", "vessels:fleet:read"] } ``` `GET /api/v2/employers/me/vessels` returns the vessels on your account. ```bash cURL curl -sS "$BASE/api/v2/employers/me/vessels" \ -H "Authorization: Bearer $CPK_KEY" ``` ```json Response { "items": [ { "vessel_id": "ves_abc", "vessel_name": "M/Y Example", "imo": "9780987", "mmsi": "232003200", "flag_state": "Cayman Islands", "vessel_type": "M/Y" } ] } ``` `GET /api/v2/employers/me/fleet` returns every crew member on your vessels, their verification + background-check status, and a count of documents expiring soon. ```python Python import httpx r = httpx.get( f"{BASE}/api/v2/employers/me/fleet", params={"include_documents_expiring_within_days": 30}, headers={"Authorization": f"Bearer {CPK_KEY}"}, ) print(r.json()) ``` ```json Response { "items": [ { "crew_unique_id": "crew_001", "name": "A. Crew", "vessel_id": "ves_abc", "vessel_name": "M/Y Example", "role": "Chief Engineer", "status": "active", "background_check_status": "completed", "documents_expiring": 1, "as_of": "2026-06-08T10:00:00Z" } ], "next_cursor": null, "expiring_within_days": 30 } ``` Paginate with `?cursor=&limit=50` (max `limit` 200). `status` and `background_check_status` appear only when `crew:status:read` is effective for that crew member. If you hold crew by email, resolve to a `crew_unique_id`. ```bash cURL curl -sS "$BASE/api/v2/employers/me/crew/lookup" \ -H "Authorization: Bearer $CPK_KEY" -H "Content-Type: application/json" \ -d '{"email":"crew@example.com"}' ``` A crew member not on one of your vessels returns `404` — you can never enumerate the wider CrewPass user base. With a `crew_unique_id`, read the detail (the `GET`s carry only the Bearer header; the compliance check is a read-only `POST` with a JSON body): ```bash cURL H=(-H "Authorization: Bearer $CPK_KEY") # Profile (identity fields included when crew:profile:full:read is effective) curl -sS "$BASE/api/v2/employers/me/crew/crew_001/profile" "${H[@]}" # Documents (issuer + verification_status) curl -sS "$BASE/api/v2/employers/me/crew/crew_001/documents" "${H[@]}" # Compliance snapshot (POST with body) — role / STCW / medical breakdown curl -sS "$BASE/api/v2/employers/me/crew/crew_001/compliance-checks" \ -H "Authorization: Bearer $CPK_KEY" -H "Content-Type: application/json" -d '{}' ``` `GET …/documents/{document_id}/download` returns a short-lived, CrewPass-hosted link (never a raw storage URL). Follow `download_url` to stream the file. ```json Response { "document_id": "doc_1", "download_url": "https://partners.crewpass.co.uk/api/v2/files/eyJ…", "expires_at": "2026-06-08T10:15:00Z", "content_type": "application/pdf", "file_name": "PST_certificate.pdf" } ``` ## Next steps Now dive into a capability: - [See your crew](/guides/crew) · [Read a profile](/guides/profiles) - [Documents & downloads](/guides/documents) · [Check compliance](/guides/compliance) - [Webhooks](/webhooks) — get notified instead of polling. - [Errors](/errors/index) — every error code and how to handle it. --- # Authentication Source: https://docs.crewpass.co.uk/partners/authentication The Partner API v1 is a read surface. Every request authenticates with a single credential: - **An API key**, presented as a Bearer token over TLS. It identifies your partner account and carries your granted scopes. There is no request body to sign on a read, so v1 reads do **not** use a request signature. Request signing (HMAC) returns for the v2 write surface, which is described below and is currently disabled. ## API keys Keys are issued by CrewPass and look like: | Prefix | Meaning | |---|---| | `cpk_live_…` | Live key, operates on production data. | | `cpk_test_…` | Test-mode key, operates on test-flagged data. | Present the key as a Bearer token (preferred) or in the `X-Partner-API-Key` header: ```http Authorization: Bearer cpk_live_xxxxxxxxxxxxxxxxxxxxxxxx ``` Your granted scopes are **derived from your CrewPass plan**, so you do not manage a separate API permission set. Call [`GET /api/v2/partners/me`](/api-reference/overview) to see your identity and full granted-scope list. It is the first call any partner makes. Keep your API key server-side. Never embed it in a browser or mobile app. Public read-only widget keys (`cppk_*`) are a separate, future surface and are not accepted here. ## Calling a read endpoint Every v1 read takes the same Bearer header and nothing else: ```bash cURL curl -sS https://partners.crewpass.co.uk/api/v2/employers/me/vessels \ -H "Authorization: Bearer $CPK_KEY" ``` ```python Python import httpx resp = httpx.get( "https://partners.crewpass.co.uk/api/v2/employers/me/vessels", headers={"Authorization": f"Bearer {CPK_KEY}"}, ) resp.raise_for_status() ``` ```javascript Node const resp = await fetch( "https://partners.crewpass.co.uk/api/v2/employers/me/vessels", { headers: { Authorization: `Bearer ${CPK_KEY}` } }, ); ``` A missing or invalid key returns [`invalid_api_key`](/errors/invalid_api_key). Calls are still rate-limited per partner (see [Rate limits](/rate-limits)) and recorded in your audit log. ## Request signing (v2 write surface) Mutating endpoints (vessel attach, crew placements, background-check issuance) are deferred to v2 and are not part of this release. When they ship, each mutating request is signed with an HMAC-SHA256 body signature so the body and timing are provably genuine and not replayed. The scheme uses three headers and binds a single-use nonce into the signature: | Header | Value | |---|---| | `X-CrewPass-Timestamp` | Unix time in **seconds**. Must be within ±300s of server time. | | `X-CrewPass-Nonce` | A unique, single-use token per request. | | `X-CrewPass-Signature` | `v1=` | This is documented now so integrators can plan ahead; it is **not** required for any v1 read. ```python Python import hashlib, hmac, time, uuid def signed_headers(api_key: str, secret: str, body: bytes = b"") -> dict[str, str]: ts = str(int(time.time())) nonce = uuid.uuid4().hex msg = f"{ts}.{nonce}.".encode() + body sig = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest() return { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "X-CrewPass-Timestamp": ts, "X-CrewPass-Nonce": nonce, "X-CrewPass-Signature": f"v1={sig}", } ``` ```javascript Node import crypto from "node:crypto"; export function signedHeaders(apiKey, secret, body = "") { const ts = Math.floor(Date.now() / 1000).toString(); const nonce = crypto.randomUUID(); const sig = crypto .createHmac("sha256", secret) .update(`${ts}.${nonce}.` + body) .digest("hex"); return { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "X-CrewPass-Timestamp": ts, "X-CrewPass-Nonce": nonce, "X-CrewPass-Signature": `v1=${sig}`, }; } ``` Outbound **webhook** deliveries are signed by CrewPass with a separate scheme so you can verify they are genuine. That is unrelated to request signing; see [Webhooks](/webhooks) for how to verify a delivery signature. --- # List your vessels Source: https://docs.crewpass.co.uk/partners/guides/vessels Your fleet is the set of vessels on your CrewPass employer account — the same list your dashboard shows. This is usually the first thing you read: it gives you the `vessel_id`s you'll see attached to crew elsewhere. **Requires** the `vessels:fleet:read` capability. Authenticated with your API key as a Bearer token over TLS; reads are not signed (see [Authentication](/authentication)). ## Get your vessels ```bash cURL curl -sS "$BASE/api/v2/employers/me/vessels" \ -H "Authorization: Bearer $CPK_KEY" ``` ```python Python import httpx r = httpx.get(f"{BASE}/api/v2/employers/me/vessels", headers={"Authorization": f"Bearer {CPK_KEY}"}) for v in r.json()["items"]: print(v["vessel_id"], v["vessel_name"], v["flag_state"]) ``` ```json Response { "items": [ { "vessel_id": "ves_abc", "vessel_name": "M/Y Example", "imo": "9780987", "mmsi": "232003200", "flag_state": "Cayman Islands", "vessel_type": "M/Y" } ] } ``` | Field | What it is | |---|---| | `vessel_id` | The opaque id you'll use to identify this vessel everywhere else. | | `vessel_name` | Display name. | | `imo` / `mmsi` | Vessel registration identifiers (strings). | | `flag_state` | Flag country. | | `vessel_type` | e.g. `M/Y`. | ## Next - [See your crew](/guides/crew) — the people on these vessels. --- # See your crew Source: https://docs.crewpass.co.uk/partners/guides/crew The fleet roster is the heart of the API: every crew member on your vessels, with their verification status, background-check status, and a count of documents about to expire. Each crew member has a **`crew_unique_id`** — the id you pass to read their profile, documents, and compliance. **Requires** `vessels:fleet:read`. Status fields also need `crew:status:read`, which your management plan includes. Authenticated with your API key as a Bearer token over TLS; reads are not signed. ## List everyone on your fleet ```bash cURL curl -sS "$BASE/api/v2/employers/me/fleet?include_documents_expiring_within_days=30" \ -H "Authorization: Bearer $CPK_KEY" ``` ```json Response { "items": [ { "crew_unique_id": "crew_001", "name": "A. Crew", "vessel_id": "ves_abc", "vessel_name": "M/Y Example", "role": "Chief Engineer", "verification_status": "verified", "background_check_status": "completed", "documents_expiring": 1, "as_of": "2026-06-08T10:00:00Z" } ], "next_cursor": null, "expiring_within_days": 30 } ``` | Field | What it is | |---|---| | `crew_unique_id` | The crew member's id — use it for profile, documents, compliance. | | `role` | Their position on this vessel. | | `verification_status` | Whether CrewPass has verified the crew member. | | `background_check_status` | Standardised status — see [Background-check status](/guides/background-checks). | | `documents_expiring` | How many current documents expire within the window you asked for. | Tune the expiry window with `?include_documents_expiring_within_days=30` (0–730). `verification_status` and `background_check_status` are shown only when that crew member has consented to share status with you. ### Paging Large fleets are paged. When `next_cursor` is set, pass it back to get the next page: ``` GET /api/v2/employers/me/fleet?cursor=&limit=50 # limit up to 200 ``` ## Find one crew member by email If you already hold a crew member by email (say, from your own system), resolve them to a `crew_unique_id`: ```bash cURL curl -sS "$BASE/api/v2/employers/me/crew/lookup" \ -H "Authorization: Bearer $CPK_KEY" -H "Content-Type: application/json" \ -d '{"email":"crew@example.com"}' ``` ```json Response { "crew_unique_id": "crew_001", "status": "verified" } ``` A crew member who isn't on one of your vessels returns `404` — you can never use this to discover people outside your fleet. ## Next - [Read a crew profile](/guides/profiles) - [Documents & downloads](/guides/documents) - [Check compliance](/guides/compliance) --- # Read a crew profile Source: https://docs.crewpass.co.uk/partners/guides/profiles Once you have a `crew_unique_id` (from [your crew list](/guides/crew)), read their profile. You always get the base profile; sensitive **identity** fields appear only when the crew member has explicitly consented to share them with you. **Requires** `crew:profile:read`. Identity fields additionally require `crew:profile:full:read` **and** the crew member's consent. Authenticated with your API key as a Bearer token over TLS; reads are not signed. ## Get a profile ```bash cURL curl -sS "$BASE/api/v2/employers/me/crew/crew_001/profile" \ -H "Authorization: Bearer $CPK_KEY" ``` ```json Response { "crew_unique_id": "crew_001", "name": "Alex Crew", "rank": "Chief Engineer", "nationality": "British", "languages": [{ "language": "English", "proficiency": "Native" }], "skills": ["MEOL", "AEC"], "years_experience": "12", "bio": "…", "email": "alex@example.com", "phone": "+44 7700 900000", "profile_photo_url": "https://partners.crewpass.co.uk/api/v2/employers/me/crew/crew_001/photo", "employment_history": [ { "vessel_name": "M/Y Example", "role": "2nd Engineer", "started_at": "2022-01-01", "ended_at": "2024-06-01", "source": "employer_confirmed" } ], "full_profile": true, "date_of_birth": "1990-04-12", "address": { "line1": "…", "city": "…", "country": "United Kingdom", "postcode": "…" }, "passport": { "nationality": "British", "number": "123456789", "expiry": "2030-04-01" }, "visas": [{ "type": "B1/B2", "country": "USA", "expiry": "2029-01-01" }] } ``` ### Base fields (always) `name`, `rank`, `nationality`, `languages`, `skills`, `years_experience`, `bio`, `email`, `phone`, `profile_photo_url`. ### Identity fields (consent + `full_profile: true`) `date_of_birth`, `address`, `passport`, `visas`, and `employment_history`. When the crew member hasn't consented to identity sharing, `full_profile` is `false` and these fields are simply absent — the rest of the profile still returns. Don't treat a missing identity field as an error. ## Photo `profile_photo_url` points at a CrewPass-hosted photo proxy (never a raw storage link). Fetch it the same way as any other `GET`, with the Bearer header: ``` GET /api/v2/employers/me/crew/crew_001/photo ``` It streams the image, or returns `404` if the crew member has no photo. ## Next - [Documents & downloads](/guides/documents) - [Check compliance](/guides/compliance) --- # Documents & downloads Source: https://docs.crewpass.co.uk/partners/guides/documents For any crew member on your fleet you can list their current documents — with the issuing authority and CrewPass's verification status — and download the actual files through a short-lived, CrewPass-hosted link. **Requires** `crew:documents:read` to list, and `crew:documents:download` to fetch a file. Both authenticated with your API key as a Bearer token over TLS; reads are not signed. ## 1. List documents ```bash cURL curl -sS "$BASE/api/v2/employers/me/crew/crew_001/documents" \ -H "Authorization: Bearer $CPK_KEY" ``` ```json Response { "crew_unique_id": "crew_001", "items": [ { "document_id": "doc_1", "type": "STCW", "category": "STCW Modules", "title": "Personal Survival Techniques", "issuer": "Maritime & Coastguard Agency", "verification_status": "verified", "issue_date": "2023-03-01", "expiry_date": "2028-03-01", "document_number": "PST-12345" } ] } ``` Only the **current** version of each document is returned (superseded re-uploads are hidden). What the key fields mean: | Field | Meaning | |---|---| | `issuer` | The issuing authority CrewPass resolved (e.g. a flag state or training school). `null` if it couldn't be determined. Never a verification vendor. | | `verification_status` | CrewPass's check on the document: `verified`, `pending`, `rejected`, or `expired`. | | `expiry_date` | When it expires. Combine with the fleet's `documents_expiring` count to spot renewals. | | `document_number` | The certificate's own reference number. | ## 2. Download a file Downloading is a **two-step** flow. First ask for a link; then follow it. The link is hosted on a CrewPass domain, expires in about 15 minutes, and the underlying storage location is never exposed. ```bash cURL # Step A — get a short-lived link curl -sS "$BASE/api/v2/employers/me/crew/crew_001/documents/doc_1/download" \ -H "Authorization: Bearer $CPK_KEY" ``` ```json Response { "document_id": "doc_1", "download_url": "https://partners.crewpass.co.uk/api/v2/files/eyJ…", "expires_at": "2026-06-08T10:15:00Z", "content_type": "application/pdf", "file_name": "PST_certificate.pdf" } ``` ```bash cURL # Step B — follow the link (no API key needed; the link itself is the credential) curl -sSL "" -o PST_certificate.pdf ``` ## Download every document for a crew member A common job — pull all of someone's certificates into your own system. List, then download each one: ```python Python import httpx auth = {"Authorization": f"Bearer {CPK_KEY}"} base = f"{BASE}/api/v2/employers/me/crew/crew_001" docs = httpx.get(f"{base}/documents", headers=auth).json() for d in docs["items"]: link = httpx.get( f"{base}/documents/{d['document_id']}/download", headers=auth, ).json() pdf = httpx.get(link["download_url"]).content # follow the branded link with open(link["file_name"], "wb") as f: f.write(pdf) print("saved", link["file_name"], f"({len(pdf)} bytes)") ``` Each download link is single-purpose and short-lived. If `crew:documents:download` isn't enabled, or the crew member is off your fleet, you get `403` / `404` rather than a link. If the file store is briefly unreachable you get a `503` — never an unsigned storage URL. ## Next - [Check compliance](/guides/compliance) — which of these documents satisfy the role's requirements. --- # Check compliance Source: https://docs.crewpass.co.uk/partners/guides/compliance Compliance answers one question: **is this crew member fully certified for the role they hold on this vessel?** CrewPass works that out for you by matching the role's requirements against the crew member's verified documents — you read the result, you don't compute it. **Requires** `crew:compliance:read`. Authenticated with your API key as a Bearer token over TLS; reads are not signed. It's a read-only `POST` with an empty JSON body. ## What feeds compliance Three kinds of requirement are checked, and each shows up in the response: - **Role certificates** (`source: "explicit"`) — the specific certificates the crew member's position requires (e.g. Advanced Fire Fighting for a senior role). - **STCW basic safety training** (`source: "stcw"`) — the standard safety modules (Personal Survival, Fire Prevention, First Aid, Personal Safety…), each with its own status and expiry. - **Medical** (`source: "medical"`) — the ENG1 (or equivalent) seafarer medical: its fitness category and expiry. CrewPass never exposes the doctor or clinic. ## Get a compliance snapshot ```bash cURL curl -sS "$BASE/api/v2/employers/me/crew/crew_001/compliance-checks" \ -H "Authorization: Bearer $CPK_KEY" -H "Content-Type: application/json" \ -d '{}' ``` If a crew member is on more than one of your vessels, add `?vessel_id=` (from their fleet row) so the snapshot is for the right position. ```json Response { "crew_unique_id": "crew_001", "overall_status": "at_risk", "requirements_total": 8, "requirements_met": 7, "requirements_expiring": 1, "requirements_expired": 0, "requirements_missing": 0, "stcw_completion_percentage": 100.0, "next_expiry_date": "2026-08-01", "requirements": [ { "requirement_key": "eng1", "title": "ENG1 Medical", "source": "medical", "status": "expiring", "expiry_date": "2026-08-01", "days_until_expiry": 54 }, { "requirement_key": "advanced_firefighting", "title": "Advanced Fire Fighting", "source": "explicit", "status": "met", "document_id": "doc_5", "expiry_date": "2027-02-01" } ], "stcw": { "completion_percentage": 100.0, "modules": [ { "code": "A-VI/1-1", "name": "Personal Survival", "status": "met", "expiry_date": "2028-03-01" } ] }, "medical": { "status": "valid", "expiry_date": "2026-08-01", "fit_category": "Fit" } } ``` ### Reading the result Start with **`overall_status`**: | `overall_status` | Meaning | |---|---| | `compliant` | Everything met, nothing expiring within 90 days. | | `at_risk` | All met, but at least one requirement expires soon. | | `non_compliant` | At least one requirement is missing, expired, or rejected. | | `no_role` | The crew member has no assigned role to check against. | Then drill in: - **`requirements[]`** is the actionable list — each role/STCW/medical requirement with its `status` (`met` · `expiring` · `expired` · `missing` · `pending`), the `document_id` that satisfies it (for role certificates), and `days_until_expiry`. - **`stcw`** breaks out the basic-safety modules and overall percentage. - **`medical`** gives the fitness category and expiry only. A practical "who needs attention" check: anyone whose `overall_status` is `non_compliant`, or `at_risk` with a `next_expiry_date` inside your renewal window. ## Next - [Documents & downloads](/guides/documents) — pull the certificate behind a requirement. - [Webhooks](/webhooks) — get a `crew.compliance.changed` event instead of polling. --- # Background-check status Source: https://docs.crewpass.co.uk/partners/guides/background-checks Each crew member on your fleet carries a **background-check status**. It appears on the fleet roster alongside verification status, so you can see at a glance who has cleared and who is still in progress. **Ordering a background check through the API is part of the v2 write surface, which is deferred and currently disabled.** When the write surface ships, issuing a check is a mutating request and requires HMAC request signing (see [Authentication](/authentication)). Today this page is **read-only**: you read a crew member's standardised background-check status, but you cannot initiate a check. **Requires** `crew:status:read` (included in your management plan). It's part of the [crew roster](/guides/crew) response. Authenticated with your API key as a Bearer token over TLS; reads are not signed. ## Where you see it ```json { "crew_unique_id": "crew_001", "verification_status": "verified", "background_check_status": "completed", "...": "..." } ``` ## The status values CrewPass normalises every provider's vocabulary into one standard set: | Status | Meaning | |---|---| | `pending` | A check exists but hasn't started. | | `awaiting` | Waiting on information from the crew member. | | `in-progress` | Being processed. | | `completed` | Cleared. | | `declined` | Did not clear. | | `expired` | A previously completed check has lapsed. | `null` means there's no background check on record for that crew member. CrewPass never tells you **who** ran the check, and never invents an ETA — you get the standardised state and nothing more. Ordering a check through the API is a later version; today this is read-only. ## Next - [Webhooks](/webhooks) — receive a `crew.status.changed` event when this moves. --- # Webhooks Source: https://docs.crewpass.co.uk/partners/webhooks CrewPass pushes a webhook to your registered endpoint when data about a crew member on your fleet changes — so you don't have to poll. Delivery is asynchronous, signed, retried with backoff, and dead-lettered after repeated failure. You receive an event only for a crew member on your fleet, only for an event whose scope you hold, and only when that crew member's consent allows it — the same gate as the read surface. ## How it works CrewPass continuously watches the data behind your fleet. When something relevant changes — a certificate finishes verifying, compliance flips, a status or profile updates — CrewPass builds the matching event and delivers it to the callback URL you registered. There is nothing to run on your side beyond an HTTPS endpoint that accepts a `POST`, verifies the signature, and returns `2xx`. Two things worth knowing: - **You don't poll.** Events are pushed in near-real-time off CrewPass's own change feed; you react to them. - **Delivery is at-least-once.** The same event may arrive more than once (e.g. after a retry), so deduplicate on `event_id`. ## Subscribing During onboarding, CrewPass registers your callback URL, the events you want, and issues a **webhook signing secret** used to sign every delivery (this is separate from the request-signing secret used by the v2 write surface; v1 reads are not signed at all). ## The envelope Every delivery is a JSON body with this shape: ```json { "schema_version": 1, "event_id": "evt_abc", "event_type": "crew.document.processed", "occurred_at": "2026-06-08T10:00:00Z", "partner_id": "prt_123", "data": { } } ``` And these headers: | Header | Value | |---|---| | `X-CrewPass-Event-Id` | The event id (dedupe on this). | | `X-CrewPass-Event-Type` | The event type. | | `X-CrewPass-Timestamp` | Unix seconds at signing time. | | `X-CrewPass-Signature` | `v1=` | | `X-CrewPass-Schema-Version` | `1` | ## Outbound signing differs from request signing The **outbound** webhook signature signs `"{timestamp}.{body}"` — there is **no nonce**. This is deliberately different from **inbound** request signing (used by the v2 write surface, not by v1 reads), which signs `"{timestamp}.{nonce}.{body}"`. Use the right scheme for the direction. ### Verifying a delivery ```python Python import hashlib, hmac def verify(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool: expected = "v1=" + hmac.new( secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) # In your handler: read the RAW body bytes (do not re-serialize), then: # verify(SECRET, request.headers["X-CrewPass-Timestamp"], # request.headers["X-CrewPass-Signature"], raw_body) ``` ```javascript Node import crypto from "node:crypto"; export function verify(secret, timestamp, signature, rawBody) { const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${timestamp}.` + rawBody).digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); } ``` ## Receiver requirements 1. **Verify** the signature against the raw bytes. 2. **Deduplicate** on `event_id` — delivery is at-least-once, so the same event may arrive more than once. 3. **Respond `2xx` quickly** and process asynchronously. 4. Anything else is **retried with exponential backoff** (up to 8 attempts), then **dead-lettered**. A `4xx` (other than `408`/`429`) is treated as a permanent rejection and not retried. ## Event catalogue (v1) | Event | Fires when | Scope required | |---|---|---| | `crew.document.processed` | A certificate finishes processing & verification | `crew:documents:read` | | `crew.document.updated` | A document's verification status changes | `crew:documents:read` | | `crew.compliance.changed` | A crew member's compliance state changes | `crew:compliance:read` | | `crew.status.changed` | Verification or background-check status changes | `crew:status:subscribe` | | `crew.profile.updated` | A crew member updates their profile | `crew:profile:read` | ### `crew.document.processed` / `crew.document.updated` ```json { "crew_unique_id": "crew_001", "document_id": "doc_9", "type": "ENG1", "category": "Medical Certificate", "title": "ENG1 Medical", "issuer": "Approved Authority", "verification_status": "verified", "issue_date": "2026-06-01", "expiry_date": "2028-06-01", "document_number": "E1-998" } ``` ### `crew.compliance.changed` ```json { "crew_unique_id": "crew_001", "vessel_id": "ves_abc", "overall_status": "at_risk", "requirements_met": 7, "requirements_total": 8, "requirements_expiring": 1, "next_expiry_date": "2026-08-01" } ``` ### `crew.status.changed` ```json { "crew_unique_id": "crew_001", "verification_status": "verified", "background_check_status": "completed" } ``` ### `crew.profile.updated` ```json { "crew_unique_id": "crew_001", "updated_at": "2026-06-08T12:00:00Z" } ``` The verification provider behind a background check or certificate is never named in any payload — only the standardised, CrewPass-owned status vocabulary. --- # Rate limits Source: https://docs.crewpass.co.uk/partners/rate-limits Requests are rate-limited per partner, per endpoint class, per scope, using a token bucket. Read endpoints share a generous budget; mutating endpoints (a later phase) are tighter. | Endpoint class | Per second | Per minute | |---|---|---| | `read` (all current endpoints) | 60 | 1,000 | | `mutation` (later phase) | 10 | 100 | The limiter is checked **before** authentication, so repeated unauthenticated attempts still consume your budget — a deliberate abuse guard. ## Handling 429 When you exceed the budget you get [`partner_rate_limited`](/errors/partner_rate_limited) with HTTP `429` and a `retry_after_seconds` hint: ```json { "error": { "code": "partner_rate_limited", "message": "rate limit exceeded", "retry_after_seconds": 0.42 } } ``` Back off for at least `retry_after_seconds` and retry the same request (reads are idempotent and carry only the Bearer header, so you can simply resend). Prefer the paginated `GET /fleet` over polling individual crew where you can — one fleet page covers your whole vessel roster in a single call. --- # Errors Source: https://docs.crewpass.co.uk/partners/errors/index Every error returns a consistent JSON envelope with a stable machine-readable `code` you can branch on: ```json { "error": { "code": "scope_not_consented", "message": "scope 'crew:documents:read' is not consented for this crew member", "scope": "crew:documents:read" } } ``` Some codes add fields (`scope`, `retry_after_seconds`). Branch on `code`, not on `message`. Every response also carries an `X-Request-Id` header — include it when contacting support. ## Catalogue | Code | HTTP | Meaning | |---|---|---| | [`invalid_api_key`](/errors/invalid_api_key) | 401 | Missing, malformed, or unknown API key. | | [`invalid_signature`](/errors/invalid_signature) | 401 | Bad/missing HMAC signature, stale timestamp, or replay. | | [`api_not_enabled`](/errors/api_not_enabled) | 403 | Your plan does not include API access. | | [`scope_not_granted`](/errors/scope_not_granted) | 403 | Your plan does not grant the required scope. | | [`scope_not_consented`](/errors/scope_not_consented) | 403 | The crew member hasn't consented to that scope for you. | | [`not_found`](/errors/not_found) | 404 | The vessel/crew isn't attributed to you (or doesn't exist). | | [`partner_rate_limited`](/errors/partner_rate_limited) | 429 | You exceeded your rate-limit budget. | | [`partner_mode_mismatch`](/errors/partner_mode_mismatch) | 403 | A test-mode key was used against a live environment. | Malformed request bodies (failing schema validation) return FastAPI's standard `422` response rather than this envelope. --- # invalid_api_key (401) Source: https://docs.crewpass.co.uk/partners/errors/invalid_api_key Returned when no API key is presented, the key is malformed, or it doesn't match an active `partner_api_keys` record. ```json { "error": { "code": "invalid_api_key", "message": "invalid API key" } } ``` **Fix** - Send the key as `Authorization: Bearer cpk_live_…` (or `X-Partner-API-Key`). - Check you're using the right key for the environment (`cpk_live_*` for production, `cpk_test_*` for sandbox). - Public widget keys (`cppk_*`) are not accepted on this surface. --- # invalid_signature (401) Source: https://docs.crewpass.co.uk/partners/errors/invalid_signature This error belongs to the **HMAC request-signing** scheme, which applies only to the **v2 write surface** (deferred and currently disabled). **v1 reads are not signed** — they authenticate with the API key as a Bearer token over TLS (see [Authentication](/authentication)), so they never return this error. A failed read returns [`invalid_api_key`](/errors/invalid_api_key) instead. It is returned when the HMAC headers are missing, the signature doesn't match, the timestamp is outside the ±300s window, or the nonce has already been used. ```json { "error": { "code": "invalid_signature", "message": "signature mismatch" } } ``` **Fix (v2 write surface)** - Sign the **exact** raw body bytes you send (don't re-serialize after signing). - Send all three headers: `X-CrewPass-Timestamp`, `X-CrewPass-Nonce`, `X-CrewPass-Signature: v1=…`. - Use a Unix timestamp in **seconds**, within ±300s of server time. - Use a **fresh nonce** per request — a replayed nonce (or a re-sent identical signed request) is rejected. See [Authentication](/authentication). Verifying an **outbound webhook** delivery uses a separate signature scheme (no nonce). If a delivery fails to verify on your side, that's a problem with your verification code, not this API error — see [Webhooks](/webhooks). --- # api_not_enabled (403) Source: https://docs.crewpass.co.uk/partners/errors/api_not_enabled Returned when your CrewPass plan does not include Partner API access at all. ```json { "error": { "code": "api_not_enabled", "message": "this partner's plan does not include API access" } } ``` **Fix** — this is a plan-level setting. Talk to CrewPass about enabling the Partner API for your account. See [how access works](/introduction#access). --- # scope_not_granted (403) Source: https://docs.crewpass.co.uk/partners/errors/scope_not_granted Returned when you have API access but your plan's granted scopes don't include the one this endpoint declares. The missing scope is in the `scope` field. ```json { "error": { "code": "scope_not_granted", "message": "scope 'vessels:fleet:read' is not granted to this partner", "scope": "vessels:fleet:read" } } ``` **Fix** — each capability page lists what it needs (the `Requires` note). If a capability you expect isn't enabled for your account, talk to CrewPass. See [how access works](/introduction#access). --- # scope_not_consented (403) Source: https://docs.crewpass.co.uk/partners/errors/scope_not_consented Returned when the scope is granted to you but the crew member has not consented to it for your partner account (or has revoked it). The scope is in the `scope` field. ```json { "error": { "code": "scope_not_consented", "message": "scope 'crew:documents:read' is not consented for this crew member", "scope": "crew:documents:read" } } ``` **Fix** — this is the crew member's choice. Nothing to change on your side; the data is unavailable until they consent. See [how access works](/introduction#access). --- # not_found (404) Source: https://docs.crewpass.co.uk/partners/errors/not_found Returned when the requested crew member or vessel isn't on one of your attached vessels — or doesn't exist. The two are **deliberately indistinguishable** so you cannot enumerate the CrewPass user base. ```json { "error": { "code": "not_found", "message": "no crew member with that id on your attached vessels" } } ``` **Fix** - Confirm the vessel is attached to you (`vessels/lookup` returns `attached`). - Confirm the crew member appears in your `GET /fleet` results. - A crew member who genuinely exists but sits outside your fleet will always return `404`, never `403`. --- # partner_rate_limited (429) Source: https://docs.crewpass.co.uk/partners/errors/partner_rate_limited Returned when you exceed the token-bucket budget for an endpoint class. A `retry_after_seconds` hint is included. ```json { "error": { "code": "partner_rate_limited", "message": "rate limit exceeded", "retry_after_seconds": 0.42 } } ``` **Fix** — back off for at least `retry_after_seconds`, then retry with a fresh nonce and signature. Prefer the paginated `GET /fleet` over per-crew polling. See [Rate limits](/rate-limits). --- # partner_mode_mismatch (403) Source: https://docs.crewpass.co.uk/partners/errors/partner_mode_mismatch Returned when a `cpk_test_*` key is used against an environment that only accepts live keys (or vice-versa). ```json { "error": { "code": "partner_mode_mismatch", "message": "test-mode keys are not accepted on this environment" } } ``` **Fix** — use your `cpk_live_*` key against production (`partners.crewpass.co.uk`) and your `cpk_test_*` key against the sandbox. --- # API reference Source: https://docs.crewpass.co.uk/partners/api-reference/overview The full machine-readable contract for the CrewPass Partner API is generated directly from the service by FastAPI, so it never drifts from the running code. - **Endpoints tab** — the interactive reference (request/response schemas, examples, the "try it" playground) is generated from the OpenAPI document. - **Raw spec** — [`openapi.json`](/api-reference/openapi.json) and [`openapi.yaml`](/api-reference/openapi.yaml). Feed either to a coding agent to generate a client. - **Live spec** — the running API serves the same document at `https://partners.crewpass.co.uk/openapi.json`. ## Security Every `/api/v2/*` read requires the `PartnerApiKey` bearer scheme (your `cpk_live_*` / `cpk_test_*` key), presented as a Bearer token over TLS. Reads are not signed, so the OpenAPI playground's bearer-token field is all a live read needs. See [Authentication](/authentication). Request signing (HMAC) returns for the v2 write surface, which is currently disabled. ## For AI agents - [`/llms.txt`](/llms.txt) — curated manifest of every page. - [`/llms-full.txt`](/llms-full.txt) — all docs concatenated into one file. - Every documentation page also resolves as `.md`. --- # Changelog Source: https://docs.crewpass.co.uk/partners/changelog The API is versioned in the path (`/api/v2/`). Additive changes (new endpoints, new optional response fields) are not breaking. Removing a field, changing a type, or tightening a scope is breaking and ships under a new path version. ## 2026-06-23 — reads authenticate over TLS only v1 reads now authenticate with the API key as a Bearer token over TLS; request signing has been **dropped from all reads**. HMAC request signing is retained for the v2 write surface (deferred and currently disabled) and, as a separate scheme, for outbound webhook delivery verification. See [Authentication](/authentication). No request or response shapes changed; only the read auth requirement is relaxed. ## 2026-06 — v1 management read surface + webhooks The v1 management-company surface: **read-only plus webhooks**, on top of the auth → scope → rate-limit → HMAC → isolation → consent → audit spine. Your fleet is derived from your CrewPass employer account; there is nothing to attach. **Endpoints** - `GET /api/v2/partners/me` — identity + granted scopes. - `GET /api/v2/employers/me/vessels` — your vessels (`vessels:fleet:read`). - `GET /api/v2/employers/me/fleet` — crew across your vessels, with verification + background-check status and documents-expiring counts, paginated (`vessels:fleet:read` + per-crew `crew:status:read`). - `POST /api/v2/employers/me/crew/lookup` — resolve a crew member by email (`crew:status:read`). - `GET /api/v2/employers/me/crew/{id}/profile` — base profile, plus an identity block under `crew:profile:full:read`. - `GET /api/v2/employers/me/crew/{id}/photo` — branded photo proxy (`crew:profile:read`). - `GET /api/v2/employers/me/crew/{id}/documents` — documents with issuer + verification status (`crew:documents:read`). - `GET /api/v2/employers/me/crew/{id}/documents/{document_id}/download` — short-lived, branded file link (`crew:documents:download`). - `POST /api/v2/employers/me/crew/{id}/compliance-checks` — compliance with the role / STCW / medical breakdown (`crew:compliance:read`). **Webhooks** - `crew.document.processed`, `crew.document.updated`, `crew.compliance.changed`, `crew.status.changed`, `crew.profile.updated`. See [Webhooks](/webhooks). **Deferred to v2** - Crew onboarding & invites, vessel-placement writes, vessel self-attach, and API-initiated background checks. Background-check **status** is read-only in v1, and the verification provider's name is never exposed. ---