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 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. 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 |
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 |
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. - 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.