Four steps
1. Publish a workflow
In your dashboard, order your checks and click Publish. You'll get a slug like
wf_ng_merchant.2. Configure
allowed_originsUnder Settings → Embed, add the origins you'll embed from — e.g.
https://app.yourbrand.com. We refuse to render for origins outside this allowlist.3. Drop the snippet
Paste this anywhere in your HTML. The script self-mounts onto any element with a
data-karrverify-workflowattribute.<script src="https://embed.karverifi.com/embed.js" defer></script> <div data-karrverify-workflow="wf_ng_merchant"></div>4. Handle callbacks
Listen for
postMessageevents onwindowto react to completion, cancellation, or errors — see the event contract below.
Programmatic mount
Need to render the widget inside a modal, tab, or SPA route change? Call Karrverify.mount() directly.
Karrverify.mount({
container: '#kyc-slot', // CSS selector or Element
workflow: 'wf_ng_merchant', // published workflow slug
customer: { // pre-fill anything you already know
email: 'ada@example.com',
phone: '+2348012345678',
},
metadata: { order_id: 'ord_9f2a' },
session_token: 'tok_…', // optional: server-created session
environment: 'sandbox', // 'sandbox' | 'live' — defaults to 'live'
theme: 'auto', // 'auto' | 'light' | 'dark'
locale: 'en-NG',
onReady: () => {},
onComplete: (event) => {},
onError: (event) => {},
});For the best UX, create the session server-side with POST /v1/verifications and pass the resulting session token here — that way the widget never has to prompt the user for the information you already have.
Event contract
The widget communicates via window.postMessage. Every event is a JSON object with a type field. Always verify event.origin before trusting the payload.
karrverify:readyfired once, when the iframe has finished loading{
"type": "karrverify:ready",
"session_id": "ses_…"
}karrverify:completefired once, on final verification decision{
"type": "karrverify:complete",
"verification_id": "ver_…",
"status": "passed" | "failed" | "manual_review",
"steps": [
{ "kind": "bvn_verify", "status": "passed" },
{ "kind": "phone_verify", "status": "passed" }
]
}karrverify:errorfired any time the widget hits an unrecoverable error{
"type": "karrverify:error",
"code": "session_expired" | "origin_not_allowed" | "network_error",
"message": "Session token has expired."
}Styling
The widget renders in a sandboxed iframe, so your host page CSS never bleeds in. Control it from the outside.
Constrain width
Wrap the host element and set
max-width. The iframe fills 100% of its parent and grows vertically as steps unfold.Dark mode
theme: 'auto'respects the end-user'sprefers-color-scheme. Override withtheme: 'dark'or'light'to force one.Responsive
On mobile viewports the widget renders full-bleed with a bottom sheet for controls. No extra media queries needed on your side.
Troubleshooting
origin_not_allowed
The origin embedding the widget isn't in your allowlist. Add it under Settings → Embed. Origin = scheme + host + port, no path.
session_expired
Sessions are valid for 24 hours. Create a fresh one with POST /v1/verifications and re-mount.
Blank iframe / silent failure
Usually a missing allowed_origins entry. Open DevTools → console; we log the exact refusal reason.
CSP frame-ancestors
If your host page sets a strict Content-Security-Policy, add frame-src https://embed.karverifi.com and connect-src https://api.karverifi.com.
Prefer server-side flows? Skip to the API reference and skip the widget entirely.