# 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/api-reference/openapi.json --- # CrewPass Partner API Source: https://docs.crewpass.co.uk/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 placement 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://devpartners.crewpass.co.uk` | Everything lives under `/api/v2/`. ## 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/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/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 reads are **not** signed; the Bearer key over TLS is the only credential you need. ## 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 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. Outbound **webhook** deliveries are signed by CrewPass so you can verify they are genuine; see [Webhooks](/webhooks) for how to verify a delivery signature. --- # List your vessels Source: https://docs.crewpass.co.uk/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/guides/crew The fleet roster is the heart of the API: every crew member on your vessels, with their status on the vessel, 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", "vessel_imo": "9876543", "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 } ``` | Field | What it is | |---|---| | `crew_unique_id` | The crew member's id — use it for profile, documents, compliance. | | `vessel_imo` | The vessel's IMO number, for matching vessels in your own system without relying on names. `null` when CrewPass doesn't hold one. | | `role` | Their position on this vessel. | | `status` | Their placement on this vessel, e.g. `active` or `invited`. | | `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). `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/guides/profiles Once you have a `crew_unique_id` (from [your crew list](/guides/crew)), read their profile. `name` and `rank` always return; four further blocks are each gated on their own scope **and** the crew member's consent for it — a block is simply absent from the response when either is missing. **Requires** `crew:profile:read`. The contact block (`email`, `phone`, `country_code`) additionally requires `crew:contact:read`; the professional block (`nationality`, `languages`, `skills`, `bio`, `years_experience`, `profile_photo_url`) requires `crew:professional:read`; `employment_history` requires `crew:experience:read`; the personal block (`date_of_birth`, `address`, `passport`, `visas`) requires `crew:personal:read`. `crew:profile:full:read` is a legacy alias for `crew:experience:read` **+** `crew:personal:read` together. Each also needs 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": "+447700900000", "country_code": "GB", "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": "self_reported" } ], "full_profile": true, "date_of_birth": "1990-04-12", "address": { "line1": null, "city": "Chester", "state": "England", "country": "GB", "postcode": "CH4 9GN", "full_address": "Chester, England, CH4 9GN, GB" }, "passport": { "nationality": "British", "number": "123456789", "expiry": "2030-04-01" }, "visas": [{ "type": "B1/B2", "country": "USA", "expiry": "2029-01-01" }] } ``` ### Base fields (always) `name`, `rank`. Everything else sits in one of the four scope-gated blocks below. ### Contact block (`crew:contact:read`) `email`, `phone`, `country_code`. `phone` is returned in international (E.164) form, composing the dial code and the national number (e.g. `+447700900000`). `country_code` is the ISO country code. ### Professional block (`crew:professional:read`) `nationality`, `languages`, `skills`, `years_experience`, `bio` and `profile_photo_url`. This is the old base profile in full, minus `name` / `rank` (which need only `crew:profile:read`) and the contact fields above. ### Employment history (`crew:experience:read`) `employment_history` only. Each row's `source` is either `self_reported` (the crew member entered it) or `employer_verified` (an employer has confirmed it) — it reflects the verification level, not where the record was entered. ### Personal block (`crew:personal:read`) `date_of_birth`, `address`, `passport`, `visas`. `address`, when present, carries the structured parts (`line1`, `line2`, `city`, `state`, `country`, `postcode`) plus `full_address`, a single-line composed address. A crew member who supplied only a city/postcode (no street) will have `line1: null` but a populated `full_address` — read `full_address` for display. `full_profile` is `true` only when **both** `crew:experience:read` and `crew:personal:read` are effective for this crew member (the old `crew:profile:full:read` bundle — employment history plus the personal block). When a block isn't effective, its fields are simply absent — the rest of the profile still returns. Don't treat a missing field as an error. ## Photo `profile_photo_url` is part of the professional block: it's only set when `crew:professional:read` is effective, and 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, or `crew:professional:read` isn't effective. ## Next - [Documents & downloads](/guides/documents) - [Check compliance](/guides/compliance) --- # Documents & downloads Source: https://docs.crewpass.co.uk/guides/documents For any crew member on your fleet you can list their current documents — with the issuing authority, CrewPass's verification status and a classification you can map to your own document types — 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. A document whose `classification.document_class` is `medical` (e.g. an ENG1) is included only when `crew:medical:read` is also effective for the crew member; otherwise it's left out of the list and its download link 404s. 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": "document", "category": "STCW Modules", "title": "Personal Survival Techniques (A-VI/1-1)", "issuer": "Maritime & Coastguard Agency", "verification_status": "verified", "issue_date": "2023-03-01", "expiry_date": "2028-03-01", "document_number": "PST-12345", "review_status": "verified", "is_expired": false, "no_expiry": false, "is_current": true, "superseded_by": null, "classification": { "taxonomy_version": "2026-09-17.230e599f", "document_class": "training", "category_code": "stcw_training", "catalogue_title": { "id": "6916072356124cb47c6c53fd", "name": "Personal Survival Techniques (A-VI/1-1)" }, "stcw_regulation": "VI/1", "stcw_codes": ["VI/1-1"], "level": "support", "is_refresher": false, "flag_state": "GB", "coc_capacities": [], "sources": ["document_category", "competence_profile", "catalogue"] } } ] } ``` Each document is returned in its latest version, and only once CrewPass has **finished processing** it. The list leaves out documents that are still being processed, older uploads from before CrewPass's current document processing, duplicates, and documents CrewPass has rejected or removed. Documents left out aren't counted in a fleet's `documents_expiring` total and can't be downloaded. A new upload appears as soon as processing completes. An earlier edition of a certificate that has since been renewed is still listed, with `is_current: false`. What the key fields mean: | Field | Meaning | |---|---| | `issuer` | The authority that issued the document, such as a flag state or training provider. `null` when it isn't known; a placeholder such as "Unknown" is never sent. | | `verification_status` | CrewPass's check on the document, taking expiry into account: `verified`, `pending` or `expired`. | | `review_status` | The result of CrewPass's review, without expiry: `verified`, `pending`, or `pending_manual_review` (waiting for a check by the CrewPass team). An expired certificate that was verified stays `verified`. | | `expiry_date` | When it expires. Use it with the fleet's `documents_expiring` count to spot renewals. | | `is_expired` | `true` once `expiry_date` has passed. `null` when there is no expiry date. | | `no_expiry` | `true` for a document that does not expire, `false` when it has an expiry date, and `null` when CrewPass doesn't know. A missing `expiry_date` without `no_expiry: true` means the date wasn't recorded, so it's worth checking with the crew member. | | `is_current` / `superseded_by` | `is_current` is `false` once a newer edition of the same certificate is on file, and `superseded_by` gives the newer document's `document_id`. Both are `null` when CrewPass doesn't know. | | `document_number` | The document's own reference number. | | `classification` | What the document is, as codes you can map. See the next section. | ## 2. Classify documents The `classification` block tells you what a document is, using values from the published [code list](/reference/document-codes), so you can file it in your own system without reading the title. CrewPass builds it from what it already holds: the certificate type the document was matched to, and the crew member's maritime competence profile, medical record and STCW record. | Field | Meaning | |---|---| | `document_class` | `training`, `competency`, `endorsement`, `medical`, `identity`, `travel` or `other`. Separates certificates from paperwork. | | `category_code` | A finer grouping, e.g. `stcw_training`, `deck_qualifications`, `seafarer_identity`. | | `catalogue_title` | The CrewPass certificate type the document was matched to: a stable `id` and its `name`. Every document of the same type has the same `id`. | | `stcw_regulation` / `stcw_codes` | The STCW regulation, and the specific units the document covers. A combined basic training certificate lists `VI/1-1` to `VI/1-4`. | | `level` | `management`, `operational` or `support`, where the certificate has one. | | `is_refresher` | `true` for an updated proficiency (refresher) certificate. | | `flag_state` | ISO 3166 alpha-2 code of the state that issued the document. | | `coc_capacities` | For a certificate of competency, each capacity it grants: `stcw_regulation`, `role`, `level` and its limits (`gt_max`, `kw_max`, `power_unrestricted`, `area`, `vessel_types`). | | `sources` | What the classification was built from, in order. See [evidence sources](/reference/document-codes#evidence-sources). | | `taxonomy_version` | The version of the code list the values come from. | A `null` or an empty list means CrewPass doesn't hold that fact for the document; it never means "no". The whole block can be `null` if a document can't be classified at the moment. Treat that as unknown and fall back to `title`. ### Mapping to your own types Use the most specific signal first: 1. **`document_class`** decides where the document belongs: certificates (`training`, `competency`, `endorsement`) or paperwork (`identity`, `medical`, `travel`). 2. **STCW training:** `stcw_codes` with `is_refresher`. For example `VI/1-2` with `is_refresher: true` is the updated proficiency in fire prevention and fire fighting. 3. **Certificates of competency:** `coc_capacities`, e.g. regulation `II/2`, role Master, `gt_max: 3000`, `vessel_types: ["yacht"]`. 4. **Everything else** (short courses, specialist training): keep a one-time map keyed by `catalogue_title.id`. 5. Fall back to `title` only when none of the above is set. ## 3. 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/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`. A medical row's `status` always shows, but its `satisfied_by` document details are only included when `crew:medical:read` is also effective for the crew member — otherwise `satisfied_by` is an empty list on that row. 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 Four kinds of requirement are checked, each a row in the matrix with its own `group`: - **Medical** (`group: "medical"`) — the ENG1 (or equivalent) seafarer medical: status and expiry. CrewPass never exposes the doctor or clinic. - **STCW basic safety training** (`group: "stcw"`) — the standard safety modules (Personal Survival, Fire Prevention, First Aid, Personal Safety…), with a per-module `modules[]` breakdown. - **Certificates** (`group: "certificate"`) — the specific certificates the crew member's position requires (e.g. Ship Security Officer, Helideck courses). - **Qualifications** (`group: "qualification"`) — advanced/rank qualifications and endorsements (e.g. Master `<3000GT`, ECDIS, GMDSS), matched from the crew's maritime competence profile. ## 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 check is for the right position. The response is a single **requirement matrix**: one row per role requirement — Medical, STCW, each Certificate, each Qualification — with its own `status` and the document(s) that satisfy it. The `summary` always reconciles (the parts sum to `total`, and `total` equals the number of rows). ```json Response { "crew_unique_id": "crew_001", "vessel_id": "ves_draak", "position_name": "Captain", "as_of": "2026-07-01T12:00:00Z", "overall_status": "non_compliant", "compliance_source": "live_evaluator", "summary": { "total": 4, "met": 2, "expiring": 0, "expired": 1, "missing": 1, "pending": 0 }, "requirements": [ { "key": "medical:SEAFARERS_MEDICAL", "group": "medical", "title": "ENG1 Medical Certificate", "status": "met", "expiry_date": "2027-01-21", "days_until_expiry": 204, "required": true, "satisfied_by": [ { "document_id": "doc_med", "title": "ENG 1 (Medical Certificate)", "expiry_date": "2027-01-21", "download_path": "https://partners.crewpass.co.uk/api/v2/employers/me/crew/crew_001/documents/doc_med/download" } ] }, { "key": "explicit:sso", "group": "certificate", "title": "Ship Security Officer", "status": "met", "required": true, "satisfied_by": [ { "document_id": "doc_sso", "title": "Certificate of Proficiency as Ships Security Officer", "download_path": "…" } ] }, { "key": "qualification:II/2", "group": "qualification", "code": "II/2", "title": "Master <3000GT", "status": "met", "required": true, "satisfied_by": [] }, { "key": "certificate:lithium_ion", "group": "certificate", "title": "Lithium-Ion Battery Safety", "status": "missing", "required": true, "satisfied_by": [] } ] } ``` ### Reading the result Start with **`overall_status`** (`compliant` · `at_risk` · `non_compliant` · `no_role`), then work the **`requirements[]`** matrix: - **`group`** buckets the requirement: `medical` · `stcw` · `certificate` · `qualification`. - **`status`** is the verdict: `met` · `expiring` · `expired` · `missing` · `pending`. - **`satisfied_by[]`** is the document(s) that meet it — each with a `document_id`, `title`, `expiry_date`, and a ready `download_path` (pass it to the [document download](/guides/documents) flow). Empty when the requirement is unmet. - The STCW basic row carries a `modules[]` breakdown (per-module status). - **`summary`** gives the counts and always reconciles with the rows. **`compliance_source`** tells you how the result was produced: `live_evaluator` (the full matrix incl. certificates + qualifications) or `crewCompliance_fallback` (STCW + Medical only, used if the evaluation service is briefly unavailable). The verdict is computed by CrewPass's compliance engine — you read it, you don't recompute it. A practical "who needs attention" filter: any crew member with `overall_status` `non_compliant`/`at_risk`, or any requirement whose `status` is `missing`, `expired`, or `expiring`. ## 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/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. **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. You get the standardised status value and nothing more, with no invented ETA. ## Next - [Webhooks](/webhooks) — receive a `crew.status.changed` event when this moves. --- # Webhooks Source: https://docs.crewpass.co.uk/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 keeps watch on the data behind your fleet. When something relevant changes — a certificate finishes processing, compliance moves, a status or profile updates — CrewPass builds the matching event and delivers it to the callback URL you registered, usually within 15 minutes of the change. There is nothing to run on your side beyond an HTTPS endpoint that accepts a `POST`, verifies the signature, and returns `2xx`. Five things worth knowing: - **You don't poll.** CrewPass checks your fleet for changes every 15 minutes and pushes what has changed; you react to it. - **Delivery is at-least-once.** The same event may arrive more than once (e.g. after a retry), so deduplicate on `event_id`. - **Events are not ordered.** Treat each one as "this changed, here is how it looks now" rather than as a step in a sequence. - **You will not receive a backlog.** When your endpoint is first registered you are not sent an event for every certificate already on file. Read the current state through the API once, and webhooks keep you in step from there. - **Crew who join your fleet arrive in full.** When a crew member is added to one of your vessels, you receive a `crew.document.processed` for each document they already hold, plus their current compliance, status and profile. ## Subscribing Webhook registration is **not self-serve**. Ask your CrewPass contact to register your callback URL and the events you want; they set it up for you during onboarding, or whenever you need it changed. CrewPass then issues a **webhook signing secret** used to sign every delivery (this is separate from your API key; reads are not signed). ## 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=`, where `message` is the timestamp, a dot, then the raw request body bytes. | | `X-CrewPass-Schema-Version` | `1` | ## Signature scheme The webhook signature signs the timestamp, a dot, then the raw request body bytes (`"{timestamp}." + body`), with **no nonce**. Build your receiver's verification against exactly that string. ### 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 document you have not been sent before becomes available, including each document held by a crew member who joins your fleet | `crew:documents:read` (also `crew:medical:read` for a medical-class document) | | `crew.document.updated` | Something changes about a document you already have | `crew:documents:read` (also `crew:medical:read` for a medical-class document) | | `crew.compliance.changed` | A value in a crew member's compliance summary 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.updated`, `crew.compliance.changed` and `crew.status.changed` are sent only when a value in their payload changes. A recalculation that leaves every figure the same sends nothing. ### `crew.document.processed` / `crew.document.updated` ```json { "crew_unique_id": "crew_001", "document_id": "doc_9", "type": "document", "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", "review_status": "verified", "is_expired": false, "no_expiry": false, "is_current": true, "superseded_by": null, "classification": { "taxonomy_version": "2026-09-17.230e599f", "document_class": "medical", "category_code": "medical_fitness", "catalogue_title": { "id": "…", "name": "ENG1 Medical" }, "stcw_regulation": null, "stcw_codes": [], "level": null, "is_refresher": null, "flag_state": "GB", "coc_capacities": [], "sources": ["document_category", "medical_record", "catalogue"] } } ``` The document fields match the [documents endpoint](/guides/documents), including the `classification` block. The fields from `review_status` onwards were added on 2026-09-17. Ignore any fields you don't need. ### `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" } ``` ## Reacting to an event Every payload carries a **`crew_unique_id`**: the join key between CrewPass and your own records. You learn it once, store it against your own crew record, then use it to look up the detail behind any event. Discover a crew member's `crew_unique_id` once, then keep it: - `GET /api/v2/employers/me/fleet` returns the fleet roster, where every crew member arrives with their `crew_unique_id`. - `POST /api/v2/employers/me/crew/lookup` resolves a single known crew member to their `crew_unique_id`. From then on, an event's `crew_unique_id` tells you exactly which of your records changed. An event is a signal, not a full snapshot: the document events carry their metadata inline, and for the others you call the matching read endpoint to fetch the current detail. **`crew.document.processed` and `crew.document.updated`.** The payload already has the document metadata (type, category, issuer, verification status, issue and expiry dates, document number) and its classification, so you can file it straight away. To fetch the **file**, call `GET /api/v2/employers/me/crew/{crew_unique_id}/documents/{document_id}/download` and follow the `download_url` it returns; that link is short-lived, so use it straight away rather than storing it. Both `crew_unique_id` and `document_id` come from the event payload. Document events fire once CrewPass has **finished processing** a document, not when a crew member uploads it. Expect a short delay between the upload and the event — processing, then up to 15 minutes until the next check. `crew.document.updated` covers any change you can see: a re-verification, a corrected date, a newer edition replacing it (`is_current`, `superseded_by`), or a change to its classification. There is no "document removed" event. If a document stops being shared — it is replaced, withdrawn, or goes back for reprocessing — nothing is sent, and your copy will be the last state you were told about. Re-read `GET /api/v2/employers/me/crew/{crew_unique_id}/documents` periodically, or after a `crew.compliance.changed` event, if that matters to you. **`crew.compliance.changed`.** The payload is a summary (overall status plus the requirement counts). For the full role, STCW, and medical breakdown, call `POST /api/v2/employers/me/crew/{crew_unique_id}/compliance-checks`. The `overall_status` values ([`compliant`, `at_risk`, `non_compliant`, `no_role`](/guides/compliance#reading-the-result)) mean the same here as on the compliance read. **`crew.profile.updated`.** The event is a change signal, not a diff: it tells you the profile changed, not what changed. Call `GET /api/v2/employers/me/crew/{crew_unique_id}/profile` to read the current profile. **`crew.status.changed`.** The payload carries the new `verification_status` and `background_check_status` directly, so update your own record straight from the payload. No follow-up read is required. Background-check status uses CrewPass's standardised, provider-agnostic vocabulary. --- # Document codes Source: https://docs.crewpass.co.uk/reference/document-codes {/* Generated by docs/site/scripts/build_document_codes.py. Do not edit by hand. */} These are the values a document's `classification` block can contain (see [Documents & downloads](/guides/documents#2-classify-documents)). Map them to your own document types once: CrewPass keeps them stable. Code list version **`2026-09-17.230e599f`**. Each classification includes the `taxonomy_version` it was built with. New codes may be added at any time, but a code is never renamed or removed within `/api/v2/`. ## Document classes `document_class`. Check this first: it separates certificates from paperwork. | Class | Meaning | |---|---| | `training` | A course or proficiency certificate (STCW modules, short courses). | | `competency` | A certificate of competency or licence to serve in a capacity. | | `endorsement` | A flag state endorsement or recognition of another certificate. | | `medical` | A medical fitness certificate or other medical record. | | `identity` | Seafarer identity and sea service paperwork (discharge books, ID). | | `travel` | A visa or other travel document. | | `other` | Anything else, including CVs. | ## Category codes `category_code`. A finer grouping. Each code has a default class, but a document's own details can place it in a different one: a course filed under a competency category is still `training`. | Code | Label | Default class | |---|---|---| | `stcw_training` | STCW training | `training` | | `medical_fitness` | Medical fitness certificates | `medical` | | `medical_professional` | Medical professional qualifications | `training` | | `deck_qualifications` | Deck qualifications and certificates of competency | `competency` | | `deck_courses` | Deck courses | `training` | | `small_craft_licences` | Small craft and pleasure craft licences | `competency` | | `engineering_qualifications` | Engineering qualifications and certificates of competency | `competency` | | `engineering_courses` | Engineering courses | `training` | | `flag_endorsements` | Flag state endorsements and recognitions | `endorsement` | | `seafarer_identity` | Seafarer identity and sea service documents | `identity` | | `interior_hospitality` | Interior and hospitality | `training` | | `food_hygiene` | Food safety and hygiene | `training` | | `security_courses` | Security courses | `training` | | `technical_it` | Technical, IT and communications courses | `training` | | `diving` | Diving | `training` | | `helideck` | Helideck | `training` | | `aviation` | Aviation | `competency` | | `instructor_qualifications` | Instructor qualifications | `training` | | `specialist_courses` | Specialist courses | `training` | | `radio_qualifications` | GMDSS and radio certificates | `competency` | | `travel_documents` | Visas and travel documents | `travel` | | `vaccinations` | Vaccination records | `medical` | | `cv` | CVs | `other` | | `other` | Other | `other` | | `legacy` | Retired categories | decided per document | `other` means CrewPass couldn't place the document more precisely. `legacy` marks a document filed under a category CrewPass no longer uses; its `document_class` still comes from the document's own details. ## STCW codes `stcw_regulation` and `stcw_codes`. A code with a unit, such as `VI/1-1`, refers to one table of the STCW Code (here, table A-VI/1-1). A regulation on its own, such as `VI/1`, is sent only when CrewPass doesn't know which unit applies. A combined course lists every unit it covers. The level column shows `management`, `operational` or `support` where a code has one. Chapter I, VII and VIII entries are general provisions and rarely appear on a document. | Code | Name | Level | |---|---|---| | `I/1` | Definitions and clarifications | — | | `I/2` | Certificates and endorsements | — | | `I/3` | Principles governing near‑coastal voyages | — | | `I/4` | Control procedures | — | | `I/5` | National provisions | — | | `I/6` | Training and assessment | — | | `I/7` | Communication of information | — | | `I/8` | Quality standards | — | | `I/9` | Medical standards | — | | `I/10` | Recognition of certificates | — | | `I/11` | Revalidation of certificates | — | | `I/12` | Use of simulators | — | | `I/13` | Conduct of trials | — | | `I/14` | Responsibilities of companies | — | | `I/15` | Transitional provisions | — | | `I/16` | Verification of compliance | — | | `II/1` | Officer in Charge of a Navigational Watch (OOW) | operational | | `II/2` | Chief Mate and Master | management | | `II/3` | Master <500 GT (near-coastal) | management | | `II/4` | Ratings forming part of a navigational watch (RFPNW) | support | | `II/5` | Able Seafarer Deck (AB Deck) | support | | `II/6` | Lookout/watchkeeping duties | support | | `III/1` | Officer in Charge of an Engineering Watch (EOOW) | operational | | `III/2` | Chief Engineer & Second Engineer (unlimited) | management | | `III/3` | Chief Engineer & Second Engineer on ships <3000 kW | management | | `III/4` | Ratings forming part of an engineering watch (RFPEW) | support | | `III/5` | Able Seafarer Engine | support | | `III/6` | Electro-Technical Officer (ETO) | operational | | `III/7` | Electro-Technical Rating (ETR) | support | | `IV/1` | GMDSS Restricted Operator / Radio Personnel | — | | `IV/2` | GMDSS General Operator's Certificate (GOC) | — | | `V/1-1` | Special training for personnel on oil and chemical tankers and for personnel involved in helicopter operations | — | | `V/1-1-1` | Basic training: Oil & Chemical tanker cargo ops | — | | `V/1-1-2` | Advanced training: Oil tanker cargo ops | — | | `V/1-1-3` | Advanced training: Chemical tanker cargo ops | — | | `V/1-2` | Liquefied Gas Tankers | — | | `V/1-2-1` | Basic training: Liquefied gas tanker cargo ops | — | | `V/1-2-2` | Advanced training: Liquefied gas tanker cargo ops | — | | `V/2` | Passenger ships | — | | `V/2-1` | Crowd Management | — | | `V/2-2` | Crisis Management & Human Behaviour | — | | `V/2-3` | Passenger Safety, Cargo Safety & Hull Integrity | — | | `V/2-4` | Passenger service safety training (direct service in passenger spaces) | — | | `V/3` | High-speed craft | — | | `V/3-1` | IGF Code – Basic training (low‑flashpoint fuels) | — | | `V/3-2` | IGF Code – Advanced training (low‑flashpoint fuels) | — | | `V/4` | Polar Code training (Basic & Advanced) | — | | `V/4-1` | Polar waters – Basic training | — | | `V/4-2` | Polar waters – Advanced training | — | | `V/5` | Deprecated – do not use | — | | `VI/1` | Basic Training (BST) | — | | `VI/1-1` | Personal Survival Techniques (PST) | support | | `VI/1-2` | Fire Prevention & Fire Fighting (FPFF) | support | | `VI/1-3` | Elementary First Aid (EFA) | support | | `VI/1-4` | Personal Safety & Social Responsibilities (PSSR) | support | | `VI/2` | Proficiency in survival craft and rescue boats (PSC & RB) | — | | `VI/2-1` | Proficiency in Survival Craft & Rescue Boats (PSC&RB) | support | | `VI/2-2` | Proficiency in Fast Rescue Boats (FRB) | support | | `VI/3` | Advanced Fire Fighting (AFF) | — | | `VI/4` | Medical First Aid / Medical Care | operational | | `VI/4-1` | Medical First Aid on board ship | — | | `VI/4-2` | Medical Care on board ship | — | | `VI/5` | Ship Security Officer (SSO) | — | | `VI/6` | Security awareness / designated security duties | support | | `VI/6-1` | Security Awareness | — | | `VI/6-2` | Designated Security Duties | — | | `VII/1` | Issue of alternative certificates | — | | `VII/2` | Certification of seafarers | — | | `VII/3` | Principles governing the issue of alternative certificates | — | | `VIII/1` | Fitness for duty | — | | `VIII/2` | Watchkeeping arrangements and principles | — | | `ECDIS` | ECDIS (Electronic Chart Display and Information System) | — | ## Evidence sources `sources` lists what a document's classification was built from, in order. | Source | Meaning | |---|---| | `document_type` | The kind of document uploaded (visa, vaccination record, CV). | | `title_override` | Details CrewPass has checked and confirmed for this certificate type. | | `competence_profile` | The crew member's maritime competence profile: the qualifications CrewPass holds for them. | | `medical_record` | The crew member's medical certificate record on CrewPass. | | `stcw_record` | The crew member's STCW basic training record on CrewPass. | | `document_category` | The category the document is filed under on CrewPass. | | `catalogue` | The CrewPass certificate type the document was matched to. | | `structured_data` | Details read from the document itself. | --- # Rate limits Source: https://docs.crewpass.co.uk/rate-limits Requests are rate-limited per partner, per endpoint class, per scope, using a token bucket. | Endpoint class | Per second | Per minute | |---|---|---| | `read` (all endpoints) | 60 | 1,000 | 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/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/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/errors/invalid_signature This error relates to the **HMAC request-signing** scheme. **Reads are not signed**: they authenticate with the API key as a Bearer token over TLS (see [Authentication](/authentication)), so a read never returns 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** - 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/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/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/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/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/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/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/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). ## 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/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-09-22 — item-sized scopes Five new scopes, each filtering one block within a response your key already reads. **Every existing tier that could already see a block keeps seeing it — these scopes were added to `management`, `recruiter` and `vessel` automatically, so no partner on those tiers loses anything.** An external partner's per-contract scopes are unaffected unless CrewPass adds these to your contract — in particular, `crew:profile:read` alone no longer implies the contact or professional block for a `scope_overrides`-based partner that hasn't been upgraded (it always has for `management` / `recruiter` / `vessel`). **Profile** (`GET /api/v2/employers/me/crew/{id}/profile`) - **`crew:contact:read`** (new): gates `email`, `phone`, `country_code`. - **`crew:professional:read`** (new): gates `nationality`, `languages`, `skills`, `years_experience`, `bio` and `profile_photo_url` — this is the old base profile in full, minus `name` / `rank` and the contact fields above. - **`crew:experience:read`** (new): gates `employment_history` only. - **`crew:personal:read`** (new): gates `date_of_birth`, `address`, `passport` and `visas`. - `crew:profile:full:read` still works exactly as before — it's now a compatibility alias for `crew:experience:read` **+** `crew:personal:read` together (unchanged: it never included the professional-block fields above). - Only `name` and `rank` need nothing beyond `crew:profile:read`. **Photo** (`GET /api/v2/employers/me/crew/{id}/photo`) — *Behaviour change*: now also needs `crew:professional:read` (the photo is part of the professional block), on top of the existing `crew:profile:read` gate. **Documents** (`GET /api/v2/employers/me/crew/{id}/documents` and its download endpoint) — *Behaviour change*: a document whose `classification.document_class` is `medical` now needs the new **`crew:medical:read`** scope; otherwise it's left out of the list and its download link 404s. **Compliance** (`POST /api/v2/employers/me/crew/{id}/compliance-checks`) — *Behaviour change*: a medical requirement row's `satisfied_by` document details now need `crew:medical:read` too. The row's `status` and `group` are never hidden. Two scopes, `connections:read` and `connections:write`, are declared for a future surface and are not granted to any tier yet. ## 2026-09-22 — webhooks: crew who join your fleet No change to any payload or field. - **Crew who join your fleet arrive in full.** When a crew member is added to one of your vessels, you receive a `crew.document.processed` for each document they already hold, plus their current compliance, status and profile. Registering an endpoint still sends no backlog. - **Update events carry a change.** `crew.document.updated`, `crew.compliance.changed` and `crew.status.changed` are sent only when a value in their payload changes, so a recalculation that leaves every figure the same sends nothing. See [Webhooks](/webhooks#event-catalogue-v1). ## 2026-09-18 — webhook delivery, stated plainly No change to any payload or field. This writes down how delivery behaves, so you can build a receiver against it: - **Changes reach you within about 15 minutes.** CrewPass checks your fleet on a schedule rather than at the instant of a write. - **Registering an endpoint does not send you a backlog.** Read the current state through the API once; webhooks keep you in step from there. - **Events are not ordered.** Each one says how something looks now. - **`crew.document.updated`** covers any visible change to a document you already have, not only a change of verification status. - **There is no "document removed" event.** See [Webhooks](/webhooks#crew-document-processed-crew-document-updated). ## 2026-09-17 — document classification and vessel IMO numbers Documents now tell you what they are, as codes you can map to your own document types, so you no longer need to work it out from the title. Every change is additive except the two marked *Behaviour change* and *Value change*. **Documents** (`GET /api/v2/employers/me/crew/{id}/documents` and the `crew.document.*` webhooks) - **`classification`** (new): what the document is. It gives the `document_class`, the `category_code`, the CrewPass certificate type it was matched to (`catalogue_title`, with a stable `id`), `stcw_regulation` and `stcw_codes`, `level`, `is_refresher`, `flag_state`, `coc_capacities`, and the `sources` it was built from. See [Classify documents](/guides/documents#2-classify-documents). - **[Document codes](/reference/document-codes)** (new page): every value a classification can contain. Each classification includes the `taxonomy_version` (the version of this list) it was built with. - **`review_status`** (new): the result of CrewPass's review, separate from expiry. - **`is_expired`** and **`no_expiry`** (new): tell a document that doesn't expire apart from one whose expiry date wasn't recorded. - **`is_current`** and **`superseded_by`** (new): show when a certificate has been replaced by a newer edition. - *Behaviour change:* **only documents CrewPass has finished processing are shared.** Documents still being processed, and older uploads from before CrewPass's current document processing, are no longer listed, counted in `documents_expiring`, or available to download. A new upload appears as soon as processing completes. - *Value change:* **`issuer`** is `null` when the issuing authority isn't known, instead of a placeholder such as "Unknown". **Fleet** (`GET /api/v2/employers/me/fleet`) - **`vessel_imo`** (new): the vessel's IMO number, so you can match vessels without relying on names. `null` when CrewPass doesn't hold one. - *Documentation correction:* each crew member's status on the vessel is returned in `status` (for example `active` or `invited`), as it always has been. The Crew guide previously called this field `verification_status`. **API reference** - Endpoints are now grouped by task (Account, Fleet, Crew, Documents, Compliance), with clearer names and descriptions. Page addresses in the API reference have changed. ## 2026-07-02 — compliance as a unified requirement matrix `POST /api/v2/employers/me/crew/{id}/compliance-checks` now returns a full requirement matrix, so you can see at a glance exactly which requirements a crew member holds, which document satisfies each, and what is still outstanding. - **`requirements[]`** — one row per requirement, grouped as `medical`, `stcw`, `certificate`, or `qualification`. Each row has its own `status` (`met` · `expiring` · `expired` · `missing` · `pending`), plus `title`, optional `expiry_date`, and `days_until_expiry`. - **`satisfied_by[]`** — the actual document(s) meeting each requirement, each with `document_id`, `title`, `expiry_date`, and a branded `download_path` you can fetch directly. Empty when the requirement is unmet. - **STCW rows** carry per-module detail under `modules[]`. - **`summary`** — totals per status that always reconcile with `requirements[]`. - **`overall_status`** with **`compliance_source`** — the verdict comes straight from the CrewPass compliance engine (`live_evaluator`). If the engine is briefly unavailable the response falls back to the stored projection and flags it via `compliance_source`, so the provenance of every verdict is explicit. This supersedes the earlier interim compliance breakdown with a single, consistent shape. ## 2026-06-30 — richer crew profile & document detail Several read fields now carry more detail and map more precisely to the underlying records. All changes are additive except the two field-value changes noted below. - **Profile `phone`** is now returned in full international (E.164) form (dial code + national number, e.g. `+447700900000`), with a new ISO `country_code` alongside it. *Value change:* `phone` previously returned the national number only. - **Profile `address.full_address`** (new) gives the composed single-line address; `line1` stays `null` when no street was supplied. - **Employment history `source`** now reports the verification level of each entry — `self_reported` or `employer_verified`. *Value change:* it previously returned the dashboard origin (`crew_dashboard` / `employer_dashboard`). - **Documents** now return only current, live documents — rejected, replaced/deduplicated, and inactive placeholders are excluded from listings, expiring counts, and downloads, so what you see reflects what a crew member actually holds. ## 2026-06-23 — simpler read authentication (no request signing) Reads now authenticate with just your API key as a Bearer token over TLS — request signing has been **dropped from all reads**, so there is nothing to sign on a GET. HMAC signing is retained only for verifying outbound webhook deliveries. See [Authentication](/authentication). No request or response shapes changed; only the read auth requirement is simpler. ## 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 as a unified requirement matrix (`crew:compliance:read`). **Webhooks** - `crew.document.processed`, `crew.document.updated`, `crew.compliance.changed`, `crew.status.changed`, `crew.profile.updated`. See [Webhooks](/webhooks). ---