KYB API v1
Resolve dynamic requirements, quote an exact country/entity/level/risk scope, collect company and stakeholder evidence, and run the case through review, action resubmission and an accountable decision.
NOT_FOR_SALE until owner enablement. A requirement or scope comparison is not a purchase offer, a connected-provider guarantee or a verification result.Every request uses https://api.okias.io/v1. KYB rule versions and quote versions are separate: a rule version defines evidence and checks; a quote version binds that scope to its price and stakeholder-charge terms.
Authentication, permissions and lifecycle
| Caller | Credential | Required permission |
|---|---|---|
| Versioned server API at /kyb/v2 | Bearer ok_test_… / ok_live_… | Active API key; existing account, project, IP/origin and rate controls apply |
| Signed-in merchant console | Short-lived account JWT; refresh rotates the session | Account owner or an explicitly authorised team role; API keys never impersonate a dashboard JWT |
| Admin manifests and entity mappings | Staff JWT | rules.manage |
| Review, publish and retire | Staff JWT | rules.manage; persisted state transitions are audited |
Use separate sandbox and live keys. Rotate a key by creating its replacement, deploying it, verifying traffic, then revoking the old key. A revoked or suspended key fails immediately; a JWT refresh cannot revive it. Keep secret keys on your server. The browser console uses the existing JWT refresh flow and compatibility routes under /kyb; server API clients use the aligned /kyb/v2 routes documented here.
Resources are account- and project-scoped. A valid credential for another account receives the same not-found response as an unknown case. Admin publication changes rules only: it does not change catalogue prices or turn NOT_FOR_SALE into ACTIVE.
Versioned workflow
- Resolve requirements for ISO alpha-2 incorporation country, entity, level and declared risk scope. Visitor IP may be returned only as a suggestion; the client must never treat it as incorporation.
- Quote the identical scope. Render modules, sale state, package total, stakeholder inclusions/exclusions and additional charges.
- Create with quote id + version. The server rejects stale or mismatched quotes. A non-saleable live quote may remain a draft but cannot be confirmed or reserve credits.
- Explicitly confirm. No billable work starts at case creation. A confirmed response contains reservation/billing state.
- Collect evidence and stakeholders. Upload the dynamic required and conditional categories; invite required people to the existing KYC workflow.
- Submit and poll or receive webhooks. Read module status, risk assessments, review actions, decisions and events from the case detail.
- Resolve actions or quote a level change. Resubmission links new document ids to an action. Upgrades require old/new/additional-price confirmation.
1. Dynamic requirements
/v1/kyb/v2/requirements?country=GB&entity_type_code=PRIVATE_LIMITED&level=STANDARD&risk_level=LOWcurl --get https://api.okias.io/v1/kyb/v2/requirements \
-H "Authorization: Bearer ok_test_..." \
-G --data-urlencode "country=GB" \
--data-urlencode "entity_type_code=PRIVATE_LIMITED" \
--data-urlencode "level=STANDARD" \
--data-urlencode "risk_level=LOW"{
"country": "GB",
"entity_type": "PRIVATE_LIMITED",
"level": "STANDARD",
"risk_level": "LOW",
"rule_version": "gb-private-standard-v3",
"effective_from": "2026-11-04",
"visitor_country_suggestion": null,
"entities": [
{ "code": "PRIVATE_LIMITED", "name": "Private limited company", "official_local_name": "Private company limited by shares (Ltd)" }
],
"documents": [{
"code": "CERTIFICATE_OF_INCORPORATION",
"name": "Certificate of incorporation / registration",
"official_local_name": "Certificate of incorporation",
"requirement": "REQUIRED",
"condition": null,
"formats": ["application/pdf", "image/jpeg", "image/png", "image/webp"],
"max_bytes": 15728640,
"expiry": { "expires": false, "max_age_months": null, "instruction": "Must be current." },
"translation": "Certified translation required when not accepted in the original language.",
"manual_review": "Required when unreadable or inconsistent.",
"source": { "name": "Companies House", "url": null, "last_verified": "2026-11-04" }
}],
"checks": [{
"module_code": "REGISTRY_VERIFICATION",
"availability": "CONDITIONAL",
"provider": "Jurisdiction provider required",
"detail": "Runs only with a successful connected-provider result."
}]
}| Field | Meaning |
|---|---|
| country | ISO 3166-1 alpha-2 incorporation country, selected explicitly by the merchant. |
| entity_type / level / risk_level | Complete scope key. Changing any one requires a fresh requirements fetch and quote. |
| rule_version / effective_from | Version actually used. Cache by the full scope and version, not by country alone. |
| visitor_country_suggestion | Optional { country, source: "VISITOR_IP" }. Display-only suggestion; never auto-select or submit it without user action. |
| documents[].requirement | REQUIRED, CONDITIONAL or OPTIONAL. Conditional entries include their trigger in condition. |
| official_local_name | Official local document name to show alongside the English category name and stable category code. |
| formats / max_bytes / expiry | Accepted MIME types, decoded size limit, document-age and expiry instructions. |
| translation / manual_review | Language and certified-translation rule, plus when a person must review. |
| source / last_verified | Authority or source used for the rule and its verification date. This is not a claim that the registry itself was queried. |
| checks[].availability | IMPLEMENTED, CONDITIONAL or NOT_AVAILABLE, with the actual provider boundary. |
2. Quote the exact scope
/v1/kyb/v2/quotescurl https://api.okias.io/v1/kyb/v2/quotes \
-H "Authorization: Bearer ok_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"country": "GB",
"entity_type": "PRIVATE_LIMITED",
"level": "STANDARD",
"risk_level": "LOW"
}'{
"id": "<64-char-quote-fingerprint>",
"version": "v3-<requirement-version>",
"scope": { "country": "GB", "entity_type": "PRIVATE_LIMITED", "level": "STANDARD", "risk_level": "LOW" },
"sale_status": "NOT_FOR_SALE",
"total_credits": 300,
"total_usd": "3.00",
"modules": [{ "module_code": "KYB_STANDARD", "credits": 300 }],
"stakeholder_charges": {
"pricing_status": "PENDING_OWNER",
"included_roles": [],
"excluded_roles": ["UBO", "DIRECTOR"],
"description": "Per-person pricing requires owner resolution."
},
"valid_until": null,
"legacy": false,
"execution_mode": "SIMULATED",
"policy": { "code": "KYB_SIMULATED_NO_CHARGE", "version": "1" }
}| Response field | Rule |
|---|---|
| id / version / scope | Send all three back at creation and confirmation. A scope mismatch invalidates the quote. |
| sale_status | ACTIVE, NOT_FOR_SALE or PENDING_OWNER. Live commercial confirmation requires ACTIVE; a sandbox quote uses simulation regardless of the owner sale block. |
| execution_mode / policy | SIMULATED with KYB_SIMULATED_NO_CHARGE for test keys, or COMMERCIAL with KYB_APPROVED_ONLY for live keys. Never substitute one for the other. |
| total_credits / total_usd | Authoritative configured quote values. Never infer a price from a public comparison or an old package. |
| modules[] | Itemised company, provider, manual and configured stakeholder price lines. |
| stakeholder_charges | Per-person inclusion status, included/excluded roles and plain-language additional-charge rule. |
| valid_until / legacy | Expiry and compatibility marker. Do not treat an unversioned legacy estimate as a versioned scope quote. |
3. Create and explicitly confirm
/v1/kyb/v2/casescurl https://api.okias.io/v1/kyb/v2/cases \
-H "Authorization: Bearer ok_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"legal_name": "Example Trading Ltd",
"registration_number": "TEST-REG-001",
"country": "GB",
"entity_type_code": "PRIVATE_LIMITED",
"template_code": "STANDARD",
"risk_level": "LOW",
"quote_id": "<64-char-quote-fingerprint>",
"quote_version": "v3-<requirement-version>"
}'| Request field | Required / meaning |
|---|---|
| legal_name | Required legal entity name. |
| registration_number | Optional registry identifier; not a registry verification request by itself. |
| country / entity_type_code / template_code / risk_level | Required full scope, identical to the quote. Quote scope uses the display names entity_type and level. |
| quote_id / quote_version | Required for the versioned lifecycle. Reject stale or changed quotes. |
| project_id | When supplied, it must equal the API key's bound project. JWT versioned cases use the account's active default project. |
The response is the full case described below, initially awaiting confirmation. API-key clients confirm with POST /v1/kyb/v2/cases/:id/quote/accept and body { "quote_id": "...", "policy_version": "1", "accepted": true }. The JWT compatibility route is POST /v1/kyb/cases/:id/confirm with quote_id and quote_version. Billing fields are credits_quoted, credits_reserved and confirmed_at. Rejection creates no reservation; simulation always reports zero reserved credits.
4. Evidence and stakeholders
/v1/kyb/v2/cases/:id/documents/v1/kyb/v2/cases/:id/parties/v1/kyb/v2/cases/:id/parties/:partyId/invitecurl https://api.okias.io/v1/kyb/v2/cases/kybc_.../documents \
-H "Authorization: Bearer ok_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"category_code": "CERTIFICATE_OF_INCORPORATION",
"mime": "application/pdf",
"data_base64": "<base64-without-data-url-prefix>",
"issuer_country": "GB",
"issuer": "Companies House",
"issue_date": "2026-01-15"
}'Document input fields are category_code, mime, base64 data_base64, optional party_id, issuer_country, issuer, document_number, issue_date, expiry_date and supersedes_id. Accepted MIME values are PDF, JPEG, PNG and WebP. The server verifies declared MIME against file bytes; the response returns id, category_code, status, mime, size_bytes, sha256 and uploaded_at. Superseding preserves history.
A person party accepts full_name, roles, optional email, ownership_pct, voting_pct, nationality/residence and kyc_required. A company party uses company_name, registration and country fields. Invite responses contain invitation state, never a reusable invite secret in a webhook.
5. Submit and read status/results/history
/v1/kyb/v2/cases/:id/submit/v1/kyb/v2/cases/:id/v1/kyb/v2/cases?page=1&page_size=25&status=HUMAN_REVIEW| Case response field | Contents |
|---|---|
| status / public_status / public_label | Internal lifecycle and safe customer-facing projection. |
| company / billing | Company identity and quoted/reserved/confirmed billing state. |
| modules[] | module_code, status, credits_quoted, start/completion timestamps and optional provider detail. NOT_AVAILABLE never means passed. |
| documents[] / parties[] | Evidence versions and stakeholder KYC/screening states. |
| ownership | Threshold, holdings, identified UBOs and graph issues from declared evidence. |
| risk_assessments[] | Score, level, signals, model version and creation time. |
| action_requests[] | Action id, item type, reason, instructions, open/resolved state and timestamps. |
| review_decisions[] / events[] | Decision round/reason and append-only status/activity history. |
6. Action resubmission
/v1/kyb/v2/cases/:id/actions/:actionId/resubmitUpload replacement evidence first, then send { "document_ids": ["kybd_..."] }. Only documents belonging to the same case are accepted. The response is the refreshed case. If upload succeeds but resubmission fails, retain the document id and retry the action call; do not report the action as resolved.
7. Explicit level upgrade or scope change
# 1. Quote the change; nothing changes yet.
curl https://api.okias.io/v1/kyb/v2/cases/kybc_.../upgrade-quote \
-H "Authorization: Bearer ok_test_..." \
-H "Content-Type: application/json" \
-d '{ "target_level": "ENHANCED", "previous_quote_id": "<64-char-old-quote>", "previous_quote_version": "v3-<old-requirement-version>" }'
# Response includes previous + new quote and additional_credits/additional_usd.
# 2. Confirm only after displaying those exact values to the merchant.
curl https://api.okias.io/v1/kyb/v2/cases/kybc_.../upgrade/confirm \
-H "Authorization: Bearer ok_test_..." \
-H "Content-Type: application/json" \
-d '{
"quote_id": "<64-char-new-quote>",
"quote_version": "v4-<new-requirement-version>",
"previous_quote_id": "<64-char-old-quote>",
"previous_quote_version": "v3-<old-requirement-version>",
"additional_credits": 225,
"explicit_confirmation": true
}'The upgrade quote includes previous.level, previous quote id/version/total, new scope, new quote id/version/total, additional_credits, additional_usd and stakeholder-charge terms. The merchant must explicitly confirm those exact values. A negative difference, changed previous quote or non-active live sale state is rejected; never silently charge the new package total or downgrade evidence. Sandbox upgrades remain simulated and create no hold adjustment.
Idempotency and safe retries
Send a unique Idempotency-Key on case creation. Reusing the same credential, key and byte-equivalent create request returns the original case without a duplicate. Reusing the key for different request details returns 409 IDEMPOTENCY_KEY_REUSED. Other mutations use exact quote, policy, action and document identifiers plus explicit confirmation; do not imply that a generic idempotency header is persisted for them.
A network timeout is not proof of failure. Retry with the same key, then fetch the case. Use document and action ids from the authoritative response rather than constructing client-side success state.
Rate limits
| Operation class | Per credential / account | On exhaustion |
|---|---|---|
| Requirements, template, case and result reads | 60 requests / minute | 429 RATE_LIMITED + Retry-After |
| Quotes and case mutations | 30 requests / minute | 429 RATE_LIMITED + Retry-After |
| Document uploads and stakeholder invitations | 10 requests / minute | 429 RATE_LIMITED + Retry-After |
Limits protect each account and may be lowered by platform policy. Cache requirement responses by full scope + rule version, debounce selectors, and cancel superseded browser reads. Honour Retry-After with exponential backoff and jitter; never bypass a 429 by rotating keys.
Errors
| HTTP / code | Handling |
|---|---|
| 400 VALIDATION_ERROR | Fix request fields. Do not retry unchanged. |
| 400 KYB_COUNTRY_NOT_AVAILABLE / KYB_ENTITY_NOT_AVAILABLE | Choose an explicitly KYB-enabled country and persisted entity mapping; KYC publication is insufficient. |
| 400 KYB_QUOTE_NOT_CONFIRMED | Confirm the exact quote before invitations or submission. |
| 401 UNAUTHORIZED | Check credential, rotation and environment. Do not retry as another account. |
| 403 PERMISSION_DENIED | Grant the required staff permission or use an appropriately controlled API key. |
| 404 KYB_CASE_NOT_FOUND | Unknown case or not owned by this account, project or API key. |
| 409 IDEMPOTENCY_KEY_REUSED | The key was used with different request details; use a new key only for a new operation. |
| 409 KYB_QUOTE_MISMATCH / KYB_QUOTE_CHANGED | Fetch requirements and a fresh quote for the current scope and execution mode. |
| 409 KYB_NOT_FOR_SALE | Live commercial confirmation is blocked. Public comparison or manifest publication cannot enable it. |
| 409 KYB_STAKEHOLDER_PRICING_REVIEW_REQUIRED | Per-person pricing is unresolved; do not confirm or infer inclusion. |
| 409 ACTION_REQUESTS_OPEN | Resolve open actions before approval. |
| 409 SCREENING_HITS_UNADJUDICATED | A reviewer must adjudicate potential matches. |
| 409 KYB_CASE_TERMINAL | Terminal cases are immutable. |
| 400 DOCUMENT_MIME_MISMATCH | Declared MIME and file bytes differ. |
| 429 RATE_LIMITED | Wait for Retry-After and back off. |
| 503 PROVIDER_UNAVAILABLE | Retry later with the same idempotency key; no successful provider result is claimed. |
Errors use { "error": { "code": "...", "message": "..." } }. Structured fields such as unresolved counts or module codes may accompany the code.
KYB webhooks
Meaningful lifecycle events include kyb.created, kyb.updated, kyb.document_uploaded, kyb.quote_accepted, kyb.submitted, kyb.screening_completed, kyb.risk_completed, kyb.review_action_required, kyb.action_responded, kyb.approved and kyb.rejected. Endpoint selection support depends on the shared webhook catalogue; endpoints subscribed to all events receive newly added types.
{
"id": "delivery_...",
"event": "kyb.review_action_required",
"created": 1785405600,
"data": {
"case_id": "kybc_...",
"reference_code": "3J7Q9WD2M4XT",
"status": "ADDITIONAL_INFO_REQUIRED",
"action_request_id": "kyba_...",
"livemode": false
}
}| Header | Verification rule |
|---|---|
| X-Okias-Event | Branch on the actual event; acknowledge unknown future types. |
| X-Okias-Delivery | Stable delivery id across retries; use it to de-duplicate. |
| X-Okias-Signature | t=<unix-seconds>,v1=<hex-hmac-sha256> |
- Compute HMAC-SHA256 with the endpoint secret over
{timestamp}.{raw-body}. Use exact received bytes and constant-time comparison. - Reject stale timestamps (five minutes is the usual tolerance). Every retry receives a fresh signature timestamp.
- Return any
2xxwithin eight seconds. Non-2xx, timeout and connection failure retry up to 34 attempts over roughly 24 hours: exponential backoff through 256 seconds, then hourly. - Persist delivery ids and make processing idempotent. Fetch the case before consequential action; webhooks are the fast path, not the source of truth.
Default KYB payloads carry identifiers, public status, risk/result summaries and livemode only. They do not include legal names, stakeholder names/email, document bytes, registry payloads or OCR identity values. Action instructions must also be written without personal data. See Webhooks for copy-paste Node/Python verification.
Authorized OCR retrieval
When a KYB stakeholder has a verification_id, OCR values are retrieved only through the authorized verification detail request with the owning account's API key or permitted JWT:
/v1/verifications/:idcurl https://api.okias.io/v1/verifications/ver_... \
-H "Authorization: Bearer ok_test_..."
# Authorized owner-only fields on the detail response:
# extracted legacy OCR identity values
# extracted_fields side-aware V2 OCR identity values
# These fields are deliberately absent from list, public and default webhook responses.extracted and, for V2 selections, extracted_fields are owner-only detail fields. They are absent from list/public responses and from the default KYB webhook. Store the verification id from the party, fetch only when necessary, restrict retention and never put OCR values into action instructions or webhook metadata.
Complete sandbox test workflow
- Create an
ok_test_…key and register a sandbox webhook. Publish test manifests and activate test templates/country/entity mappings first; unconfigured scopes fail closed. - Fetch GB/private-limited/Standard/low requirements from
/kyb/v2/requirements. Assert the full scope and rule version and inspect required and conditional document categories. - POST the same scope to
/kyb/v2/quotes. Assertexecution_mode=SIMULATEDandKYB_SIMULATED_NO_CHARGE.NOT_FOR_SALEblocks live commercial execution, not an explicitly simulated test case. - Create with the returned quote id/version and an idempotency key. Repeat the identical request and assert the same case id; repeat with a changed body and assert
IDEMPOTENCY_KEY_REUSED. - Accept the exact quote and policy, upload a small valid test PDF without real personal data, add only manifest-permitted synthetic stakeholders, and submit. Never use real identity documents in sandbox.
- To exercise action resubmission, have an authorised reviewer create an action request. Verify the signed action webhook, upload replacement evidence, then call
/kyb/v2/cases/:id/actions/:actionId/resubmitwith its document id. - Quote an Enhanced change and assert previous/new totals and additional price. Confirm only that exact quote. Check module results, risk assessment, action state, decisions and events; approve or reject through the authorised review path.
- Assert unavailable registry/PEP/adverse-media/monitoring capabilities never appear as successful. Exercise
429handling in a controlled test and reconcile terminal webhooks withGET /kyb/v2/cases/:id. Sandbox operations create no credit hold or live charge.
Admin versioned rules
Persisted manifests follow DRAFT → REVIEW → PUBLISHED, with RETIRED for withdrawal. Admin editors submit complete requirement-rule JSON containing the differentiated level, company/party/document rules, formats, expiry, translation, manual-review behavior, official HTTPS sources, last-verified date and effective dates. Published versions are immutable: create a new draft version instead. Country/entity mappings are managed separately at /admin/kyb/requirements/entities.
Manifest routes live under /admin/kyb/requirements and use staff JWT with rules.manage. Every transition is audited. Publication never changes catalogue prices or owner sale state; no independent-reviewer rule is claimed beyond the implemented state workflow.