Skip to content

Verification lifecycle & results ​

Every verification is one session that moves through a lifecycle and ends on a decision. This page is the reference for the statuses a session can hold, how they map to a decision, the reasons behind a decision, and the result data you read back (via the SDK, the Public API, or webhooks).

Session statuses ​

A session's status is its lifecycle state. Progress statuses advance as the user completes each step; the flow then settles on a terminal status (or waits in requires_review for a human).

statusTerminal?Meaning
pendingnoSession created; the client token is minted but the user hasn't started.
startednoThe user opened the widget and began the flow.
document_submittednoDocument image(s) uploaded.
address_submittednoAddress step completed (only if the workflow includes it).
liveness_submittednoSelfie / liveness captured.
processingnoRunning OCR, liveness, face match, and the decision engine.
requires_reviewsoftAuto-decision was inconclusive; waiting on a human reviewer.
approvedyesIdentity verified; all checks passed (auto or after review).
rejectedyesVerification failed, or a reviewer rejected it.
expiredyesThe session's TTL elapsed before the user finished.
cancelledyesCancelled via the API or the dashboard.

requires_review is a soft-terminal state: the session stops advancing on its own, but a reviewer (or the Dashboard API) will move it to approved or rejected. Treat it as "decision pending," not "done."

Match statuses with exact, lowercase string comparison; they are stable identifiers, not display copy.

Decisions ​

Two decision fields sit alongside the status:

  • auto_decision is the decision engine's output: approved, requires_review, or rejected (or null before processing).
  • final_decision is the decision of record. It equals auto_decision unless a human review overrode it, and is null while a session is still in requires_review.

Always key your business logic on final_decision (falling back to the terminal status). auto_decision is informational, useful for analytics on how often the engine defers to review.

Decision reasons ​

decision_reason explains why a session reached its decision. It's a stable enum you can branch on (the message may change; the key won't).

Groupdecision_reason
ApprovedAUTO_APPROVED, MANUAL_APPROVAL
DocumentLOW_DOCUMENT_QUALITY, OCR_LOW_CONFIDENCE, DOCUMENT_EXPIRED
LivenessLIVENESS_FAILED, LIVENESS_LOW_CONFIDENCE
Face matchFACE_MATCH_FAILED, FACE_MATCH_LOW_CONFIDENCE, MULTIPLE_FACES_DETECTED
AddressADDRESS_VERIFICATION_FAILED, ADDRESS_LOW_CONFIDENCE
Manual / flowMANUAL_REJECTION, RETRY_REQUESTED

The *_LOW_CONFIDENCE reasons route a session to requires_review rather than an outright rejected; the signal was ambiguous, not failing. RETRY_REQUESTED means a reviewer asked the user to redo a step.

Reading a result ​

Retrieve a session any time with the SDK (or GET /api/v1/sessions/{id}):

ts
const session = await arkyc.sessions.retrieve(sessionId)
// session.status, session.final_decision, session.decision_reason, session.risk_score, …

Fields most integrations use:

FieldTypeNotes
statusVerificationStatusLifecycle state (table above).
auto_decisionVerificationDecision | nullDecision engine output.
final_decisionVerificationDecision | nullDecision of record; branch on this.
decision_reasonDecisionReason | nullWhy (enum above).
risk_scorenumber | nullAggregate risk in [0, 1]; higher is riskier.
namestring | nullName extracted from the document (OCR), when available.
user_referencestring | nullYour id for the user, echoed back for correlation.
completed_atISO datetime | nullWhen the session reached a terminal decision.
expires_atISO datetimeWhen an unfinished session expires.

Per-check detail is delivered on the webhook payload under checks: document quality/OCR, liveness score, and face-match similarity.

The extracted identity and address data itself (name, date of birth, document number, address) is separate and more restricted: it is read only through the server SDK's retrieve() (secret key), never sent to the widget or webhooks, and only when the project holds a granted PII entitlement. See Extracted PII.

Handling each outcome ​

OutcomeWhat to do
final_decision: approvedGrant access; persist the verification against your user.
final_decision: rejectedDeny; optionally offer a fresh session if the reason is correctable (e.g. DOCUMENT_EXPIRED).
status: requires_reviewWait, and don't grant access. A reviewer settles it; you'll get a follow-up webhook.
status: expired / cancelledNothing verified. Create a new session and re-prompt the user.

The widget's onComplete is a UX signal only; use it to advance your UI. Treat the webhook (or a server-side retrieve) as your source of truth, since the browser can close before the decision settles.

Asset URLs ​

The captured images are exposed as signed, time-limited links under assets on the session and on webhook deliveries, one key per image that exists:

KeyImage
assets.document_frontFront of the document.
assets.document_backBack of the document.
assets.selfieThe liveness selfie.

Each link is a public, HMAC-signed URL (no API key), so it's safe to hand to a third party, for example a capture-only flow forwarding the image to your own KYC provider. It carries its own expiry:

  • Default lifetime is 30 minutes. A platform admin can change it under Platform settings → Assets (bounded to 60 seconds to 24 hours). The value applies to newly issued links; a link already in hand keeps its original window.
  • After it expires the link returns HTTP 403 (Invalid or expired asset link). The image itself is retained in storage; only the link lapses.
  • Fetch a fresh link by reading the session again. URLs are minted on every read, never stored, so a new retrieve (or the next webhook) always carries links with a fresh window.

Treat the assets URLs as short-lived and fetch-on-demand: don't persist them. If you need an image after the window closes, download the bytes promptly while the link is valid, or re-retrieve the session for a new one.

Session expiry ​

A session is valid for 15 minutes from creation, and its client token expires with it. If the user doesn't finish in time, the session lazily transitions to expired the next time it's touched (a widget call or a retrieve), and any further submissions are rejected. The window is fixed; to retry an expired or abandoned user, create a new session and hand the fresh token to the widget.

Idempotent creation ​

sessions.create is not idempotent: each call opens a new session with its own token. To avoid stacking duplicate sessions for one user (a double form submit, a retried request, a re-render), dedupe on your side. Store the returned session.id keyed by your user_reference, and reuse a session that's still pending/in progress instead of creating another; only start a fresh one once the previous is terminal or expired.

Capture-only ​

If your workflow runs with OCR/decisioning off (capture-only), a session still moves through the capture statuses and completes, but the decision is left to you: pull the captured document and selfie artifacts (signed URLs on the session) and forward them to your own KYC provider. See the server SDK.

Released under the MIT License.