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.

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

Status and checkStatus

A submitted verification answers two different questions, and each has its own field.

FieldQuestion it answersValues
statusWhat is the outcome? Should this person be onboarded, rejected, or are we still waiting?The ten values in the table above.
checkStatusWhat 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.

checkStatusMeaning
pendingThe checks are still running.
verifiedEvery check passed.
failedA 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_foundThe ID number is not in the government database.
errorThe 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.

  1. A reviewer's decision. A person on your team approved it, declined it, or sent it back. This gives approved, declined or awaiting_resubmission.
  2. Your workflow's decision. When your workflow has decision rules, its verdict gives approved, declined or in_review. While the workflow is still deciding (for example, waiting on a watchlist screen or on a business's directors), status is processing, whatever the checks found.
  3. The checks alone, when no workflow decision rules are involved:
checkStatusstatus
pendingprocessing
verifiedapproved
faileddeclined
not_founddeclined
errorerror

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

statuscheckStatusWhat happened
processingpendingThe checks are running.
processingverified or failedThe checks have finished, but your workflow has not decided yet. waitingOn says what it is waiting for.
approvedverifiedThe checks passed and the person was accepted.
approvedfailed or not_foundThe 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.
declinedfailed or not_foundThe checks failed and the person was rejected.
declinedverifiedThe 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_reviewany finished valueYour workflow asked a person to decide. Wait for them.
awaiting_resubmissionany finished valueA reviewer sent it back so the applicant can redo some steps.
errorerrorA 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

  • checkStatus is set once per attempt. It moves from pending to 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 (attempt goes up by one), checkStatus goes back to pending, and the checks run again.
  • status can 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 the verification.status_updated webhook, with previousStatus, the new status, and a source saying who or what made the change.

Where you see them

WherestatuscheckStatus
GET /status/:id (publishable or secret key)CurrentCurrent, once submitted
GET /verifications/:id (secret key)CurrentCurrent
GET /verifications (secret key)Current, per rowCurrent, per row
verification.status_updated webhookThe new status, with previousStatusCurrent
verification.started, .completed, .failed, .not_found, .error webhooksWhen the checks finished, before any workflow decision. See below.The checks' result

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_review and awaiting_resubmission → keep waiting, a person is involved; processing → keep waiting; error → retry or contact support.
  • Store checkStatus, reason and reasonCode next to it. They explain the outcome, and they are the only record of an exception.
  • Use reasonCode (read alongside checkStatus) to decide what to tell the applicant. See failure reason codes.
  • Take the latest status from verification.status_updated or 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.

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 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_facefailedThe 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_missingfailedThe 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_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
identity_suspendedfailedThe 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_restrictedfailedThe 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_unverifiedfailedThe workflow restricts by age, but no date of birth could be read from the ID.New photo or ID
unsupported_id_typeerrorThis ID type isn't supported for the country.No
provider_errorerrorA temporary verification service 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. Only reached when credit pause is not active for your organisation, otherwise the verification pauses instead.After top-up
credit_hold_expirederrorA paused verification reached the end of its retention window before funding arrived; the captured documents were deleted.New capture
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 (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

  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

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

  • 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. See Status and checkStatus.
  • Branch on reasonCode to tailor the user's next step (e.g. "your ID has expired" on document_expired), or show the reason as it stands on a failed or not_found outcome. On an error outcome the reason is 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 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.

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.