This is the runnable companion to the [Quickstart](/developers/sdks/quickstart): a
tiny Express server and a single HTML page that embed identity verification
directly in your own page. It is deliberately small so you can read the whole
integration top to bottom, then adapt it to your stack.

The integration is two steps, and it hinges on one split of responsibility:

- Your **backend** holds the **secret key** (`ah_sk_...`) and does the one thing
  the browser must never do: create a session and return a durable, low-privilege
  `clientToken`.
- Your **frontend** loads the SDK with the **publishable key** (`ah_pk_...`, safe
  to expose because it is origin-pinned) and renders the verification.

You write no token-lifecycle code. `mount()` calls your `fetchToken` callback
ONCE at the cold start to get the `clientToken`, then exchanges it for
short-lived working tokens internally and refreshes them silently.

:::note[Need a browser token minted fresh on every page load instead?]
This sample uses the durable `clientToken` pattern. If your backend already mints
a short-lived `bt_*` browser token per request instead, see [Alternative:
per-request tokens](/developers/sdks/quickstart#alternative-per-request-tokens)
in the Quickstart. Both paths are valid.
:::

## The two steps

| Step | Where    | Call                                          |
| ---- | -------- | --------------------------------------------- |
| 1    | backend  | Create a session and return its `clientToken` |
| 2    | frontend | `Checktiv.mount(target, { fetchToken, ... })` |

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

Your secret key must never reach the browser. Create the session on your server
using the SDK's `sessions.create` helper and return only the durable
`clientToken` - it can resume the applicant's own in-progress session but can
never read results or PII.

```js
// GET /api/client-token (your backend)
import { sessions, regionToApiBase } from '@checktiv/sdk-web';

// This route mints a durable, resume-capable token, so it must sit behind
// your own authentication (session cookie, JWT, etc.) - never expose it as a
// public, unauthenticated endpoint.
app.get('/api/client-token', async (req, res) => {
  const { clientToken } = await sessions.create(
    {
      // `templateId` is a saved workflow template id (`wt_...`). The workflow
      // template decides which modules run; the SDK renders whatever the
      // server declares for the session.
      templateId: 'wt_01abc',
      // Inline applicant details for a new applicant. Every field is optional.
      // The name is `familyName` plus an ordered `givenNames` array.
      applicant: { familyName: 'Lovelace', givenNames: ['Ada'], email: 'ada@example.com' },
    },
    {
      apiBase: regionToApiBase('us'), // or 'eu' for your account's region
      secretKey: process.env.CHECKTIV_SK_KEY, // ah_sk_... - never in the browser
      // One stable key PER logical create (here, the authenticated caller's
      // id); use a fresh key for a distinct applicant. The same key with the
      // same body replays the original response; the same key with a
      // different body returns 409 idempotency_conflict.
      idempotencyKey: req.user.id,
    },
  );
  res.set('Cache-Control', 'no-store'); // never cache a resume-capable token
  res.json({ clientToken });
});
```

`sessions.create` throws if it runs in a browser, so call it only from your
server. Because the `clientToken` is durable and resume-capable, the sample
above derives the idempotency key from the authenticated caller (`req.user.id`)
rather than a client-supplied value - see the
[Quickstart](/developers/sdks/quickstart#step-2---create-the-session-and-return-a-clienttoken-backend)
for the full security note on authenticating this endpoint.

## Step 2 - Load and mount the SDK (frontend)

Load the SDK bundle from the CDN. It exposes a global `Checktiv`.

```html
<script src="https://sdk.us.checktiv.com/v1/sdk.js" crossorigin="anonymous"></script>
```

This loads the latest release. For production, 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 how to
derive the SRI hash.

Then mount into a target element, with a `fetchToken` callback that asks your
backend for the `clientToken`:

```html
<div id="checktiv"></div>

<script>
  const handle = Checktiv.mount(document.getElementById('checktiv'), {
    publishableKey: 'ah_pk_us_live_...', // public by design (origin-pinned)
    // Called ONCE at the cold start. The SDK exchanges the clientToken for
    // short-lived working tokens internally and refreshes them silently -
    // this is the whole token lifecycle, and you write none of it.
    fetchToken: function () {
      return fetch('/api/client-token')
        .then(function (r) {
          return r.json();
        })
        .then(function (d) {
          return d.clientToken;
        });
    },
    onEvent: function (event) {
      if (event.type === 'checktiv.idv.submitted') {
        // Capture finished. Wait for the webhook verdict before acting on it.
      }
    },
  });
</script>
```

`mount()` reads the modules the server declared for the session and renders
them. For a template whose first applicant step is `id_verification` that is
the managed IDV (document + selfie) capture flow. 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 webhook. See
[Verdict and webhooks](/developers/sdks/verdict-and-webhooks) for the receiver
and signature verification.

:::note[Your frontend origin must be registered first]
The publishable key is origin-pinned: the page must be reached through an
origin registered on the key (console -> Developers -> API keys) or a
registered custom domain, or the mount fails with `origin_not_allowed`. Bare
`http://localhost` is never an allowed origin - front your local server with a
tunnel (for example `cloudflared tunnel --url http://localhost:3000`) and
register the tunnel's origin, or open the hosted demo (`sdk-demo.checktiv.com`)
to see the capture flow with no local setup. See [Step 1 of the
Quickstart](/developers/sdks/quickstart#step-1---register-your-frontend-origin).
:::

## Run the full sample

The complete runnable version of this walkthrough ships as an SDK example named
`idv-sdk-zerolifecycle`: one Express server file, one HTML file, a
`.env.example`, and a run script. It reads the secret key from the
`CHECKTIV_SK_KEY` environment variable, never from a file, and injects the
publishable key into the page at render time.

```bash
npm install
export CHECKTIV_SK_KEY='ah_sk_...'          # your secret key, from your environment
export CHECKTIV_TEMPLATE_ID='wt_...'         # a saved workflow template id
export CHECKTIV_PK_KEY='ah_pk_...'           # your publishable key
node server.js                                # serves the page on port 3000
```

Because the publishable key is origin-pinned, open the page through the origin
your key is registered for, not bare `http://localhost`.

Its sibling, `idv-sdk-quickstart`, runs the same integration with the
[alternative per-request `bt_*` flow](/developers/sdks/quickstart#alternative-per-request-tokens)
instead - read the two side by side if you need that pattern.

## Next steps

- [Quickstart](/developers/sdks/quickstart) - the same flow with the webhook receiver, plus the alternative per-request token path
- [Token handoff](/developers/sdks/token-handoff) - the full session and token lifecycle, including both token shapes
- [Error reference](/developers/sdks/error-reference) - handle every error code in `onEvent`
- [React](/developers/sdks/react) - React integration: `<ChecktivJourney>` and `<ChecktivProvider>`