Skip to content

Token handoff

Understanding the token handoff is essential for a correct and secure integration. This page explains what each key type does, how they relate, and the security boundaries the design enforces.

Key Prefix Where it lives Purpose
Secret key ah_sk_* Your backend only Authenticates your server to the public API. Never in the browser.
Publishable key ah_pk_* Browser / frontend Identifies your organization and region to the SDK. Carries no scopes.
Browser token bt_* Browser (short-lived) Scoped credential the SDK uses to call sdk-api. Minted by your backend on demand.
Client token opaque (no fixed prefix) Your backend, then browser (durable) Low-privilege, resume-only capability. Exchanged internally for short-lived working tokens by Checktiv.mount(); never reads verification results or applicant PII.

The SDK never mints a bt_* itself. It calls your getSessionToken callback and receives the token from your backend.

Which token you hand to the SDK, and how often, depends on which of two shapes you use:

  • Backend mints a fresh bt_* per request (the flow diagrammed below). Your getSessionToken callback asks your backend for a browser token on demand, and Checktiv.init(...).mountProvisioned(...) drives capture. The rest of this page describes this flow in detail.
  • Zero-lifecycle: a durable client token plus Checktiv.mount(). Your backend mints ONE client token per applicant instead of a bt_* per page load, and the browser calls Checktiv.mount(target, { fetchToken, ... }) with fetchToken returning that client token. The SDK exchanges it for short-lived working tokens internally and refreshes them silently, so you write no token-refresh endpoint, no expiration handler, and no resume endpoint. This is the primary path documented end to end in the Quickstart.

Both shapes authorize the same sdk-api data plane; they differ only in who mints what, how often, and whether your backend is on the hot path for every token refresh.

sequenceDiagram
    participant Backend as Your backend
    participant SDK as SDK in the browser
    participant Platform as platform
    Backend->>Platform: POST /v1/sessions (ah_sk_*)
    Platform-->>Backend: { id: "vs_01..." }
    Note over Backend: store vs_01 in your server session
    SDK->>Backend: GET /your-page (browser loads)
    Backend->>SDK: render page with publishable key, Checktiv.init(...)
    Note over SDK: SDK calls getSessionToken()
    SDK->>Backend: POST /api/checktiv/token
    Backend->>Platform: POST /v1/sessions/vs_01/browser_token
    Platform-->>Backend: { browserToken: "bt_..." }
    Backend-->>SDK: return { token: "bt_..." }
    Note over SDK: SDK holds bt_* in memory only
    Note over SDK: SDK calls sdk-api with Authorization: Bearer bt_*

Why the browser token is short-lived and per-run

Section titled “Why the browser token is short-lived and per-run”

The bt_* token is scoped to one session and expires quickly (minutes, not hours). This limits the blast radius of a compromised token:

  • A leaked bt_* can only affect the one session it was issued for.
  • It cannot be used to list applicants, access other sessions, or perform any action requiring the secret key.
  • The SDK holds it in memory only; it is never written to localStorage or sessionStorage.

When the token expires mid-journey (the server returns 401), the SDK calls getSessionToken again with ctx.reason: '401' and retries the failed request exactly once. Your mint endpoint should issue a fresh bt_* for the same session without starting a new one.

The publishable key (ah_pk_us_test_...) encodes your region (us/eu) and mode (test/live) in its prefix. The SDK reads these to pick the correct endpoint automatically. Because it carries no scopes, it is safe to include in your frontend code and ship to browsers.

Before the SDK loads on your domain, that domain must be listed as an allowed origin for the publishable key. Add origins in the console under Developers -> API keys. A request from an unregistered origin produces an origin_not_allowed error. See API keys.

Concept What it is Who holds it
Session ID (vs_*) The durable server-side record of one verification run Your backend (store it server-side)
Browser token (bt_*) A short-lived credential scoped to that session The SDK in memory only

Your getSessionToken callback bridges the two: it knows the session ID (from your server session) and mints a fresh bt_* for it. The SDK never learns the session ID directly. Pass the vs_* id from the create response straight back as the :sessionId path segment when you call POST /v1/sessions/<vs_id>/browser_token; the mint endpoint accepts the wire id as-is, so you never strip or rewrite it.

The token manager in the SDK ensures only one in-flight mint request runs at a time. If two SDK calls need a token simultaneously, the second waits for the first refresh to complete rather than issuing a duplicate request to your backend.

The refresh cycle:

  1. SDK calls getSessionToken({ sessionId: '', reason: 'initial' }) on first use.
  2. If the server returns 401, SDK calls getSessionToken({ ..., reason: '401' }) and retries once.
  3. If the retry also returns 401, the SDK surfaces a token_expired error via onEvent.

The sessionId field in the callback context is empty until the SDK resolves GET /sdk/v1/sessions/me. In most implementations you do not need it because your backend already knows which session to mint a token for from the server-side session.

  • API keys - manage publishable and secret keys
  • Error reference - token_expired, wrong_token_type, origin_not_allowed, and recovery steps
  • Quickstart - see the full flow end to end, including the zero-lifecycle client-token shape
  • AI agent steering - rules and runnable recipes for both integration shapes