Core concepts

Hosted flow & Embed SDK

OKIAS ships a ready-made, mobile-first capture experience — document photos, guidance and active liveness included. Run it two ways: redirect the user to the hosted link, or embed the same flow on your own domain with one script tag.

Either way the server-side contract is identical: create a verification with your API key, get back an id and a hosted_url, and receive the decision on a signed webhook. No camera code, no liveness SDK, no upload pipeline.

Choose an integration

Option A — Redirect / linkOption B — Embed SDK
Where the user isOn okias.io (or an in-app webview)On your domain — the flow renders in an iframe on your page
Frontend workNone — send the user to hosted_urlOne script tag + one function call
Progress eventsLifecycle callbacks + window message events
Best forFastest path to production, email/SMS linksOnboarding funnels where users never leave your site

If you would rather run your own capture UI end to end and upload assets directly, use the API path in Verifications → Submit assets instead.

How it works

1
Create a verification from your server
A single authenticated POST /v1/verifications returns an id and a hosted_url. Your API key never touches the browser.
2
Hand the flow to the user
Option A: redirect to hosted_url. Option B: pass the id to OkiasKYC.render() (or OkiasKYC.open()) on your page.
3
OKIAS handles capture + liveness
The user photographs their document and completes an active liveness challenge — a randomised action sequence that cannot be pre-recorded. Capture quality is validated in real time, with guided retries.
4
Receive the decision
When scoring finishes you get a signed webhook (and the verification is readable via the API). Provision, review or block based on the outcome.

1 · Create the verification (server-side)

POST/v1/verifications
curl
curl https://api.okias.io/v1/verifications \
  -H "Authorization: Bearer $OKIAS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "level": ["FULL_KYC"], "country": "pk", "end_user_ref": "user_8842" }'

The response includes the verification id, the hosted_url and the liveness_challenge — the server-generated, single-use sequence of head-turn actions the hosted flow will guide the user through in order (see the liveness challenge):

201 Created
{
  "id": "cms80k9ix001moea2jxy64fgu",
  "status": "PENDING",
  "hosted_url": "https://okias.io/verify/cms80k9ix001moea2jxy64fgu",
  "liveness_challenge": { "actions": ["LOOK_DOWN", "TURN_RIGHT", "LOOK_UP", "TURN_LEFT"] },
  ...
}

Store the id against your user before handing off, so you can reconcile the incoming webhook later.

2A · Option A — Redirect to the hosted link

Send the user's browser to hosted_url (or open it in an in-app webview):

server.ts
// After creating the verification, send the user to the hosted flow.
const verification = await createVerification(user);

// Store verification.id against your user so you can reconcile the webhook.
await db.users.update(user.id, { okiasVerificationId: verification.id });

// Redirect the browser to the hosted capture experience.
res.redirect(verification.hosted_url);
The link is the credential
The hosted URL contains an unguessable verification id and needs no API key — it is safe to open in the user's browser, and works equally well delivered by email or SMS. Never expose your secret API key to the client.

2B · Option B — Embed on your own site

The embed SDK renders the same hosted flow inside an iframe on your page, so users never leave your domain. It is a dependency-free script (~4 KB) served from https://okias.io/embed/okias.js — no npm install, no build step.

Inline widget

Drop a container element on the page and call OkiasKYC.render() with the verification id from your server:

embed.html
<script src="https://okias.io/embed/okias.js"></script>
<div id="kyc"></div>

<script>
  OkiasKYC.render({
    container: '#kyc',
    id: 'cms80k9ix001moea2jxy64fgu',            // verification id from your server
    onReady:    () => {},            // widget loaded
    onStarted:  () => {},            // user tapped "Get started"
    onStep:     (e) => {},           // e.step: 'document' | 'details' | 'selfie' | 'review'
    onComplete: (r) => {},           // r.status: APPROVED | DECLINED | REVIEW | PENDING
    onError:    (e) => {},           // e.message
  });
</script>

Prefer a dialog over an inline section? OkiasKYC.open() takes the same options (minus container) and overlays the flow centred on the page with a close button:

modal
// Modal overlay instead of an inline widget — same options, plus onClose.
OkiasKYC.open({
  id: 'cms80k9ix001moea2jxy64fgu',
  onComplete: (r) => console.log(r.status),
  onClose:    () => console.log('widget closed'),
});

Options

OptionTypeDescription
containerstring | ElementSelector or element to render into. render() only.
idstringThe verification id returned by POST /v1/verifications. Required.
heightnumber | stringOptional fixed height for the inline widget. Omit it to let the widget auto-size (below).
dismissablebooleanopen() only. Set false to stop a backdrop click from closing the modal. Default true.

Both calls return a handle with a destroy() method — call it to remove the iframe and detach listeners, e.g. when your SPA navigates away:

cleanup
const widget = OkiasKYC.render({ container: '#kyc', id: 'cms80k9ix001moea2jxy64fgu' });

// Later — remove the iframe and detach listeners (e.g. on SPA route change):
widget.destroy();

Events & callbacks

The widget reports its lifecycle back to your page. Callbacks first:

CallbackFires when
onReady()The flow finished loading inside the iframe.
onStarted()The user tapped “Get started”.
onStep(e)The user reached a step — e.step is document, details, selfie or review.
onComplete(r)The flow finished — r.status is APPROVED, DECLINED, REVIEW or PENDING.
onError(e)Submission failed — e.message explains why.
onClose()The modal was closed (modal only).

Window message events

The same lifecycle also arrives as message events on window, tagged source: 'okias-kyc' — useful if you listen outside the SDK (analytics, frameworks with their own event plumbing):

message listener
window.addEventListener('message', (event) => {
  // 1 · Only trust messages from the OKIAS origin.
  if (event.origin !== 'https://okias.io') return;

  // 2 · Only handle widget messages.
  const msg = event.data;
  if (!msg || msg.source !== 'okias-kyc') return;

  // msg.type: 'ready' | 'started' | 'step' | 'completed' | 'error' | 'close' | 'resize'
  if (msg.type === 'completed') {
    console.log(msg.status); // APPROVED | DECLINED | REVIEW | PENDING
  }
});
typeMeaning
readyThe flow loaded.
startedThe user tapped “Get started”.
stepStep change — carries step.
completedTerminal result — carries status.
errorSubmission failed — carries message.
closeThe flow asked to be closed (modal dismisses itself).
resizeContent height changed — carries height.
Always check event.origin
Any page can post messages to your window. Ignore everything where event.origin !== 'https://okias.io' before reading the payload — the SDK already applies this check internally for its callbacks.

Auto-sizing

The inline embed sizes itself: as the user moves through the flow the widget posts resize {height} messages and the SDK adjusts the iframe height to match, so there is no clipped content and no inner scrollbar. Pass height only if you need a fixed-size slot.

Camera & permissions

Liveness needs the camera, and the widget runs in a cross-origin iframe — so camera access must be delegated to the frame. The SDK does this for you: the iframe it creates carries allow="camera; microphone" automatically.

  • If your site sends a Permissions-Policy header, it must allow OKIAS as well, or the browser blocks the camera before the iframe attribute is even consulted:
response header
Permissions-Policy: camera=(self "https://okias.io"), microphone=(self "https://okias.io")
  • HTTPS is required. Browsers only expose the camera on secure origins — the embedding page must be served over HTTPS (localhost is fine during development).

Security model

  • The verification id is single-user scope. It grants access to one verification for one end user — hand each user only their own id, and never reuse one across users.
  • Trust decisions only from signed webhooks. Browser events (onComplete, message events) are for UX — advancing your UI, showing a success screen. Anything a client sends can be forged; provision the user only after the signed webhook confirms the decision server-side.
  • Only /verify/* is embeddable. The verification flow is the one route that may be framed by other sites; every other OKIAS page is protected with frame-ancestors 'self', so the dashboard and console can never be embedded.
  • Your API key stays server-side. The browser only ever sees the verification id — never a secret key.

Under the hood: hosted endpoints

Inside the flow, the page talks to a public, unauthenticated hosted surface — authorisation is the unguessable id in the link:

GET/v1/hosted/verifications/:id
POST/v1/hosted/verifications/:id/submit

You do not normally call these yourself — the hosted page (and the embedded widget) does. They are documented so you understand what happens under the hood.

hosted (public) — reads status only
curl https://api.okias.io/v1/hosted/verifications/cms80k9ix001moea2jxy64fgu

Get the decision

When the pipeline resolves, the verification reaches a terminal status APPROVED, REVIEW, DECLINED or ERROR — and a signed webhook fires. Reconcile it to your user via the id in the event (the id you stored at create time — the event payload does not carry end_user_ref). Set up delivery in Webhooks.

No polling required
The hosted flow and webhooks are designed to work together: hand off capture, get the decision pushed back. If you prefer, you can still GET the verification.

Next steps