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.

## 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

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:

```js
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.

## Step 1 - Create a session (backend)

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

**Node.js example:**

```js
// 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:**

```bash
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):

```bash
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)

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

```js
// 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.

## Step 3 - Initialize the SDK (frontend)

Install via npm:

```bash
npm install @checktiv/sdk-web
```

Or load via CDN:

```html
<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](/developers/sdks/versioning) for the pin URL shape and the SRI hash from the [release notes](/developers/sdks/release-notes).

Initialize the client and render the managed IDV module:

```html
<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)

`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](/developers/sdks/verdict-and-webhooks) for the full receiver implementation including HMAC signature verification.

Quick example for Node.js:

```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](/developers/sdks/verdict-and-webhooks) for the full `status` + `outcome` mapping.

## Next steps

- [Token handoff](/developers/sdks/token-handoff) - understand the full session and token lifecycle
- [Error reference](/developers/sdks/error-reference) - handle every error code in `onEvent`
- [Verdict and webhooks](/developers/sdks/verdict-and-webhooks) - complete signature verification implementation
- [React](/developers/sdks/react) - React-specific integration with `<ChecktivProvider>`