Statuses
The lifecycle of a verification, and which states you are allowed to act on
status tells you where a verification is in its life. verdict tells you what was decided. They
are separate fields answering separate questions, and reading one for the other is the most common
integration bug we see.
The six statuses
| Status | Terminal | Meaning |
|---|---|---|
pending | no | Created. The user has not finished submitting. |
processing | no | Submissions received, checks are running. |
approved | yes | Every required check passed. |
rejected | yes | At least one required check failed. |
manual_review | yes | A human needs to look at this before it is final. |
expired | yes | The user never completed the session in time. |
There is no completed status. A finished verification lands on approved, rejected or
manual_review, and treating those three as the terminal set is the correct way to write your
handler.
Which states you may act on
Do nothing on pending or processing. A verification in those states may already have some checks
resolved, but the roll-up is not final and can still change.
approved, rejected and manual_review are the decision points. expired is not a decision, it
means the user dropped out, so the right response is usually to invite them to start again rather
than to reject them.
switch (verification.status) {
case "approved":
return grantAccess(user);
case "rejected":
return declineWithReasons(user, verification.reason_codes);
case "manual_review":
return holdPendingReview(user);
case "expired":
return inviteToRetry(user);
default:
return; // pending or processing: not a decision yet
}manual_review is terminal for you, not for us
manual_review means the automated pipeline has finished and handed the case to a person. It is a
terminal status from your integration's point of view, so you act on it, typically by holding the
account in a restricted state.
When the review concludes, the verification's verdict is updated and you receive another webhook. That is why your handler must be safe to run more than once for the same verification, and why you should apply the latest state rather than assume the first terminal event you saw is the last word.
Expiry
Every verification carries expires_at. An unfinished session past that point moves to expired
and its hosted link and client token stop working. Create a fresh verification for a returning user
rather than trying to revive an expired one. The expired record stays in your history.
Per-check statuses
Each check inside a verification has its own status: pending, processing, completed or
failed. failed means the check could not be evaluated at all, for example because a capture was
unusable, which is different from the check completing with a reject verdict. Both surface reason
codes, so your user-facing copy can distinguish "we could not read this" from "this did not pass".