Docs

Core concepts

Errors

What a failed call looks like, which failures are worth retrying, and how to retry without creating duplicate verifications.

Error shape

Errors come back as JSON with the HTTP status repeated in the body. statusMessage is a short machine-ish description; data is present on validation failures and carries the field-level detail.

400 Bad Request
{ "statusCode": 400, "statusMessage": "Invalid request body", "message": "Invalid request body", "data": { "issues": [ { "path": ["purpose"], "message": "Invalid enum value" } ] } }

Branch on the HTTP status code, not on the text of statusMessage. The wording can change without notice.

Status codes

CodeNameWhat it means
400Bad requestThe body failed validation. The data field carries the field-level detail. Not retryable, so fix the request.
401UnauthorizedThe key is missing, unrecognised, or expired. Not retryable. Check the Authorization header and whether the key has passed its expiry.
403ForbiddenThe key is valid but lacks the scope for this endpoint, or the request came from an IP outside the key's allowlist. Not retryable.
404Not foundNo such resource, or it belongs to another organization. IDs are scoped per organization, so a valid ID under the wrong key is a 404, not a 403.
429Too many requestsYou hit a rate limit. Retryable, but back off before trying again.
5xxServer errorSomething failed on our side. Retryable with backoff. Check the status page if it persists.

401 vs 403

These mean different things and are worth handling separately, because the fix is different:

  • 401 means we do not accept this key at all. Wrong secret, deleted key, or past its expiry.
  • 403 means the key is fine, but it isn't allowed to do this. Missing scope, or a blocked source IP.

A 403 will never fix itself by retrying. Add the scope in the dashboard, or add the caller's egress IP to the key's allowlist. See Authentication.

Retrying safely

Retry 429 and 5xx. Never retry 400, 401, 403 or 404. The same request will fail the same way and you will just burn your rate limit.

Use exponential backoff with jitter, and cap the attempts. A tight retry loop against a struggling service makes it worse.

retry.ts
const RETRYABLE = new Set([429, 500, 502, 503, 504]); async function withRetry(fn, attempts = 4) { for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (err) { const status = err.statusCode ?? err.response?.status; if (!RETRYABLE.has(status) || i === attempts - 1) throw err; // Exponential backoff with jitter: ~1s, 2s, 4s.const delay = 2 ** i * 1000 + Math.random() * 250; await new Promise(r => setTimeout(r, delay)); } } }

Don't create duplicate verifications

POST /verifications creates a new session every time it is called. If a request times out you cannot tell whether the session was created, and a blind retry gives you two: two sessions, two verification:created webhooks, and a user who may complete the wrong one.

Protect against this on your side:

  1. Generate your external_id before the call and persist it, so a retry reuses the same value.
  2. On a timeout, call GET /verifications and look for a session already carrying that external_id before creating another.
  3. Store the returned verification_id in the same transaction that marks your record as "verification started".

external_id is yours to define and is echoed back on every webhook, which makes it the natural key for reconciliation.

Timeouts

Set a client timeout. The SDKs default to a sensible one, raw fetch does not. Document extraction and face matching do real work and are slower than session creation, so allow more headroom on those than on a plain POST /verifications.

A timeout is not a failure signal. The work may have completed. Treat it as "unknown" and reconcile rather than assuming either outcome.

What to log

Log the status code, statusMessage, and your own external_id. Never log the request body of a verification or extraction call, because it contains identity documents and face images. Never log your API secret, including in a URL or an error dump that echoes headers.

When it keeps failing

Check Service Status first. If everything is green and calls are still failing, email contact@idproval.com with the status code, the approximate timestamp, and the external_id or verification_id. That is enough for us to find the request.

Stuck on this page?

Email contact@idproval.com and a real engineer will pick it up.

Business support