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