AI coding agents integrate the Checktiv Web SDK best when handed the rules and runnable
recipes below. The same content ships inside the package so an agent finds it without the
docs:

- `AGENTS.md` and a machine-readable `manifest.json` ship in `@checktiv/sdk-web`, resolvable
  via the `@checktiv/sdk-web/agents` export. The manifest carries the module composition
  contract and the full code for every recipe below.
- This page mirrors that content for humans and is included in the docs `/llms-full.txt`.

## Rules

1. **The server mints the session, never the browser.** Your backend calls the public API `POST /v1/sessions` with your secret key (`ah_sk_*`) and receives the session. The secret key is server-only: NEVER put `ah_sk_*` in browser code. To hand control to the frontend, your backend mints a short-lived browser token (`bt_*`) by calling `POST /v1/sessions/{id}/browser_token` with the same `ah_sk_*`; that route returns `{ data: { browser_token, expires_at } }`. Only the `bt_*` string crosses to the browser.

2. **Use mountProvisioned as the default path.** Call `Checktiv.init({ ... }).mountProvisioned({ target })`. It reads the server-declared `session.modules` from `GET /sdk/v1/sessions/me` and renders exactly what the server composed, so the server drives composition. `mount('idv')` / `mount('fraud')` is the explicit override / escape hatch, not the first choice.

3. **Completion is not a verdict: trust the signed webhook.** The client event `checktiv.idv.submitted` is terminal-for-capture only; it means the applicant finished the capture step, NOT that they passed. The decision arrives on your server through the signed `kyc.session.*` webhook. Trust the signed webhook as the only outcome anchor; never infer a pass/fail from a client event.

4. **Satisfy each applicant-info requirement once, through any one path.** When a session declares applicant-info requirements (name, date of birth, address, and similar fields), each requirement is satisfied by ANY ONE of the supported collection paths, not by all of them. Supply a field through one path and the SDK treats that requirement as met; do not double-collect the same field. Let the server-declared session decide which fields are required rather than hard-coding a form.

5. **Develop local-first with the synthetic driver.** Use a **test-mode publishable key** (`ah_pk_<region>_test_*`) PLUS the synthetic opt-in. Test mode is selected by the key itself, not by a `mode` property on `init`. A test-mode key without the synthetic option renders a BLANK embed on a local origin because the capture license is domain-bound, whereas the synthetic path runs the full `checktiv.idv.*` flow locally with no camera, no license, and no deployed environment. Ensure your own session-mint backend and token endpoint are running before the SDK calls `getSessionToken`. Build and verify locally with the synthetic driver, then graduate to a deployed environment for real capture and ship through the standard npm / CDN path.

6. **getSessionToken supplies a fresh bt_* on demand.** `getSessionToken` is `string | ((ctx: { sessionId; reason: 'initial' | 'expired' | '401' }) => Promise<string>)`. The SDK calls it on mount and again on expiry or a 401, then retries the failed request once. Always return a freshly minted `bt_*` from your backend; never hold a long-lived credential in the browser. If your callback rejects, the SDK emits `token_expired` with `recovery: 'refresh_session'` and halts cleanly.

7. **Add your frontend origin to the publishable key allowlist first.** Each publishable key carries an origin allowlist that you set in the console / dashboard. Your frontend's origin MUST be on that allowlist before the first mount, or EVERY mount fails with `origin_not_allowed` (the `bt_*` data-plane routes reject an off-allowlist `Origin`). Add your origin to the publishable key allowlist before you integrate.

8. **The SDK adds X-Publishable-Key itself on the customer path.** Pass `publishableKey` to `Checktiv.init(...)` and stop there. On the customer (pk-present) path the managed transport attaches the `X-Publishable-Key` header to its own `bt_*` data-plane calls internally. Do NOT also hand-add an `X-Publishable-Key` header yourself; double-adding it is wrong. (The first-party hosted path used internally by the verify app carries no publishable key and sends none.)

9. **Wire cross-device QR handoff via openCrossDeviceOverlay.** Cross-device handoff is wired via STANDALONE exports from the `@checktiv/sdk-web/idv/cross-device` subpath (NOT via an option on `IdvMountOptions` / `mountProvisioned`). Import: `import { openCrossDeviceOverlay, preloadCrossDeviceChunk } from '@checktiv/sdk-web/idv/cross-device';`

`openCrossDeviceOverlay(opts: CrossDeviceOverlayOptions)` opens the overlay. Key fields on `CrossDeviceOverlayOptions`:
  • `target: HTMLElement`: the module's root container element.
  • `onOpenCrossDevice: CrossDeviceHandoffHook`: HOST-supplied mint hook. Type: `() => Promise<{ kind: 'ok'; url: string } | { kind: 'unavailable' }>`. Your host calls its own OTL-mint BFF and returns `{ kind: 'ok', url }` with the journey URL, or `{ kind: 'unavailable' }` if the mint fails. The URL MUST be `https:` (the SDK validates this and collapses `javascript:`, `http:`, and `data:` schemes to `'unavailable'`). The hook is re-callable (the overlay has a "refresh link" affordance); cap re-mints with a per-overlay counter on your host.
  • `copy: CrossDeviceCopy`: host-injected copy map; no hardcoded strings.
  • `isMobile: boolean`: `true` suppresses the QR (same-device scan is circular).
  • `emit: (e: ChecktivIdvEvent) => void`: forward to the host's `onEvent` handler.
  • `onClose?: () => void`: called when the applicant closes the overlay or `destroy()` is called.

Returns `CrossDeviceOverlayHandle` with:
  • `setCompleting()`: marks the session completing (phone finished; overlay stays mounted with a waiting label instead of disappearing).
  • `destroy()`: tears down the overlay. Idempotent.

`preloadCrossDeviceChunk()`: fire-and-forget preload of the lazy CDN chunk. Call it when the IDV module becomes active to reduce perceived latency on open.

Events (both are token-free, NEVER contain the url/OTL/short-code):
  • `checktiv.idv.cross_device_opened`: hook returned `ok`, overlay shown.
  • `checktiv.idv.cross_device_unavailable`: hook returned `unavailable` or URL validation failed.
There is NO `cross_device_completed` event. Completion is the HOST's responsibility: poll your own `/api/idv/cross-device-status` BFF (returns `{ advanced: boolean }`) and call `window.location.reload()` exactly once when `advanced: true`. The SDK module NEVER writes `window.location`. Use `setCompleting()` on the returned handle before reloading so the overlay shows a completing state rather than disappearing abruptly.

The browser SDK CANNOT self-mint: `bt_*` bearers are rejected at the OTL-mint route. The host must own a server-side BFF that holds a `vt_*` cookie (HttpOnly, SameSite=Lax) and calls the sdk-api OTL-mint endpoint to produce the handoff URL.

## Runnable recipes

The full, copy-pasteable code for each step ships in the package manifest
(`@checktiv/sdk-web/agents`):

- **(a) Backend: mint a session with your secret key**
- **(b) Backend: mint a bt_* browser token and hand it to the frontend**
- **(c) Frontend: init and mountProvisioned**
- **(d) Backend: verify the signed webhook (HMAC) - the only outcome anchor**