This guide takes you from zero to a working identity verification integration. You will:
- Create a session on your backend using your secret key.
- Mint a
bt_*browser token and hand it to the SDK. - Render the SDK in your page.
- Receive the outcome on your server via webhook.
Total reading time: about 10 minutes.
Prerequisites
Section titled “Prerequisites”- A Checktiv account with a test-mode secret key (
ah_sk_us_test_...) and publishable key (ah_pk_us_test_...). - Node.js 18+ on your backend, or any HTTP server that can make authenticated requests.
- For local development: ensure your own session-mint backend and token endpoint are running before the SDK calls
getSessionToken.
Local development: use a test-mode key with the synthetic driver
Section titled “Local development: use a test-mode key with the synthetic driver”A test-mode publishable key (ah_pk_<region>_test_*) on a local-dev origin renders a blank embed because the capture UI is licensed per domain and localhost is not an allowed origin. Use the synthetic driver instead: it runs the full checktiv.idv.* event flow with no camera, no capture license, and no deployed cell required.
Test mode is selected by the key itself, not by a mode property on init. Pass synthetic: {} to mount to enable the no-camera happy-path driver, or inject a specific error code to exercise error handling:
const client = Checktiv.init({ publishableKey: 'ah_pk_us_test_...', // test-mode key - mode derived from the key getSessionToken: async () => { const res = await fetch('/api/checktiv/token', { method: 'POST' }); return (await res.json()).token; },});
// Happy-path synthetic driver: no camera, no license, full checktiv.idv.* flowclient.mount('idv', { target: document.getElementById('idv-container'), synthetic: {}, // empty object = happy path; omit to use real capture onEvent: (event) => console.log(event),});
// Error-injection example: force a specific error code for error-handling testsclient.mount('idv', { target: document.getElementById('idv-container'), synthetic: { injectError: 'camera_denied' }, onEvent: (event) => console.log(event),});Graduate to a deployed dev cell when you want to test real camera capture.
Step 1 - Create a session (backend)
Section titled “Step 1 - Create a session (backend)”Never create sessions in the browser. Your secret key must stay on your server.
Node.js example:
// POST /api/checktiv/sessionexport async function POST(req) { const response = await fetch('https://api.us.checktiv.com/v1/sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.CHECKTIV_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ applicant_id: 'app_01abc', // your `app_*` identifier for this person checks: ['id_verification'], // one or more check types to run }), }); const { data } = await response.json(); return Response.json({ sessionId: data.id }); // a `vs_*` string}curl example:
curl -X POST https://api.us.checktiv.com/v1/sessions \ -H "Authorization: Bearer ah_sk_us_test_<your-key>" \ -H "Content-Type: application/json" \ -d '{"applicant_id":"app_01abc","checks":["id_verification"]}'The request body needs exactly one applicant selector and exactly one check selector:
- Applicant: an existing
applicant_id(must start withapp_), or inlineapplicantfields (for exampleemail,first_name,last_name) to create the applicant in the same call. - Checks: a non-empty
checksarray of check types, or a savedworkflow_template_id(must start withwt_) to run the checks configured on that workflow.
One-call example (new applicant, saved workflow, no separate applicant-create step):
curl -X POST https://api.us.checktiv.com/v1/sessions \ -H "Authorization: Bearer ah_sk_us_test_<your-key>" \ -H "Content-Type: application/json" \ -d '{ "applicant": { "email": "jane.doe@example.com", "first_name": "Jane", "last_name": "Doe" }, "workflow_template_id": "wt_01abc" }'Save the returned id (a vs_* string) in your session or database: you pass it straight back as the session id when you mint a browser token.
Step 2 - Mint a browser token (backend)
Section titled “Step 2 - Mint a browser token (backend)”The SDK calls your getSessionToken callback when it needs a bt_* token. Implement a mint endpoint on your backend:
// POST /api/checktiv/tokenexport async function POST(req) { const { sessionId } = await req.json(); // the `vs_*` id from Step 1 const response = await fetch( `https://api.us.checktiv.com/v1/sessions/${sessionId}/browser_token`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.CHECKTIV_SECRET_KEY}` }, }, ); const { data } = await response.json(); return Response.json({ token: data.browserToken }); // bt_* string}The bt_* token is short-lived. The SDK refreshes it automatically by calling getSessionToken again with ctx.reason: '401' when the server signals expiry.
Step 3 - Initialize the SDK (frontend)
Section titled “Step 3 - Initialize the SDK (frontend)”Install via npm:
npm install @checktiv/sdk-webOr load via CDN:
<script src="https://sdk.us.checktiv.com/v1/sdk.js" crossorigin="anonymous"></script>This loads the latest release. For production deployments, pin to a specific version with a Subresource Integrity hash instead - SRI requires the immutable pinned URL (/sdk/<ver>/sdk.js), not the moving /v1/sdk.js pointer. See Versioning for the pin URL shape and the SRI hash from the release notes.
Initialize the client and render the managed IDV module:
<div id="idv-container"></div>
<script type="module"> import { init } from '@checktiv/sdk-web'; import '@checktiv/sdk-web/idv'; // registers the IDV module so mountProvisioned can render it
const client = init({ publishableKey: 'ah_pk_us_test_...', // safe to expose - no scopes getSessionToken: async (ctx) => { const res = await fetch('/api/checktiv/token', { method: 'POST', body: JSON.stringify({ reason: ctx.reason }), headers: { 'Content-Type': 'application/json' }, }); return (await res.json()).token; }, theme: { primaryColor: '#0b5fff' }, // optional: match your brand });
client.mountProvisioned({ target: document.getElementById('idv-container'), onEvent: (event) => { if (event.type === 'checktiv.idv.submitted') { // Capture is complete. Wait for the webhook verdict before acting. showMessage('Verification submitted - you will hear from us shortly.'); } }, });</script>mountProvisioned reads the server-declared modules from the session and renders them. The secret key is never in this code.
Step 4 - Receive the verdict (backend)
Section titled “Step 4 - Receive the verdict (backend)”checktiv.idv.submitted means capture finished, not that verification passed. The authoritative verdict arrives on your server as a signed kyc.session.* webhook. See Verdict and webhooks for the full receiver implementation including HMAC signature verification.
Quick example for Node.js:
// POST /webhooks/checktivexport async function POST(req) { const rawBody = await req.text(); const sig = req.headers.get('x-webhook-signature'); // t=...,v1=...
verifyWebhookSignature(sig, rawBody, process.env.CHECKTIV_WEBHOOK_SECRET); // Throws on invalid. Do not proceed if it throws.
// The delivered body is the flat payload { sessionId, status, outcome, occurredAt }. // The event class is determined by your subscription, not a field in the body. // Read the verdict from `status` + `outcome`. const payload = JSON.parse(rawBody); if (payload.status === 'completed' && payload.outcome === 'approved') { await approveApplicant(payload.sessionId); }
return new Response(null, { status: 200 });}Register your webhook URL under Developers -> Webhooks in the console, then copy the signing secret. See Verdict and webhooks for the full status + outcome mapping.
Next steps
Section titled “Next steps”- Token handoff - understand the full session and token lifecycle
- Error reference - handle every error code in
onEvent - Verdict and webhooks - complete signature verification implementation
- React - React-specific integration with
<ChecktivProvider>