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: create → submit assets (or use the hosted flow) → OKIAS scores the signals → you read a final status.
Create a verification
/v1/verificationsCreating 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
| Field | Type | Description |
|---|---|---|
| level | string[] | 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. |
| country | string (ISO-3166 alpha-2) | Two-letter country of the user's document, e.g. pk. Optional; improves document routing and checksum validation. |
| doc_type | enum | One of PASSPORT, NATIONAL_ID, DRIVING_LICENCE, RESIDENCE_PERMIT. Optional — inferred from the document if omitted. |
| end_user_ref | string | Your own reference for the user (max 120 chars). Echoed back so you can reconcile the decision to your records. Optional. |
| applicant | object | The 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. |
| assets | Asset[] | Optionally include the document/selfie assets at creation to run in one call. Otherwise submit them separately (below) or via the hosted flow. |
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:
{
"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.
| Check | What it runs | Credits |
|---|---|---|
| FULL_KYC | The complete pipeline: document authenticity and registry validation, active liveness, 1:1 face match, and sanctions screening. | 1 |
| ID_DOCUMENT | Document-only: classification, OCR/MRZ extraction, registry checks and tamper detection. | 1 |
| AML_SCREENING | Sanctions/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_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.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:
"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_challengereads asnullonce consumed or expired. - Pure-API creates skip it. If you include
assetsinline 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 toREVIEW).
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
/v1/verifications/:id/submitIf 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 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
| kind | Description |
|---|---|
| DOCUMENT_FRONT | Front of the ID document. Required. |
| DOCUMENT_BACK | Back of the document, where applicable (e.g. national ID cards). |
| SELFIE | A selfie for 1:1 face match against the document portrait. |
| LIVENESS_FRAME | Frame(s) captured during liveness. Supplied automatically by the hosted flow. |
Statuses
A verification's status moves from pending to one of four terminal outcomes:
| status | Meaning | What to do |
|---|---|---|
| PENDING | Created, awaiting assets / still processing. | Wait for the webhook or poll GET. |
| APPROVED | Checks passed. Identity verified. | Provision the user. |
| REVIEW | Borderline signals routed to manual review. | Hold the user; a final event follows. |
| DECLINED | Checks failed (e.g. face mismatch, tampered document). | Block or retry per your policy. |
| ERROR | Processing 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_scoreis 0–100, and higher is worse.0means no risk detected;100is maximum risk. It is the weighted risk across the stages that ran, rounded to an integer.- UI band guidance: treat
< 20as low risk,20–59as elevated, and≥ 60as high risk. APPROVEDresults carry a low score and an emptyreason_codesarray — approved verifications never carry reason codes. A declined one carries the specific signals that failed alongside a high score.
{
"id": "cms81hq0p0031oea2vv17b1om",
"status": "DECLINED",
"risk_score": 71,
"reason_codes": ["FACE_MISMATCH"],
"completed_at": "2026-07-30T10:04:11.900Z"
}Common reason codes
| Reason code | Meaning |
|---|---|
| FACE_MISMATCH | The selfie did not match the document portrait. |
| COUNTRY_NOT_SUPPORTED | The document's country is not eligible for verification. |
| DOC_NUMBER_INVALID | The document number failed format or checksum validation. |
| DOCUMENT_TAMPERING | Strong evidence of document manipulation (see TAMPER_* signals). |
| LIVENESS_SPOOF | The liveness check detected a presentation attack (also emitted as LIVENESS_SPOOF_<TYPE>). |
| SANCTIONS_POSSIBLE_MATCH | A possible sanctions/watchlist match — routed to human review, never auto-declined. |
| PROMPT_INJECTION_SUSPECTED | Instruction-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_HIGH | The aggregate weighted risk crossed the decline threshold. |
Retrieve a verification
/v1/verifications/:idFetch 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):
| Field | Description |
|---|---|
| extracted | Identity fields read from the document (e.g. fullName, documentNumber, dateOfBirth, dateOfExpiry). |
| images | The captured document/selfie images as data: URLs (front, back, selfie); subject to media retention. |
| declared | What the applicant typed in the hosted flow (firstName, lastName, dateOfBirth, dateOfExpiry, sex). |
| match | Declared-vs-document cross-match verdict: status (MATCH / PARTIAL / MISMATCH) plus per-field verdicts. |
{
"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 with409 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.