Skip to content

Quickstart

This guide takes you from zero to a working, live-mode identity verification integration embedded in your own page. You will:

  1. Register your frontend’s origin on your publishable key.
  2. Create a session on your backend and return a durable clientToken.
  3. Mount 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 live-mode secret key (ah_sk_<region>_live_...) and publishable key (ah_pk_<region>_live_...) - see Create an API key if you do not have one yet.
  • Node.js 18+ on your backend, or any HTTP server that can make authenticated requests.
  • For local development: ensure your backend endpoint that creates the session and returns the clientToken is running before the SDK calls fetchToken.

The SDK only renders on an origin your publishable key allows. Open Developers -> API keys in the console, edit your publishable key, and add the origin your page will run on (or register it as a custom domain). Skip this step and every mount fails with origin_not_allowed, or the embed renders blank.

Step 2 - Create the session and return a clientToken (backend)

Section titled “Step 2 - Create the session and return a clientToken (backend)”
// GET /kyc/session (your backend)
import { sessions, regionToApiBase } from '@checktiv/sdk-web';
app.get('/kyc/session', async (req, res) => {
const session = await sessions.create(
{ templateId: 'wt_01abc', applicant: { externalId: req.user.id } },
{
apiBase: regionToApiBase('us'), // or 'eu' for your account's region
secretKey: process.env.CHECKTIV_SECRET_KEY, // ah_sk_* - never in the browser
idempotencyKey: req.user.id, // same user maps to the same session; retries never double-mint
},
);
res.json({ clientToken: session.clientToken }); // durable, low-privilege, resume-only
});

sessions.create throws if it runs in a browser - it needs your secret key, so call it only from your server. The clientToken it returns can only resume the applicant’s own in-progress session; it never reads verification results or applicant PII.

Install via npm:

Terminal window
npm install @checktiv/sdk-web
<div id="checktiv"></div>
<script type="module">
import { mount } from '@checktiv/sdk-web';
import '@checktiv/sdk-web/idv'; // self-registers the modules mount may render
import '@checktiv/sdk-web/idv/cross-device'; // registers openCrossDevice() (see cross-device below)
import '@checktiv/sdk-web/fraud'; // omit if this workflow never declares fraud
import '@checktiv/sdk-web/capture-ui/style.css'; // required: styles the managed capture UI
const handle = mount(document.getElementById('checktiv'), {
publishableKey: 'ah_pk_us_live_...', // safe to expose - no scopes
// Called ONCE for the cold start (or a new device), never on every expiry.
fetchToken: () =>
fetch('/kyc/session')
.then((res) => res.json())
.then((data) => data.clientToken),
onConsent: () => showConsentPrompt(), // required only if the session declares fraud
onEvent: (event) => console.log(event),
onComplete: (result) => {
// Capture is complete. Wait for the webhook verdict before acting.
showMessage('Verification submitted - you will hear from us shortly.');
},
});
</script>

Pass onConsent whenever the session declares the consent-gated fraud module - see Fraud consent for the default-deny behavior. mount() also accepts an optional resumeKey so an applicant who abandons and returns resumes at their current step with no new backend call; see AI agent steering for the full option surface. It also accepts layout, theme, and copy. Pass layout: 'immersive' for a full-screen capture on phones. Pass theme, for example { primaryColor: '#0b5fff' }, to brand the capture surface (see Theming). Pass copy to override the capture UI text: the per-phase status line, the inline error messages, the “Try again” button label, the coaching banners, and the frame title. Every key is optional and falls back to a built-in English string, so a partial map is safe. On a desktop the capture frame sizes itself up for the larger viewport automatically (with a height cap so it never overflows a short window) and centers in your container, so no stylesheet override is needed; on a phone it stays sized for the mobile viewport. To control the frame’s width directly, pass maxWidth with a CSS length such as '32rem' or '480px'; it applies uniformly across every screen size and orientation, including a sideways phone. The built-in aspect ratio and the height clamps (which keep the frame from overflowing a short or landscape viewport) stay independent of it. An invalid value is ignored and falls back to the default sizing, so leave it unset to keep that default.

mount() exposes openCrossDevice() on its returned handle so a desktop applicant can continue on their phone. Pass a crossDeviceCopy map, render your own “Continue on your phone” trigger, and call handle.openCrossDevice() from it. The SDK owns the rest: it mints the one-time link, opens the QR overlay over the live capture frame, and runs a completion poll that fires your onComplete when the phone finishes. The trigger is desktop only: gate it to a fine-pointer device, because a phone applicant already gets the immersive capture layout and the SDK ignores the call on a touch device. On the npm or bundler path, add the side-effect import import '@checktiv/sdk-web/idv/cross-device'; (it registers the opener, like the /idv and /fraud imports) or openCrossDevice() is a warn-no-op; the CDN script-tag bundle registers it automatically. See Cross-device handoff.

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.

If your page sends a Content Security Policy, this origin needs an entry in script-src before the journey will run, and the entry is required whichever modules your sessions use. See Security headers.

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 },
// plus `mode`, `type` and `decisionSource` on events emitted after this release.
// Treat all three as optional: they are absent on older or replayed events, and
// `decisionSource` is also absent when the transition carried no decision and when
// the applicant ended the session themselves. Do not treat an absent `mode` as
// `live`, and do not read an absent `decisionSource` as "no decision was made".
// 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.

Use this shape when your backend mints a fresh bt_* browser token on every request instead of holding a durable clientToken - for example, an existing backend that already mints a token per page load. For new integrations, the clientToken + mount() path above is the recommended default: it needs no token-mint endpoint and no expiry handling.

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_live_<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, family_name, given_names) 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_live_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"applicant": { "email": "jane.doe@example.com", "family_name": "Garcia Lopez", "given_names": ["Jose", "Maria"] },
"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 the session-create step
// Authenticate the caller and confirm they own this sessionId before minting -
// never mint a browser token for an arbitrary client-supplied id.
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.browser_token }); // 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.

Initialize the client with your publishable key plus a getSessionToken callback that fetches a fresh token from your backend, 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
import '@checktiv/sdk-web/capture-ui/style.css'; // required: styles the managed capture UI
const SESSION_ID = '<the vs_* id your backend returned at session-create>';
const client = init({
publishableKey: 'ah_pk_us_live_...', // safe to expose - no scopes
getSessionToken: async (ctx) => {
const res = await fetch('/api/checktiv/token', {
method: 'POST',
body: JSON.stringify({ sessionId: SESSION_ID, 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. The workflow-template caution in Step 3 above applies here too: order your workflow so an SDK-renderable step is first.

Receive the verdict the same way as the golden path above - see Step 4.

A test-mode key (ah_pk_<region>_test_* / ah_sk_<region>_test_*) is for automated and CI testing: it runs headless, with no capture UI. See AI agent steering for the synthetic-driver recipe that runs the full checktiv.idv.* event flow locally with no camera, no capture license, and no deployed cell.