Core concepts

Verifications

A verification represents one identity check for one end user. You create it, provide a document and selfie, and OKIAS returns a decision with a risk score and reason codes.

The lifecycle is simple: createsubmit assets (or use the hosted flow) → OKIAS scores the signals → you read a final status.

Create a verification

POST/v1/verifications

Creating a verification places a hold on one credit (in live mode) and returns a hosted_url you can redirect the user to. Always send an Idempotency-Key header.

curl https://api.okias.io/v1/verifications \
  -H "Authorization: Bearer $OKIAS_API_KEY" \
  -H "Idempotency-Key: 9f2c4a1b-1e33-4c07-9c1e-2b7f0d5a1a10" \
  -H "Content-Type: application/json" \
  -d '{
    "level": ["FULL_KYC"],
    "country": "pk",
    "doc_type": "NATIONAL_ID",
    "end_user_ref": "user_8842",
    "applicant": {
      "expected_first_name": "Ayesha",
      "expected_last_name": "Khan",
      "expected_dob": "1994-03-22"
    }
  }'

Request body

FieldTypeDescription
levelstring[]The checks to run, e.g. ["FULL_KYC"]. The catalog has exactly three values: FULL_KYC, ID_DOCUMENT, AML_SCREENING (see below). Any other value is rejected with 400 VALIDATION_ERROR (UNKNOWN_CHECK:<value>). Optional — defaults to the key's configured checks.
countrystring (ISO-3166 alpha-2)Two-letter country of the user's document, e.g. pk. Optional; improves document routing and checksum validation.
doc_typeenumOne of PASSPORT, NATIONAL_ID, DRIVING_LICENCE, RESIDENCE_PERMIT. Optional — inferred from the document if omitted.
end_user_refstringYour own reference for the user (max 120 chars). Echoed back so you can reconcile the decision to your records. Optional.
applicantobjectThe identity you already hold for this person, cross-checked against the document.expected_first_name, expected_last_name, expected_dob — all optional; a field you omit is skipped rather than counted as a mismatch. Recommended.
assetsAsset[]Optionally include the document/selfie assets at creation to run in one call. Otherwise submit them separately (below) or via the hosted flow.
Send the expected identity from your side

If you already know who this person claims to be — from your own signup — pass it in applicant. We compare it to what we read off the document and flag a disagreement.

It belongs on your call rather than in the applicant’s form for two reasons. The applicant never sees it, so they cannot make it agree with whatever document they are holding — which is what makes it evidence at all. And it removes a form from the flow: the person just photographs their ID, and we read the name and date of birth off it.

The response is 201 Created with the verification as a flat JSON object — there is no data envelope. status starts at PENDING:

201 Created
{
  "id": "cms80k9ix001moea2jxy64fgu",
  "status": "PENDING",
  "country": "pk",
  "doc_type": "NATIONAL_ID",
  "end_user_ref": "user_8842",
  "cost_credits": 1,
  "mode": "LIVE",
  "risk_score": null,
  "hosted_url": "https://okias.io/verify/cms80k9ix001moea2jxy64fgu",
  "liveness_challenge": { "actions": ["LOOK_DOWN", "TURN_RIGHT", "LOOK_UP", "TURN_LEFT"] },
  "reason_codes": [],
  "created_at": "2026-07-30T10:00:00.054Z",
  "completed_at": null
}

Levels & checks

level is an array of check identifiers. The purchasable catalog is exactly three checks — anything else returns 400 VALIDATION_ERROR with UNKNOWN_CHECK:<value>. Your API key's scope determines which of them it may request; see key scopes.

CheckWhat it runsCredits
FULL_KYCThe complete pipeline: document authenticity and registry validation, active liveness, 1:1 face match, and sanctions screening.1
ID_DOCUMENTDocument-only: classification, OCR/MRZ extraction, registry checks and tamper detection.1
AML_SCREENINGSanctions/watchlist screening of the name read from the document.1

Requesting several checks in one call sums their credits — ["ID_DOCUMENT", "AML_SCREENING"] returns cost_credits: 2. In live mode that total is the size of the hold, and it is still charged only if the verification resolves APPROVED.

KYB, proof of address, bank, phone and email checks are not available
KYB_BUSINESS, PROOF_OF_ADDRESS, BANK_STATEMENT, PHONE_VERIFICATION and EMAIL_VERIFICATION are not built and are no longer in the catalog — sending any of them returns 400 VALIDATION_ERROR / UNKNOWN_CHECK. If you integrated against an older version of this page, drop them from your level array.
One credit per approved full KYC
A full-KYC verification costs one credit ($0.15). In live mode the credit is held when you create the verification and consumed only on APPROVED. DECLINED and ERROR release the hold — they are free. A REVIEW keeps the hold until a human reviewer resolves the case, and is charged only if it resolves to approved. Sandbox (ok_test_) never holds or charges.

The liveness challenge

When you create a verification without inline assets (the hosted / deferred-capture flow), the response includes a liveness_challenge object:

fragment of the create response
"liveness_challenge": { "actions": ["LOOK_DOWN", "TURN_RIGHT", "LOOK_UP", "TURN_LEFT"] }
  • Server-generated and random. Each challenge is a random ordering of the four head-turn actions (TURN_LEFT, TURN_RIGHT, LOOK_UP, LOOK_DOWN), generated server-side per verification. The random order is the anti-replay signal — a pre-recorded video cannot match it.
  • The actions must be captured in order. The client performs the actions live, in exactly the sequence given, while liveness frames are captured. The hosted flow does all of this for you.
  • Single-use, time-limited. The challenge is consumed when the pipeline scores the capture (it cannot be replayed) and expires 30 minutes after issuance. liveness_challenge reads as null once consumed or expired.
  • Pure-API creates skip it. If you include assets inline in the create call there is no interactive capture, so no challenge is issued and liveness is evaluated passively only (which caps how strong the liveness signal can be — borderline cases lean to REVIEW).
Uploading your own capture with an issued challenge
If a challenge was issued (deferred capture) but the submitted frames do not match it, the verification cannot pass on liveness and will resolve REVIEW at best. Run active liveness through the hosted flow; for a fully API-driven integration, send your assets inline in the create call instead.

Submit assets

POST/v1/verifications/:id/submit

If you run your own capture UI, submit the document and selfie as base64 (no data: prefix). Each asset has a kind, an optional mime_type and the data.

curl
curl https://api.okias.io/v1/verifications/cms80k9ix001moea2jxy64fgu/submit \
  -H "Authorization: Bearer $OKIAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "assets": [
      { "kind": "DOCUMENT_FRONT", "mime_type": "image/jpeg", "data": "<base64>" },
      { "kind": "DOCUMENT_BACK",  "mime_type": "image/jpeg", "data": "<base64>" },
      { "kind": "SELFIE",         "mime_type": "image/jpeg", "data": "<base64>" }
    ]
  }'

Asset kinds

kindDescription
DOCUMENT_FRONTFront of the ID document. Required.
DOCUMENT_BACKBack of the document, where applicable (e.g. national ID cards).
SELFIEA selfie for 1:1 face match against the document portrait.
LIVENESS_FRAMEFrame(s) captured during liveness. Supplied automatically by the hosted flow.
Prefer the hosted flow for capture
The hosted flow handles capture quality, document guidance and active liveness for you — no need to build a camera pipeline or submit assets yourself.

Statuses

A verification's status moves from pending to one of four terminal outcomes:

statusMeaningWhat to do
PENDINGCreated, awaiting assets / still processing.Wait for the webhook or poll GET.
APPROVEDChecks passed. Identity verified.Provision the user.
REVIEWBorderline signals routed to manual review.Hold the user; a final event follows.
DECLINEDChecks failed (e.g. face mismatch, tampered document).Block or retry per your policy.
ERRORProcessing could not complete (e.g. unreadable capture).Ask the user to retry; the credit hold is released.

In lowercase, webhook events map to these outcomes: verification.approved, verification.review, verification.declined and verification.error. See Webhooks.

Risk score & reason codes

Every resolved verification carries a risk_score and a reason_codes array explaining the decision.

  • risk_score is 0–100, and higher is worse. 0 means no risk detected; 100 is maximum risk. It is the weighted risk across the stages that ran, rounded to an integer.
  • UI band guidance: treat < 20 as low risk, 20–59 as elevated, and ≥ 60 as high risk.
  • APPROVED results carry a low score and an empty reason_codes array — approved verifications never carry reason codes. A declined one carries the specific signals that failed alongside a high score.
declined example (abridged)
{
  "id": "cms81hq0p0031oea2vv17b1om",
  "status": "DECLINED",
  "risk_score": 71,
  "reason_codes": ["FACE_MISMATCH"],
  "completed_at": "2026-07-30T10:04:11.900Z"
}

Common reason codes

Reason codeMeaning
FACE_MISMATCHThe selfie did not match the document portrait.
COUNTRY_NOT_SUPPORTEDThe document's country is not eligible for verification.
DOC_NUMBER_INVALIDThe document number failed format or checksum validation.
DOCUMENT_TAMPERINGStrong evidence of document manipulation (see TAMPER_* signals).
LIVENESS_SPOOFThe liveness check detected a presentation attack (also emitted as LIVENESS_SPOOF_<TYPE>).
SANCTIONS_POSSIBLE_MATCHA possible sanctions/watchlist match — routed to human review, never auto-declined.
PROMPT_INJECTION_SUSPECTEDInstruction-like text aimed at the verification model was found in the submitted media; the affected stage is not trusted and the case goes to human review.
RISK_SCORE_HIGHThe aggregate weighted risk crossed the decline threshold.
Reason codes are additive
A single decision can carry multiple reason codes. Treat them as diagnostic signals for your own risk policy and support tooling. The complete reference — every code the engine emits, with the recommended integrator action — is in Errors & rate limits → Reason codes.

Retrieve a verification

GET/v1/verifications/:id

Fetch the current state and decision at any time. On top of the fields returned by create, GET includes four owner-only detail fields. All four are null until processing completes (and stay null where no data was captured):

FieldDescription
extractedIdentity fields read from the document (e.g. fullName, documentNumber, dateOfBirth, dateOfExpiry).
imagesThe captured document/selfie images as data: URLs (front, back, selfie); subject to media retention.
declaredWhat the applicant typed in the hosted flow (firstName, lastName, dateOfBirth, dateOfExpiry, sex).
matchDeclared-vs-document cross-match verdict: status (MATCH / PARTIAL / MISMATCH) plus per-field verdicts.
200 OK
{
  "id": "cms80k9ix001moea2jxy64fgu",
  "status": "APPROVED",
  "country": "pk",
  "doc_type": "NATIONAL_ID",
  "end_user_ref": "user_8842",
  "cost_credits": 1,
  "mode": "LIVE",
  "risk_score": 8,
  "hosted_url": "https://okias.io/verify/cms80k9ix001moea2jxy64fgu",
  "liveness_challenge": null,
  "reason_codes": [],
  "created_at": "2026-07-30T10:00:00.054Z",
  "completed_at": "2026-07-30T10:03:41.120Z",
  "extracted": { "fullName": "OKIAS SAMPLE USER", "documentNumber": "AB1234567", "dateOfBirth": "1990-01-01", "dateOfExpiry": "2030-01-01" },
  "images": { "front": "data:image/jpeg;base64,...", "back": null, "selfie": "data:image/jpeg;base64,..." },
  "declared": { "firstName": "Okias", "lastName": "Sample User", "dateOfBirth": "1990-01-01" },
  "match": { "status": "MATCH", "fields": { "name": "MATCH", "dateOfBirth": "MATCH", "dateOfExpiry": "SKIP", "sex": "SKIP" }, "reasons": [] }
}

Idempotency

Send a unique Idempotency-Key header on every create (supported on POST /v1/verifications and the partner variant POST /v1/p/:code/verifications). If a network hiccup makes you retry, OKIAS returns the original verification instead of creating a second one — so retries never double-charge or duplicate.

  • The header is optional — but omitting it makes every call chargeable. Without an Idempotency-Key, each request creates a brand-new verification (and, in live mode, a new credit hold).
  • 24-hour replay window. Repeating the same key within 24 hours returns the same verification object — never a second charge.
  • Concurrent duplicates can return 409. If two requests race on the same key and the first has not finished, the duplicate may fail with 409 IDEMPOTENCY_IN_PROGRESS — retry it and you will get the original verification.
  • Use a UUID or any unique string per logical operation (e.g. per user signup attempt).
  • Reuse the same key when retrying the same request; use a new key for a genuinely new verification.

Next steps