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.

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

Your backend SDK in the browser
| |
|-- POST /v1/sessions (ah_sk_*) -----------> platform
|<-- { id: "vs_01..." } --------------------- platform
| |
| (store vs_01 in your server session) |
| |
|<-- GET /your-page ---------- browser loads |
| |
|-- render page with publishable key --------> Checktiv.init(...)
| |
| SDK calls getSessionToken()
|<-- POST /api/checktiv/token |
| |
|-- POST /v1/sessions/vs_01/browser_token ---> platform
|<-- { browserToken: "bt_..." } ------------- platform
| |
|-- return { token: "bt_..." } ------------> |
| |
| SDK holds bt_* in memory only
| 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.