Verification lifecycle
A verification is asynchronous. Creating one returns instantly with processing; the platform then runs the checks, a decision graph may route it to a person, and it settles. You learn the outcome via webhooks or polling.
States
Every surface reports state with the same vocabulary, and each row carries two fields: status (what happened) and checkStatus (what the checks found). They match unless a person overrode the automated result.
not_started ──► in_progress ──► processing ──┬──► approved
│
├──► declined
│
├──► in_review ──┬──► approved
│ ├──► declined
│ └──► awaiting_resubmission ──► processing
│
└──► error
not_started ──► expired (nobody ever opened the link)
in_progress ──► abandoned (opened, then left unfinished)| Status | Meaning | Settled? |
|---|---|---|
not_started | The link exists but nobody has opened it. | No |
in_progress | The applicant is part-way through. | No |
processing | Submitted. Checks are running, or a decision has not landed. | No |
in_review | Your workflow asked a person to decide this one. | No |
awaiting_resubmission | A reviewer sent it back to redo some steps. | No |
approved | Accepted, automatically or by a person. | Yes |
declined | Rejected, automatically or by a person. | Yes |
abandoned | Opened, then left unfinished past its deadline. | Yes |
expired | Timed out without ever being opened. Send a new link. | Yes |
error | A fault on our side. You were not charged. | Yes |
checkStatus is the narrower question of what the checks themselves found: pending, verified, failed, not_found or error. It never moves once they finish.
A verification whose checks did not pass always carries a human-readable reason (a full sentence, safe to show a user) and a stable reasonCode you can branch on. Both stay populated even if a reviewer later approves it, because they describe what was overridden. See failure reason codes below.
An ID the government database does not hold is a
declined, not a status of its own.reasonCode: "identity_not_found"is what distinguishes it from a failed face match, which is the level of detail worth branching on.
Failure reason codes
reasonCode is a stable machine token paired with reason on every non-success outcome. Branch on the code to drive tailored messaging or retry logic; show or log the reason for detail. It appears in status responses and webhook payloads (data.reasonCode).
The middle column is checkStatus, not status: a reason code describes what the CHECKS found, which is why it survives a reviewer later approving the verification.
New codes may be added over time, so treat an unrecognised code as a generic failure.
reasonCode | checkStatus | Meaning | Retry? |
|---|---|---|---|
document_unreadable | failed | No text could be read from the document photo. | New photo |
document_blurry | failed | The document photo was too blurry to read. | New photo |
document_type_mismatch | failed | The document doesn't match the selected ID type. | Fix input |
id_number_not_found | failed | No ID number could be read from the document. | New photo |
document_data_mismatch | failed | Submitted name/DOB don't match the document. | Fix input |
document_expired | failed | The document has expired. | New document |
selfie_mismatch | failed | The selfie doesn't match the government photo. | New selfie |
gov_data_mismatch | failed | Submitted details don't match the government record. | Fix input |
gov_validation_failed | failed | The government database flagged the ID as failed. | No |
identity_not_found | not_found | The ID number wasn't found in the government database. | Fix input |
unsupported_id_type | error | This ID type isn't supported for the country. | No |
provider_error | error | A temporary provider error occurred; not charged. | Yes |
media_not_found | error | An uploaded file expired before processing; not charged. | Yes |
insufficient_credits | error | Your credit balance was too low; not charged. | After top-up |
system_error | error | An unexpected server error occurred; not charged. | Yes |
id_type_not_enabled | error | This ID type isn't enabled for your organisation. | No |
document_verification_disabled | error | Document verification is disabled for your organisation. | No |
gov_db_check_disabled | error | Government database checks are disabled. | No |
sandbox_not_found | not_found | Sandbox test ID returned the canned not_found outcome. | — |
sandbox_failed | failed | Sandbox test ID returned the canned failed outcome. | — |
When more than one check fails (e.g. selfie and data),
reasonCodereflects the primary failure whilereasonstill describes every issue. Retries must reuse the samerequestId.
End-to-end flow
- Configure:
GET /configto learn enabled ID types and their features. - Capture & upload: for document/liveness flows,
POST /uploadeach file →mediaId. - Create:
POST /verifywithcountry,idType, optionalidNumber,userData,mediaIds, and a uniquemetadata.requestId. Returns202+verificationId, statuspending. - Settle: the platform fires
verification.started, then exactly one terminal event (verification.completed,.failed,.not_found, or.error). - Reconcile: update your records from the webhook (or a
GET /status/:idpoll).
Idempotency
metadata.requestId is your idempotency key. Submitting the same requestId again returns the existing verification rather than creating a new one (and never double-charges). Always:
- Generate one stable
requestIdper logical verification. - Reuse it verbatim on any retry (timeout,
5xx,429).
See create verification.
Designing your handler
- Treat results as eventual. Never block a user flow on a synchronous result; show a "verifying…" state and resolve on webhook/poll.
- Be idempotent on your side too. The same terminal event may be delivered more than once; key your processing on
verificationId. - Branch on
status, storecheckStatusalongside it.approved→ pass;declined→ reject or request a retry;error→ retry or escalate;in_reviewandawaiting_resubmission→ keep waiting, a person is involved. StoringcheckStatustoo is what lets you answer why an exception was allowed. - Branch on
reasonCodeto tailor the user's next step (e.g. "your ID has expired" ondocument_expired); surfacereasonto your support team for detail. See failure reason codes.
Data & privacy notes
- ID numbers are stored as plaintext (
idNumber) so they appear unmasked in your dashboard and in webhook payloads (data.idNumber). The only place they're masked is theGET /status/:idresponse, which returnsresult.idNumberMasked(e.g.1234•••901); that endpoint is designed for safe polling from frontends, so it never echoes the full number back to whoever holds the API key. - If an ID type or feature is not enabled for your org, the attempt is recorded as
errorwithout persisting the submitted PII, and any uploaded media is discarded immediately.