Errors
Every Certivant API error code explained — response shape, HTTP status mapping, per-code handling guidance, and a robust retry wrapper you can drop into your integration.
Every error response from the Certivant API follows the same shape. Build one error handler that consumes this shape and you can handle every endpoint uniformly.
Response shape
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Body validation failed.",
"fields": {
"applicant.dateOfBirth": "Invalid date format. Expected YYYY-MM-DD.",
"applicant.nationality": "Must be a 2-character ISO 3166-1 alpha-2 code."
}
}
}
| Field | Type | Notes |
|---|---|---|
success | boolean | Always false on errors |
error.code | string | Machine-readable. Switch on this for control flow |
error.message | string | Human-readable. Safe to log; not safe to display to applicants directly |
error.fields | object | Only present on VALIDATION_ERROR. Maps dot-notation field paths to per-field error messages |
Error message strings are not guaranteed to be stable across
versions. Use code for any logic; treat message as logging /
developer ergonomics.
HTTP status code → error code
| HTTP | Typical error.code | What it means |
|---|---|---|
400 | VALIDATION_ERROR | Request body failed Zod validation. Check fields |
400 | BAD_REQUEST | Generic bad request (e.g. submitting before all docs uploaded) |
401 | UNAUTHORIZED | Missing, malformed, revoked, or expired API key |
402 | verification_unavailable | The organization has no credit left and isn't set to continue without credit |
403 | FORBIDDEN | The key is valid but lacks the required scope, comes from an address outside its IP allowlist, is restricted to other forms, or the resource belongs to a different organization |
404 | NOT_FOUND | The resource (req_…, doc_…, etc.) doesn't exist or isn't visible to this key's organization |
409 | CONFLICT | State transition not allowed (e.g. submitting a request twice), or a unique constraint violation (externalRef already in use) |
413 | BAD_REQUEST | File upload over 10 MB |
415 | BAD_REQUEST | Unsupported Content-Type for the endpoint (e.g. JSON body to a multipart endpoint) |
429 | RATE_LIMITED | Per-minute rate limit exceeded. See Rate limits |
500 | INTERNAL_ERROR | Unexpected server error. Always logged on our side; retry safe |
502 | BAD_GATEWAY | An upstream service (sanctions provider, payment gateway, registry) is down. Retry after a delay |
503 | SERVICE_UNAVAILABLE | Database degraded. Retry with backoff |
Error code catalogue
UNAUTHORIZED
The API key in the Authorization header is missing, malformed, or revoked.
{ "success": false, "error": { "code": "UNAUTHORIZED", "message": "Invalid or revoked API key." } }
error.reason tells you which of the cases it is: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired or api_key_rotated (the key was rotated and its overlap window has ended). The full list is in Authentication.
How to handle: Confirm the header is Authorization: Bearer ck_live_… (or ck_test_…; older keys start with kyk_). Check the dashboard for the key's status. If it shows revoked or expired, create or rotate a key.
FORBIDDEN
The key is valid but doesn't have the required scope, or the resource belongs to a different organization than the key's organization.
{ "success": false, "error": { "code": "FORBIDDEN", "message": "This API key does not include the kyc:manage scope required for this endpoint." } }
error.reason is insufficient_scope (the key lacks a scope), ip_not_allowed (the caller's address is not on the key's IP allowlist) or form_not_allowed (the key is restricted to other forms).
How to handle: Check the scopes and restrictions on your key in the dashboard (KYC → Developer, the pencil icon). You can edit a key's permissions, form and workflow restrictions, IP allowlist and expiry without changing its secret; the secret and the environment (live or test) cannot be changed.
verification_unavailable (HTTP 402)
Your organization has no credit left and is not set to continue without credit, so Certivant cannot accept a new verification. The response uses the code verification_unavailable:
{ "success": false, "error": { "code": "verification_unavailable", "message": "This organization is temporarily unable to accept new verifications. Please contact them to continue." } }
How to handle: Top up the credit balance in Settings → Credits & Usage (your owners and admins also get a notification and an email, at most once a day). Applicants who hit the same condition in a form or hosted page see a page asking them to contact your organization; the page never mentions credit or money. See Billing & credits.
NOT_FOUND
{ "success": false, "error": { "code": "NOT_FOUND", "message": "Request not found." } }
How to handle: Verify the resource ID. A common cause is using an ID from a different environment — staging IDs don't exist in production and vice versa.
VALIDATION_ERROR
The request body failed schema validation. The fields map lists every failing field with a per-field message.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Body validation failed.",
"fields": {
"type": "Required",
"applicant.email": "Invalid email format"
}
}
}
How to handle: Render fields next to your form inputs. Field paths use dot notation for nested fields and [N] for array indices (directors[0].dateOfBirth).
BAD_REQUEST
Generic bad request that doesn't fit other categories.
{ "success": false, "error": { "code": "BAD_REQUEST", "message": "Cannot submit a request with pending documents. Upload the remaining documents first." } }
How to handle: Read the message. These usually indicate a sequencing problem — call the prerequisite endpoint first.
CONFLICT
A state transition that isn't legal from the current state, or a unique-constraint violation.
{ "success": false, "error": { "code": "CONFLICT", "message": "externalRef already in use by request req_…" } }
How to handle: Look at the request's current state via GET /requests/:id and adjust. For unique-ref violations, either fetch the existing request (using ?externalRef=… on the list endpoint) or choose a different ref.
RATE_LIMITED
{ "success": false, "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded. Max 120 requests per 60s. Retry in 14s." } }
Headers on this response include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until the bucket refills). See Rate limits for retry strategy.
INTERNAL_ERROR
Something broke on our side. Certivant logs every 500 with full context.
{ "success": false, "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred. Please try again." } }
How to handle: Retry once after a short delay. If it persists, check the status page and contact support with the request ID.
BAD_GATEWAY and SERVICE_UNAVAILABLE
An upstream dependency is down (commercial register API, sanctions data provider, storage) or the database is degraded.
{ "success": false, "error": { "code": "BAD_GATEWAY", "message": "Sanctions screening provider is unreachable. The request remains in PENDING state — Certivant will retry automatically." } }
How to handle: Treat these as transient. Implement exponential backoff (1s, 2s, 4s, 8s, 16s) and stop after ~5 attempts. The request is not lost — Certivant's own queue will resume processing when the upstream recovers.
A robust error handler
async function callCertivant<T>(
fn: () => Promise<Response>,
options: { maxRetries?: number } = {},
): Promise<T> {
const maxRetries = options.maxRetries ?? 3;
let attempt = 0;
let delay = 500;
while (true) {
const res = await fn();
if (res.ok) return (await res.json()) as T;
const body = (await res.json().catch(() => null)) as
| { error?: { code: string; message: string } }
| null;
const code = body?.error?.code ?? "INTERNAL_ERROR";
// Retry on transient errors
if (
attempt < maxRetries &&
(res.status === 429 || res.status >= 500)
) {
const retryAfter =
res.status === 429
? Number(res.headers.get("X-RateLimit-Reset") ?? 60) * 1000
: delay;
await sleep(retryAfter);
delay *= 2;
attempt += 1;
continue;
}
// Bubble everything else up
throw new CertivantError(code, body?.error?.message ?? "Unknown error", res.status);
}
}
This pattern handles every error you've documented above:
200-class → parse and return.429→ wait forX-RateLimit-Reset, retry.5xx→ exponential backoff, retry.4xx→ throw immediately (your code is wrong; no retry will fix it).
API reference (OpenAPI)
The complete Certivant REST API as an OpenAPI 3.1 document — a public JSON spec you can import into Postman or feed to a code generator, plus an interactive reference with every endpoint, schema, scope and webhook event.
Rate limits
Certivant enforces a plan-based per-minute API rate limit — learn how to read rate-limit headers, handle 429 responses, retry correctly, and design your integration to stay under the limit.