Verification lifecycle
In this video you will see the difference between a liveness check and a face match, and how unclear cases reach a reviewer. Everything it covers is also written out below.
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 a submitted verification carries two fields: status (the outcome, which you act on) and checkStatus (what the automated checks found, which explains it). They often disagree, and that is expected. See Status and checkStatus below for how to read the two together.
Status and checkStatus
A submitted verification answers two different questions, and each has its own field.
Act on status. Keep checkStatus as the explanation.
What each checkStatus value means
checkStatus covers the checks and nothing else. Your workflow’s decision rules, and any reviewer’s decision, never change it.
How status is decided
status is worked out from up to three things, in this order. The first one that applies wins.
- A reviewer’s decision. A person on your team approved it, declined it, or sent it back. This gives
approved,declinedorawaiting_resubmission. - Your workflow’s decision. When your workflow has decision rules, its verdict gives
approved,declinedorin_review. While the workflow is still deciding (for example, waiting on a watchlist screen or on a business’s directors),statusisprocessing, whatever the checks found. - The checks alone, when no workflow decision rules are involved:
So the two fields agree only when nothing but the checks decided the outcome. Once a workflow or a person is involved, they can say different things, and both are true.
Common combinations
An approved status with checkStatus: "failed" is not a contradiction. It records that the person was accepted, and that they were accepted despite a failed check. If you stored only approved, you could not later explain why that exception was allowed, which is exactly what an auditor or regulator asks about.
When the two change
checkStatusis set once per attempt. It moves frompendingto its result when the checks finish, then stays put. The one exception is a resubmission: when the applicant redoes a verification that was sent back, the same verification id starts a new attempt (attemptgoes up by one),checkStatusgoes back topending, and the checks run again.statuscan change after the checks finish. A workflow can reach its verdict minutes or days later, and a reviewer can decide, or change an earlier decision, at any time. Each of those changes is announced by theverification.status_updatedwebhook, withpreviousStatus, the newstatus, and asourcesaying who or what made the change.
Where you see them
If your workflow has decision rules, do not treat verification.completed as the final answer. Those events are sent the moment the checks finish, before your workflow has decided, so their status reflects the checks alone. The workflow’s verdict arrives afterwards as verification.status_updated (with source: "workflow"), and that event carries the final status. Without decision rules, the checks are the decision, and the status on verification.completed is final unless a reviewer later changes it.
Handling both in your code
- Update your own record from
status:approved→ onboard;declined→ reject or ask them to try again;in_reviewandawaiting_resubmission→ keep waiting, a person is involved;processing→ keep waiting;error→ retry or contact support. - Store
checkStatus,reasonandreasonCodenext to it. They explain the outcome, and they are the only record of an exception. - Use
reasonCode(read alongsidecheckStatus) to decide what to tell the applicant. See failure reason codes. - Take the latest
statusfromverification.status_updatedor from polling, not from the first webhook you received.
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. It appears in status responses and webhook payloads (data.reasonCode).
reason is written for the applicant, on every failed and not_found outcome. It is plain language you can forward to the person as it stands: it names what did not match and what to do next, and it deliberately carries no score, no threshold and nothing read back out of the government record. The numbers are not lost, they are structured fields on the result (facialConfidence, dataValidationDetails) where your support team reads them.
reason on an error outcome is written for YOU, not for them. Those say things like “top up your balance” or “contact Toden”, because nothing the applicant does will fix a verification service outage or an empty credit balance. Show the person a generic “something went wrong, please try again” and route the reason to your own team. The checkStatus column below tells the two apart: error is ours, failed and not_found are theirs.
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.
When more than one check fails (e.g. selfie and data), reasonCode reflects the primary failure (the selfie, before the data) while reason names every leg that failed in one sentence. Retries must reuse the same requestId.
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. See Status and checkStatus. - Branch on
reasonCodeto tailor the user’s next step (e.g. “your ID has expired” ondocument_expired), or show thereasonas it stands on afailedornot_foundoutcome. On anerroroutcome thereasonis addressed to you, so show the person a generic message instead. 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.
Location evidence and the verification map
When device or IP-location evidence is available, the verification detail page shows the masked IP, approximate place and a map when valid coordinates are available. A map is intentionally not shown when the result contains only a country, city or IP address without usable coordinates; the page explains the missing evidence instead of placing a misleading marker.
The same observation is connected to the verified person or business under Entities → Connected records → Locations & IPs. This gives investigators one history across identity verification and later monitored activity. The entity view groups repeated observations, keeps source and enrichment provenance, labels test evidence, and highlights useful signals such as new IP, rapid location change, shared IP or datacentre use.
Locations are approximate risk evidence, not proof of where a person was physically present. IP addresses stay masked by default; revealing a full address requires sensitive-case permission and creates an audit record.