Verification lifecycle
Build with

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
Build with

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.

code
  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)
StatusMeaningSettled?
not_startedThe link exists but nobody has opened it.No
in_progressThe applicant is part-way through.No
processingSubmitted. Checks are running, or a decision has not landed.No
in_reviewYour workflow asked a person to decide this one.No
awaiting_resubmissionA reviewer sent it back to redo some steps.No
approvedAccepted, automatically or by a person.Yes
declinedRejected, automatically or by a person.Yes
abandonedOpened, then left unfinished past its deadline.Yes
expiredTimed out without ever being opened. Send a new link.Yes
errorA 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
Build with

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.

reasonCodecheckStatusMeaningRetry?
document_unreadablefailedNo text could be read from the document photo.New photo
document_blurryfailedThe document photo was too blurry to read.New photo
document_type_mismatchfailedThe document doesn't match the selected ID type.Fix input
id_number_not_foundfailedNo ID number could be read from the document.New photo
document_data_mismatchfailedSubmitted name/DOB don't match the document.Fix input
document_expiredfailedThe document has expired.New document
selfie_mismatchfailedThe selfie doesn't match the government photo.New selfie
gov_data_mismatchfailedSubmitted details don't match the government record.Fix input
gov_validation_failedfailedThe government database flagged the ID as failed.No
identity_not_foundnot_foundThe ID number wasn't found in the government database.Fix input
unsupported_id_typeerrorThis ID type isn't supported for the country.No
provider_errorerrorA temporary provider error occurred; not charged.Yes
media_not_founderrorAn uploaded file expired before processing; not charged.Yes
insufficient_creditserrorYour credit balance was too low; not charged.After top-up
system_errorerrorAn unexpected server error occurred; not charged.Yes
id_type_not_enablederrorThis ID type isn't enabled for your organisation.No
document_verification_disablederrorDocument verification is disabled for your organisation.No
gov_db_check_disablederrorGovernment database checks are disabled.No
sandbox_not_foundnot_foundSandbox test ID returned the canned not_found outcome.
sandbox_failedfailedSandbox test ID returned the canned failed outcome.

When more than one check fails (e.g. selfie and data), reasonCode reflects the primary failure while reason still describes every issue. Retries must reuse the same requestId.

End-to-end flow
Build with

  1. Configure: GET /config to learn enabled ID types and their features.
  2. Capture & upload: for document/liveness flows, POST /upload each file → mediaId.
  3. Create: POST /verify with country, idType, optional idNumber, userData, mediaIds, and a unique metadata.requestId. Returns 202 + verificationId, status pending.
  4. Settle: the platform fires verification.started, then exactly one terminal event (verification.completed, .failed, .not_found, or .error).
  5. Reconcile: update your records from the webhook (or a GET /status/:id poll).

Idempotency
Build with

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 requestId per logical verification.
  • Reuse it verbatim on any retry (timeout, 5xx, 429).

See create verification.

Designing your handler
Build with

  • 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, store checkStatus alongside it. approved → pass; declined → reject or request a retry; error → retry or escalate; in_review and awaiting_resubmission → keep waiting, a person is involved. Storing checkStatus too is what lets you answer why an exception was allowed.
  • Branch on reasonCode to tailor the user's next step (e.g. "your ID has expired" on document_expired); surface reason to your support team for detail. See failure reason codes.

Data & privacy notes
Build with

  • 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 the GET /status/:id response, which returns result.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 error without persisting the submitted PII, and any uploaded media is discarded immediately.