Getting started
Errors & rate limits
Every refusal is a plain sentence that says what happened and what fixes it. Errors are JSON with a single envelope; machine-readable codes appear where a program should branch.
The error envelope#
{
"error": "More than one lot matches that identity. Provide lot_id.",
"code": "ambiguous_lot", // present when a program should branch on it
"candidateLotIds": ["LOT-…", "LOT-…"] // extra fields when they help fix the request
}
Status codes#
| Status | When | Example (verbatim) |
|---|---|---|
400 | The request is invalid as sent. | order_refs rejected: an entry looks like patient information. Use opaque references (order or PO numbers), never patient information. (phi_in_order_refs) |
401 | Missing or unrecognised API key. | A valid API key is required: Authorization: Bearer vk_live_... (or vk_test_...). |
404 | The referenced record does not exist for this account: an unknown lot_id or batch_id. | , |
409 | The request is coherent but conflicts with the record. | More than one lot matches that identity. Provide lot_id. (ambiguous_lot) |
429 | Rate or concurrency limit reached. | Rate limit exceeded. |
Rate limits#
Each API key has its own allowance on each endpoint, per minute, wherever it calls from. Beyond it the answer is 429, "code": "rate_limited", with Retry-After, retryAfterSeconds in the body, and X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. A second, per-address ceiling of the same size also applies on each endpoint.
| Endpoint | Paid plan, per key per minute | Supported API automation (Operations), per key per minute |
|---|---|---|
POST /v1/check | 600 | 1,200 |
POST /v1/passports | 600 | 1,200 |
POST /v1/seal-batches | 600 | 1,200 |
GET /v1/seal-batches/<id>/labels | 600 | 1,200 |
POST /v1/cleargate | 600 | 1,200 |
POST /v1/cleargate-batch | 600 | 1,200 |
POST /v1/eligibility | 600 | 1,200 |
GET /v1/network/source | 600 | 1,200 |
GET /v1/network/directory | 600 | 1,200 |
POST /v1/network/route | 600 | 1,200 |
GET /v1/pharmacies, /v1/suppliers, /v1/lookups, /v1/watches, /v1/exposure | 300 per workspace free, 600 on a paid plan, 1,200 with supported API automation; counted apart for test and live keys | |
- Per key, per minute, by plan: each
/v1endpoint allows 600 requests per key per minute on a paid plan, and 1,200 with supported API automation (Operations); beyond it,429 Rate limit exceeded./v1keys need a paid plan. - The buyer read API: 300 requests a minute per workspace, free, counted apart for test and live keys; 600 on a paid plan and 1,200 with supported API automation. Every answer carries
X-RateLimit-Limit. - Concurrency on checks:
POST /v1/checkreads a whole document, so each key may have a small number of checks in flight at once (4 by default). Beyond that the request is shed immediately with429,"code": "concurrency_limited", aRetry-Afterheader, andretryAfterSecondsin the body: nothing is queued, nothing is lost, retry after the stated delay. - Metering: document reads are metered per key per day; the workspace shows usage.
Release refusal codes#
Passport issuance enforces the release rules on every path. These are the codes POST /v1/passports can answer, with 409 unless stated; each refusal's sentence names its fix. Before a purchased lot issues anything: it is cleared, carries no hold or recall, a certificate has been read against it, and a named person has reviewed it.
| Code | Meaning | The fix |
|---|---|---|
ambiguous_lot | More than one lot matches the identity you sent. | Send lot_id (the refusal lists candidateLotIds). |
ambiguous_legacy_passport | An older record for this order matches more than one lot. | Send lot_id. |
phi_in_order_ref | order_id reads like patient information. | Use an opaque order or Rx number, never a name or a date of birth. |
phi_in_customer_ref | customer_ref reads like patient information. | Use an opaque reference. |
as_purchased_required | The lot is a purchased lot, not a preparation. | Send as_purchased: true (asPurchased) when it is dispensed as purchased, or record the preparation made from it and issue for that. |
copy_determination_required | The substance is essentially a copy of a marketed drug under your substance screen. | Confirm the prescriber's determination for the order is in your copy file (copy_determined: true). |
lot_pending_review | The lot is not cleared. | Clear the lot's documents, or record a person's review. |
cleargate_hold | The lot carries a ClearGate hold. | Close the reason the decision names; the next decision lifts the hold. |
release_policy_error | The release policy could not be evaluated. | Retry; the release is held, never waved through. |
lot_recalled | Requirement not met: no recall on the lot. | The refusal names what to do on the lot record. |
lot_on_hold | Requirement not met: no hold by a person. | The refusal names what to do on the lot record. |
no_503a_route | Requirement not met: a 503A route for the substance. | The refusal names what to do on the lot record. |
ingredient_held | Requirement not met: no ingredient lot held. | The refusal names what to do on the lot record. |
lot_not_cleared | Requirement not met: the lot is cleared. | The refusal names what to do on the lot record. |
ingredient_unreviewed | Requirement not met: every ingredient lot reviewed by a person. | The refusal names what to do on the lot record. |
no_certificate | Requirement not met: a certificate read against the lot. | The refusal names what to do on the lot record. |
person_review_required | Requirement not met: a person reviewed the lot. | The refusal names what to do on the lot record. |
specification_unresolved | A selected product specification has failed, missing, stale or unassessed evidence. | As the refusal states. |
material_chain_unresolved | The recorded material chain has unresolved source or lot-mapping evidence. | As the refusal states. |
no_lot | This passport names no canonical lot, so there is nothing to release evidence about. | As the refusal states. |
supplier_critical_gap | The supplier file is missing evidence its own persona schema calls critical. | As the refusal states. |
supplier_unqualified | No supplier qualification file exists for the source named on this passport. | As the refusal states. |
supplier_not_linked | The supplier file exists but does not link to this lot: no one has signed a qualification decision on it and it is not a reviewed packet, or the supplier is signed "Not qualified". | Sign a qualification decision on the supplier's file. |
dispenser_identity_required | This passport names the dispensing pharmacy itself as the source. | As the refusal states. |
lot_document_alias | The same document is on record for this lot under more than one supplier name, so this passport cannot state which source it came from. | As the refusal states. |
demo_lot_sandbox_only | This lot is demonstration data and cannot back a production passport. | As the refusal states. |
evidence_stale | The evidence snapshot this decision was made from is no longer current. | As the refusal states. |
sandbox_decision | This release was authorised by a sandbox decision, which cannot release production evidence. | As the refusal states. |
human_review_required | This workspace requires a person to release, and no human release is recorded. | As the refusal states. |
cleargate_required | This workspace requires a ClearGate Allow decision on the lot before a passport can be issued. | As the refusal states. |
no_production_capability | This workspace is not activated for production evidence. | As the refusal states. |
phi_in_order_ref and phi_in_customer_ref answer 400; release_policy_error answers 500. A seal batch refuses an entry that reads like patient information with 400 phi_in_order_refs, and a test key with 403 test_mode_unsupported.
Signing refusals#
A signature in the workspace (a lot review, a hold, the 503A basis, a by-hand release, a Seal assignment, a quality unit decision) is made by a signed-in person, never by an API key. The routes that write one answer 428 when the person must confirm who they are first; nothing is recorded, and the same request succeeds once they have.
| Code | Meaning | The fix |
|---|---|---|
password_step_up_required | The first signature in a session, from a seat with a password and no authenticator, is made with the password as well as the name (21 CFR 11.200(a)(1)(i)). | POST /api/auth/password/step-up with { "password": ... }, then send the request again. The rest of the session signs without asking. A wrong password answers 403. |
mfa_step_up_required | The seat has an authenticator, and its second factor was not confirmed in the last minutes. | POST /api/auth/mfa/step-up with { "code": ... } (an authenticator or recovery code), then send the request again. |
Each signature records how it was made: reauthenticated, and reauthMethod password or authenticator.
Retries and idempotency#
- On
429, honorRetry-Afterwhen present; otherwise back off and retry. Shed requests were never processed. - Passport issuance is idempotent per order: re-issuing the same order returns the existing record with
reused: truerather than a duplicate. - Seal batches are not idempotent: every successful
POST /v1/seal-batchesmints new serials. Retry only requests that failed.