For developers
Validate a courier with one API.
Ask whether the driver holds an issued, currently valid Courier Passport. You receive status — never identity images, DVLA payloads, or DBS files.
The integration
- 1. Create a sandbox key in the business hub
- 2. POST /api/v1/consents — wait for the driver to approve
- 3. GET /api/v1/passports/{number}
- 4. Treat valid as a pass. Expiring can pass with a warning.
Start here
Register as a business with your Companies House number. Sandbox keys are available at once. Live keys after KYB and a signed partner DPA. Keys are shown once — store them in a secrets manager. Never ship a key in a browser, driver app, or git repository.
Base URL
https://api.courierpassport.com
Local development: http://127.0.0.1:8000
Sandbox
Prefix cdp_test_. Use a live issued number from a driver who has approved you. Demo format: CP-UK-7H3M-K2NQ-W.
Live
Prefix cdp_live_. Issued only after KYB approval. Same paths, same status rules.
Authenticate
Partner API routes under /api/v1 use the API key. The signed-in business hub uses a user session and is not for server-to-server calls. The Courier Passport iOS and Android apps use /api/app/v1 with a driver JWT — never ship a partner key in a native app.
Header
Authorization: Bearer cdp_test_00000000_your_sandbox_keyRequest consent
The driver shares a number, QR, or link. You then request access. They approve on their phone. A lookup without a live consent returns 403. Incomplete applications are not passports — you cannot consent against a number that has not been issued.
POST /api/v1/consents
curl -sS https://api.courierpassport.com/api/v1/consents \
-H "Authorization: Bearer cdp_test_00000000_your_sandbox_key" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"passport_number":"CP-UK-7H3M-K2NQ-W"}'Response 201 (or the existing row): { "id": "018f3d4a-9c2e-7b11-8a4d-6f2c1b0a9e88", "status": "pending" }. Poll the lookup after the driver approves, or wait for your product UI. Status granted means you may look up.
Look up a passport
Re-evaluate on every call. Do not cache valid for days. Subscribe to webhooks for lapses between lookups. The public QR page is not a substitute for this API — it never tells an anonymous scanner whether the passport is still valid.
GET /api/v1/passports/{number}
curl -sS https://api.courierpassport.com/api/v1/passports/CP-UK-7H3M-K2NQ-W \
-H "Authorization: Bearer cdp_test_00000000_your_sandbox_key" \
-H "Accept: application/json"200 body
{
"passport_number": "CP-UK-7H3M-K2NQ-W",
"status": "valid",
"issued_at": "2026-09-15T12:00:00+00:00",
"valid_until": "2026-12-15T12:00:00+00:00",
"driver": { "legal_name": "Alex Driver" },
"checks": {
"identity": { "label": "Identity", "status": "passed", "expires_at": "...", "evidence_strength": "provider", "result_band": null },
"insurance": { "label": "Courier van insurance", "status": "passed", "expires_at": "...", "evidence_strength": "document", "result_band": null }
}
}How to treat status
| status | Your decision |
|---|---|
| valid | Pass. Required checks are in date. |
| expiring | Pass with a warning. A required check lapses within 30 days. |
| not_issued | Fail. No issued passport for that number — including unknown numbers. |
| expired | Fail. A required check has lapsed. Same number can revalidate. |
| suspended | Fail. Ops has suspended this passport. |
Webhooks
Register an HTTPS endpoint in the business hub (POST /api/partner/webhooks while signed in). The signing secret is shown once. We POST JSON and sign the raw body with HMAC-SHA256 in X-Courier-Passport-Signature. No document payloads. Localhost HTTP is allowed only in development.
- passport.issued
- passport.expiring
- passport.expired
- passport.revalidated
- passport.suspended
- consent.revoked
POST body
{
"event": "passport.expired",
"passport_number": "CP-UK-7H3M-K2NQ-W",
"status": "expired",
"occurred_at": "2026-09-15T12:00:00+00:00"
}Verify signature (Node.js)
import crypto from "node:crypto";
const expected = crypto.createHmac("sha256", signingSecret).update(rawBody).digest("hex");
const header = request.headers.get("x-courier-passport-signature") ?? "";
if (
expected.length !== header.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header))
) {
throw new Error("Invalid webhook signature");
}Errors and limits
| HTTP | When |
|---|---|
| 400 | Malformed passport number, or no issued passport for that number on consent. |
| 401 | Missing or invalid API key. |
| 403 | Driver has not granted a live consent for this lookup. |
| 429 | Rate limit — 120 requests per minute per IP on /api/v1. |
| 500 | Unexpected error. Message is generic. Retry with backoff. |
Error bodies are { "error": "…" }. Unknown passport numbers on lookup return not_issued rather than 404, so you cannot probe the directory.
What you receive
- Passport number, overall status, issue and valid-until dates
- Driver legal name
- Required-check summary: type, status, dates, evidence strength
- DBS result band only (for example clear)
What you never receive
- Veriff or identity images
- Raw DVLA enquiry payloads
- DBS certificate PDFs or health files
- A transfer of right-to-work or insurance legal duty
Ready to integrate
Create a sandbox key, request consent on a test passport, then wire valid / expiring into your onboarding decision.