Skip to content

Quickstart

This guide takes you from zero to a working identity verification integration. You will:

  1. Create a session on your backend using your secret key.
  2. Mint a bt_* browser token and hand it to the SDK.
  3. Render the SDK in your page.
  4. Receive the outcome on your server via webhook.

Total reading time: about 10 minutes.

  • 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.* flow
client.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 tests
client.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.

Never create sessions in the browser. Your secret key must stay on your server.

Node.js example:

// POST /api/checktiv/session
export 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:

Terminal window
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 with app_), or inline applicant fields (for example email, first_name, last_name) to create the applicant in the same call.
  • Checks: a non-empty checks array of check types, or a saved workflow_template_id (must start with wt_) to run the checks configured on that workflow.

One-call example (new applicant, saved workflow, no separate applicant-create step):

Terminal window
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.

The SDK calls your getSessionToken callback when it needs a bt_* token. Implement a mint endpoint on your backend:

// POST /api/checktiv/token
export 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.

Install via npm:

Terminal window
npm install @checktiv/sdk-web

Or 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.

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/checktiv
export 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.