# 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).
---