On this page
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.
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 |
Status and checkStatus
A submitted verification answers two different questions, and each has its own field.
| Field | Question it answers | Values |
|---|---|---|
status | What is the outcome? Should this person be onboarded, rejected, or are we still waiting? | The ten values in the table above. |
checkStatus | What did the automated checks find? The document read, the government database lookup, the face match, the chip read. | pending, verified, failed, not_found, error |
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.
checkStatus | Meaning |
|---|---|
pending | The checks are still running. |
verified | Every check passed. |
failed | A check ran and did not pass: the face did not match, the details did not match the record, the document had expired, and so on. reasonCode says which. |
not_found | The ID number is not in the government database. |
error | The checks could not run because of a fault on our side. You were not charged. |
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:
checkStatus | status |
|---|---|
pending | processing |
verified | approved |
failed | declined |
not_found | declined |
error | error |
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
status | checkStatus | What happened |
|---|---|---|
processing | pending | The checks are running. |
processing | verified or failed | The checks have finished, but your workflow has not decided yet. waitingOn says what it is waiting for. |
approved | verified | The checks passed and the person was accepted. |
approved | failed or not_found | The person was accepted despite a failed check. A reviewer made an exception, or one of your workflow rules approved it. reason and reasonCode say what the exception was for. |
declined | failed or not_found | The checks failed and the person was rejected. |
declined | verified | The checks passed, but the person was rejected anyway: a rule in your workflow declined them (a watchlist match, for example), or a reviewer did. |
in_review | any finished value | Your workflow asked a person to decide. Wait for them. |
awaiting_resubmission | any finished value | A reviewer sent it back so the applicant can redo some steps. |
error | error | A fault on our side. You were not charged. Retry or contact support. |
not_started, in_progress, abandoned, expired | (absent) | Nothing has been submitted, so no checks have run and there is no checkStatus. |
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
| Where | status | checkStatus |
|---|---|---|
GET /status/:id (publishable or secret key) | Current | Current, once submitted |
GET /verifications/:id (secret key) | Current | Current |
GET /verifications (secret key) | Current, per row | Current, per row |
verification.status_updated webhook | The new status, with previousStatus | Current |
verification.started, .completed, .failed, .not_found, .error webhooks | When the checks finished, before any workflow decision. See below. | The checks' result |
If your workflow has decision rules, do not treat
verification.completedas the final answer. Those events are sent the moment the checks finish, before your workflow has decided, so theirstatusreflects the checks alone. The workflow's verdict arrives afterwards asverification.status_updated(withsource: "workflow"), and that event carries the finalstatus. Without decision rules, the checks are the decision, and thestatusonverification.completedis 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 Myaza", 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.
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 photo it was compared with: the government record, the chip, or the photo printed on the document (facialMatchSource says which). | New selfie |
document_photo_no_face | failed | The photo printed on the document was the only photo the selfie could be compared with, and no face could be seen on it. Your workflow declines that case rather than sending it to review. | New photo |
liveness_video_missing | failed | The recording of the liveness check did not arrive or could not be read, so liveness could not be re-checked. Your workflow declines that case rather than sending it to review. | 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 |
identity_suspended | failed | The government record exists, but the issuer has suspended it (a suspended NIN is the usual case). No biodata or face result is returned, since nothing on the record can be verified against. The holder has to have it restored (for a NIN, at a NIMC enrolment centre). | After restoring |
age_restricted | failed | The holder's age is outside the workflow's age limits for their country, and the workflow declines that case rather than sending it to review. | No |
age_unverified | failed | The workflow restricts by age, but no date of birth could be read from the ID. | New photo or ID |
unsupported_id_type | error | This ID type isn't supported for the country. | No |
provider_error | error | A temporary verification service 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. Only reached when credit pause is not active for your organisation, otherwise the verification pauses instead. | After top-up |
credit_hold_expired | error | A paused verification reached the end of its retention window before funding arrived; the captured documents were deleted. | New capture |
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 (the selfie, before the data) whilereasonnames every leg that failed in one sentence. 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. 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.