Skip to main content
Business verification

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.

Rollout contract and sale state
This page specifies the versioned KYB web/API contract. Endpoint availability is account- and rollout-controlled; an unavailable route fails closed rather than silently using fixture rules. All public KYB package entries remain 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
CallerCredentialRequired permission
Versioned server API at /kyb/v2Bearer ok_test_… / ok_live_…Active API key; existing account, project, IP/origin and rate controls apply
Signed-in merchant consoleShort-lived account JWT; refresh rotates the sessionAccount owner or an explicitly authorised team role; API keys never impersonate a dashboard JWT
Admin manifests and entity mappingsStaff JWTrules.manage
Review, publish and retireStaff JWTrules.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

  1. 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.
  2. Quote the identical scope. Render modules, sale state, package total, stakeholder inclusions/exclusions and additional charges.
  3. 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.
  4. Explicitly confirm. No billable work starts at case creation. A confirmed response contains reservation/billing state.
  5. Collect evidence and stakeholders. Upload the dynamic required and conditional categories; invite required people to the existing KYC workflow.
  6. Submit and poll or receive webhooks. Read module status, risk assessments, review actions, decisions and events from the case detail.
  7. 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

GET/v1/kyb/v2/requirements?country=GB&entity_type_code=PRIVATE_LIMITED&level=STANDARD&risk_level=LOW
requirements.sh
curl --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"
200 OK
{
  "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
FieldMeaning
countryISO 3166-1 alpha-2 incorporation country, selected explicitly by the merchant.
entity_type / level / risk_levelComplete scope key. Changing any one requires a fresh requirements fetch and quote.
rule_version / effective_fromVersion actually used. Cache by the full scope and version, not by country alone.
visitor_country_suggestionOptional { country, source: "VISITOR_IP" }. Display-only suggestion; never auto-select or submit it without user action.
documents[].requirementREQUIRED, CONDITIONAL or OPTIONAL. Conditional entries include their trigger in condition.
official_local_nameOfficial local document name to show alongside the English category name and stable category code.
formats / max_bytes / expiryAccepted MIME types, decoded size limit, document-age and expiry instructions.
translation / manual_reviewLanguage and certified-translation rule, plus when a person must review.
source / last_verifiedAuthority or source used for the rule and its verification date. This is not a claim that the registry itself was queried.
checks[].availabilityIMPLEMENTED, CONDITIONAL or NOT_AVAILABLE, with the actual provider boundary.
Unavailable checks stay unavailable
A submitted registry extract is not a live registry verification. Registry reconciliation requires an actual successful provider result. PEP screening, adverse-media screening and ongoing monitoring are not implied by any package.

2. Quote the exact scope

POST/v1/kyb/v2/quotes
quote.sh
curl 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"
  }'
200 OK — non-saleable example
{
  "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
Response fieldRule
id / version / scopeSend all three back at creation and confirmation. A scope mismatch invalidates the quote.
sale_statusACTIVE, NOT_FOR_SALE or PENDING_OWNER. Live commercial confirmation requires ACTIVE; a sandbox quote uses simulation regardless of the owner sale block.
execution_mode / policySIMULATED 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_usdAuthoritative 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_chargesPer-person inclusion status, included/excluded roles and plain-language additional-charge rule.
valid_until / legacyExpiry and compatibility marker. Do not treat an unversioned legacy estimate as a versioned scope quote.

3. Create and explicitly confirm

POST/v1/kyb/v2/cases
create-case.sh
curl 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
Request fieldRequired / meaning
legal_nameRequired legal entity name.
registration_numberOptional registry identifier; not a registry verification request by itself.
country / entity_type_code / template_code / risk_levelRequired full scope, identical to the quote. Quote scope uses the display names entity_type and level.
quote_id / quote_versionRequired for the versioned lifecycle. Reject stale or changed quotes.
project_idWhen 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

POST/v1/kyb/v2/cases/:id/documents
POST/v1/kyb/v2/cases/:id/parties
POST/v1/kyb/v2/cases/:id/parties/:partyId/invite
upload-document.sh
curl 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

POST/v1/kyb/v2/cases/:id/submit
GET/v1/kyb/v2/cases/:id
GET/v1/kyb/v2/cases?page=1&page_size=25&status=HUMAN_REVIEW
Case response field, Contents
Case response fieldContents
status / public_status / public_labelInternal lifecycle and safe customer-facing projection.
company / billingCompany 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.
ownershipThreshold, 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

POST/v1/kyb/v2/cases/:id/actions/:actionId/resubmit

Upload 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

upgrade.sh
# 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
Operation classPer credential / accountOn exhaustion
Requirements, template, case and result reads60 requests / minute429 RATE_LIMITED + Retry-After
Quotes and case mutations30 requests / minute429 RATE_LIMITED + Retry-After
Document uploads and stakeholder invitations10 requests / minute429 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
HTTP / codeHandling
400 VALIDATION_ERRORFix request fields. Do not retry unchanged.
400 KYB_COUNTRY_NOT_AVAILABLE / KYB_ENTITY_NOT_AVAILABLEChoose an explicitly KYB-enabled country and persisted entity mapping; KYC publication is insufficient.
400 KYB_QUOTE_NOT_CONFIRMEDConfirm the exact quote before invitations or submission.
401 UNAUTHORIZEDCheck credential, rotation and environment. Do not retry as another account.
403 PERMISSION_DENIEDGrant the required staff permission or use an appropriately controlled API key.
404 KYB_CASE_NOT_FOUNDUnknown case or not owned by this account, project or API key.
409 IDEMPOTENCY_KEY_REUSEDThe key was used with different request details; use a new key only for a new operation.
409 KYB_QUOTE_MISMATCH / KYB_QUOTE_CHANGEDFetch requirements and a fresh quote for the current scope and execution mode.
409 KYB_NOT_FOR_SALELive commercial confirmation is blocked. Public comparison or manifest publication cannot enable it.
409 KYB_STAKEHOLDER_PRICING_REVIEW_REQUIREDPer-person pricing is unresolved; do not confirm or infer inclusion.
409 ACTION_REQUESTS_OPENResolve open actions before approval.
409 SCREENING_HITS_UNADJUDICATEDA reviewer must adjudicate potential matches.
409 KYB_CASE_TERMINALTerminal cases are immutable.
400 DOCUMENT_MIME_MISMATCHDeclared MIME and file bytes differ.
429 RATE_LIMITEDWait for Retry-After and back off.
503 PROVIDER_UNAVAILABLERetry 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.

PII-free default event
{
  "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
HeaderVerification rule
X-Okias-EventBranch on the actual event; acknowledge unknown future types.
X-Okias-DeliveryStable delivery id across retries; use it to de-duplicate.
X-Okias-Signaturet=<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 2xx within 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:

GET/v1/verifications/:id
authorized-ocr-fetch.sh
curl 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

  1. 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.
  2. 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.
  3. POST the same scope to /kyb/v2/quotes. Assert execution_mode=SIMULATED and KYB_SIMULATED_NO_CHARGE. NOT_FOR_SALE blocks live commercial execution, not an explicitly simulated test case.
  4. 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.
  5. 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.
  6. 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/resubmit with its document id.
  7. 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.
  8. Assert unavailable registry/PEP/adverse-media/monitoring capabilities never appear as successful. Exercise 429 handling in a controlled test and reconcile terminal webhooks with GET /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.