Errors and status codes
Errors return a JSON body with an error field:
{ "error": "invalid apikey" }
Two responses carry more than that, and both are worth handling by their machine-readable field rather than their message text.
Status codes
| Code | Meaning | What to do |
|---|---|---|
400 | Malformed request, unknown ID, or a rejected value | Fix the request. An unknown UUID also lands here, not on 404 |
401 | Missing or invalid apikey or bearer token | Check the header. A key deleted in the dashboard fails this way |
402 | Free plan — this action needs a paid one | See below |
403 | Authenticated, but not allowed | Not a member of the owning organization, or a Premium+ schema from a non-Premium+ account |
404 | No such policy or organization code | |
409 | State conflict — already on a paid plan, no billing account, voucher already redeemed | Not retryable as-is |
410 | A policy request token has expired | Mint a new one |
429 | Too many code-exchange attempts from one IP | Back off. Only /vouchers/exchange is throttled |
500 | Our side | Safe to retry |
502 | An upstream service (Stripe, the wallet agent) failed | Retry with backoff |
503 | A feature is not configured on this deployment | Not retryable. Contact us |
402 — upgrade required
{
"error": "This action needs a paid plan. Contact our team to upgrade.",
"code": "UPGRADE_REQUIRED",
"tier": "FREE"
}
Branch on code. The message is written for a person and may change; UPGRADE_REQUIRED
will not. See Plans, tiers and usage.
400 sql: no rows in result set
You will meet this one. It is what a lookup for an ID that does not exist returns —
a wrong verification ID, a customer_id that was never opened against this verification, a
credential from another account. Read it as “not found”, and check the ID before checking
anything else.
A FAILED verification is not an error
GET /verifications/{individual_id}/verify returns 200 with status: "FAILED" when the
person presented a credential that did not pass. The HTTP call worked. The answer was no.
validation_error says why:
| Message shape | Cause |
|---|---|
| names an attribute and an operator | A condition was not satisfied |
credential issuer … is not trusted by this verification | The issuer is not in the trusted set |
presented credential carries no issuer, and this verification pins its issuers | The issuer could not be identified, and this verification pins |
Retrying
POST /verifications/individuals is idempotent per (customer_id, verification_id), so
retrying it is safe. GET /verifications/{id}/connect is safe to call repeatedly — it
returns the same link for 2 minutes, then mints a new one. Everything else that mutates
(issuing, minting keys, minting codes) creates a new record on each call, so retry those
only on a timeout, and check before you repeat.
Getting help
Contact us through the contact page. Include the verification ID and the individual ID, and the timestamp. With those we can trace one run end to end, which is much faster than reasoning from a status code.