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.

## Keys and their roles

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

## The handoff flow

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

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

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](/developers/api-keys).

## Session ID vs browser token

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

## Token refresh in the SDK

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.

## Related pages

- [API keys](/developers/api-keys) - manage publishable and secret keys
- [Error reference](/developers/sdks/error-reference) - `token_expired`, `origin_not_allowed`, and recovery steps
- [Quickstart](/developers/sdks/quickstart) - see the full flow end to end