> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# Client tokens and domain restrictions

Use a client token to authenticate a chat widget in client-side code. The token is public, scopes requests to its capabilities, and restricts requests by origin.

## How client tokens work

The generated widget embed code uses the client token that you select in **Embed** in the workbench or **Ship** in the classic dashboard. Client tokens have these properties:

* **Public**: Include a client token in client-side code. Keep API keys in server-side code.
* **Capability-scoped**: The token authorizes calls only to the Flows and Agents that you attach to it.
* **Origin-restricted**: The token accepts requests when the request origin matches its `allowedOrigins` list. Test tokens also allow local origins automatically.

## Choose a token environment

A client token is a test token or a live token. Every Surface accepts both:

* **Test token** (`ct_test_` prefix): stays copyable after creation and automatically allows local origins. Use it while you build.
* **Live token** (`ct_live_` prefix): shown once when the dashboard creates it. Use it for real traffic.

Test tokens automatically allow `http://localhost`, `http://127.0.0.1`, `http://0.0.0.0`, and `http://[::1]` with any port. Add staging and tunnel origins to the allowed origins explicitly. Both environments use the same session and rate limits. For more information, see [Set session and rate limits](#set-session-and-rate-limits).

## Create a client token

A token must name at least one Flow or Agent. In the dashboard, a token takes its capabilities from the chat Surface, so link at least one Flow or Agent to the Surface before you create the token. The API, SDK, and CLI take the Agent or Flow IDs directly and do not need a Surface. For those paths, see [Create a token from code](#create-a-token-from-code).

In the workbench, a new token takes its release target and limits from **Token defaults** in **Access & keys**. Set those defaults first, or change a single token later in its **Token settings**.

To create a client token in the dashboard, follow these steps:

1. In the Runtype dashboard, open your Product and select the chat Surface.
2. In the workbench, open **Access & keys** and click **New token**. In the classic dashboard, open **Auth** and click **Create Token**.
3. Enter a name for the token.
4. In the workbench, select **Live** or **Test** in **Mode**. The classic dashboard creates a live token.
5. In **Allowed origins** (**Allowed Origins** in the classic dashboard), enter a comma-separated list of origins.
6. Click **Create token** in the workbench or **Create Token** in the classic dashboard. The token appears in the token list.

Save a live token's value when it is displayed. The workbench cannot reveal it later. Test tokens remain copyable from their row. Select the token in **Embed** or **Ship** to generate widget code; replace `YOUR_CLIENT_TOKEN` in a live snippet with the saved value.

To confirm that the widget uses the token, load your page and send a message. The token's `lastUsedAt` and `conversationCount` values update, and you can read them with `Runtype.clientTokens.get(id)` or the [Client Tokens API](/api-reference/runtype-api/client-tokens/list-client-tokens).

## Create a token from code

To create a token for an Agent from a script or CI job, pass the Agent's ID in `agentIds`. Pass Flow IDs in `flowIds` together with a `defaultFlowId`. You do not need a Product or a Surface.

`Runtype.clientTokens.ensure` from `@runtypelabs/sdk` manages a token by name. It creates the token when no token has that name, updates the token when its configuration differs, and does nothing when it matches. The result includes the token value, so a repeated run can still configure the widget:

**`SDK`**

```typescript title="SDK"
import { Runtype } from '@runtypelabs/sdk'

Runtype.configure({ apiKey: process.env.RUNTYPE_API_KEY })

const { result, clientToken, token } = await Runtype.clientTokens.ensure({
  name: 'Docs site widget',
  agentIds: ['agent_01jz...'],
  allowedOrigins: ['https://docs.example.com'],
})
// result is 'created', 'updated', or 'unchanged'.
// Pass token and the Agent ID to the widget.
```

`ensure` behaves as follows:

* **Identity**: `ensure` matches on the exact token name within the account that the API key belongs to. It throws when more than one token has that name.
* **Always converged**: `agentIds`, `flowIds`, `allowedOrigins`, `targetAlias`, and `targetVersionId`. An omitted target field removes that target from the token. The order of entries in a list does not matter.
* **Converged when set**: `defaultFlowId`, the rate limits, `maxMessagesPerSession`, `sessionIdleTimeoutMinutes`, and `config`. An omitted field keeps its current value.
* **Fixed at creation**: `environment` (default `live`) and `productSurfaceId`. `ensure` throws when the existing token has a different value, because the API cannot change these fields. The API also cannot remove every Flow from a token, so `ensure` throws when the token has Flows and the definition has none. Delete the token and run `ensure` again.
* **Never**: `ensure` does not delete or regenerate a token.

Pass `{ dryRun: true }` to return a plan without writing, or `{ expectNoChanges: true }` to throw `ClientTokenDriftError` when the token differs from the definition.

`ensure` returns `token: null` for a live token that was created in the workbench, because the workbench does not store a live token's value. A token that the API, SDK, or CLI creates stores its value, and `Runtype.clientTokens.getToken(id)` returns it later. For a workbench-created live token whose value you lost, [regenerate the token](#regenerate-a-client-token).

`Runtype.clientTokens` also provides `list`, `get`, `create`, `update`, `delete`, `regenerate`, and `getToken`. The same methods are available on `createClient({ apiKey }).clientTokens`.

To create a token from the CLI, use `runtype client-tokens create`. The CLI creates a `test` token unless you pass `--environment live`, and it allows every origin unless you pass `--origins`:

**`CLI`**

```bash title="CLI"
runtype client-tokens create \
  --name "Docs site widget" \
  --agent-ids agent_01jz... \
  --environment live \
  --origins https://docs.example.com
```

The CLI also has `list`, `get <id>`, `delete <id>`, and `regenerate <id>` subcommands. It has no update command. To change an existing token, use the workbench, the SDK, or the [Client Tokens API](/api-reference/runtype-api/client-tokens/list-client-tokens). The Runtype MCP server covers the same actions with the `list_client_tokens`, `get_client_token`, `create_client_token`, `update_client_token`, `delete_client_token`, and `regenerate_client_token` tools.

For a complete example that defines an Agent, creates a token, and embeds the widget, see [Build the widget from code](/user-guide/getting-started/quickstart-from-agent-to-chat-widget#build-the-widget-from-code).

## Set allowed origins

Set the token's `allowedOrigins` value to the origins that can use the widget. Enter the scheme and hostname, and include the port when the site uses a non-default port. To change the origins on an existing token in the workbench, open **Access & keys**, click the token's options menu, and select **Edit token settings**.

Use these origin forms:

* `https://example.com`: an exact origin.
* `https://www.example.com`: a hostname that differs from `example.com`.
* `http://localhost:3000`: a local origin with port `3000`.

Do not enter a bare hostname such as `example.com`. The scheme must match the request origin.

In the workbench, a chat Surface can also have its own **Allowed origins** list under **Origins**. Requests must match both the token and Surface lists. A Surface list also applies to test-token localhost requests. Leave the Surface list unset if token-level restrictions are sufficient.

### Check exact origin matching

For exact origin entries, Runtype compares the scheme, hostname, and port. Check these values when a request fails:

* **Scheme**: `https://example.com` does not match `http://example.com`.
* **Hostname**: `example.com` does not match `www.example.com`.
* **Port**: `http://localhost:3000` does not match `http://localhost:5173`.

### Allow subdomains

To match subdomains under one hostname, enter `https://*.example.com`. Runtype matches `https://app.example.com` and `https://staging.example.com`, but it does not match `https://example.com`. Add `https://example.com` separately when you need the apex domain.

A wildcard entry ignores the port and matches subdomains at any depth, such as `https://a.b.example.com`. Use exact origins when you need port-specific matching.

### Allow every origin or native apps

To allow every origin, enter `*`. Avoid this setting on a Surface that serves live traffic because it removes origin filtering.

For a native app that sends no `Origin` header, enter `native` as an allowed origin. This entry admits originless requests and does not match a web origin. Use a separate token for native apps because this setting removes origin protection for that token.

When you create or update a token with an `allowedOrigins` list that contains `native`, the response includes a `warnings` array with a `native_origin` entry.

## Set session and rate limits

Each token carries four limits. The following table lists each limit's field, workbench label, CLI flag, default, and allowed range:

| Field                       | Workbench label             | CLI flag                  | Default | Range      |
| --------------------------- | --------------------------- | ------------------------- | ------- | ---------- |
| `rateLimitPerMinute`        | **Per minute**              | `--rate-limit-per-minute` | 10      | 1 to 1000  |
| `rateLimitPerHour`          | **Per hour**                | `--rate-limit-per-hour`   | 100     | 1 to 10000 |
| `maxMessagesPerSession`     | **Messages per session**    | `--max-messages`          | 100     | 1 to 1000  |
| `sessionIdleTimeoutMinutes` | **Idle timeout in minutes** | `--session-timeout`       | 30      | 1 to 1440  |

A widget session ends when it reaches its message limit or stays idle longer than the idle timeout. Each message resets the idle timer. After either event, the visitor's next message fails and the widget must start a new session.

Client tokens have no daily cap. A test token and a live token get the same limits and the same defaults.

To change the limits in the workbench, set **Limits** in **Token defaults** for new tokens, or open a token's **Token settings** to change one token. To copy the default limits onto every active token, click **Apply limits to active tokens**. From code, pass the fields to `Runtype.clientTokens.update`:

**`SDK`**

```typescript title="SDK"
await Runtype.clientTokens.update('ct_01jz...', {
  rateLimitPerMinute: 30,
  rateLimitPerHour: 500,
})
```

### How the rate limits count requests

The per-minute and per-hour limits apply when you call a Surface's chat endpoint, `POST /v1/products/PRODUCT_ID/surfaces/SURFACE_ID/chat`, or its AG-UI endpoint, `POST /v1/products/PRODUCT_ID/surfaces/SURFACE_ID/ag-ui`, with the token. `PRODUCT_ID` and `SURFACE_ID` are the IDs of your Product and chat Surface.

Messages from the embedded chat widget do not count against these two limits. The widget's messages count against **Messages per session** instead.

Runtype counts these limits as follows:

* **Fixed windows**: A count resets at the start of each clock minute and each clock hour. It does not reset 60 seconds after the first request.
* **Per token**: Each token has its own counts. Two tokens on the same Surface do not share a budget.
* **Rejected requests**: A request that exceeds the per-minute limit still counts toward the hour. A client that retries in a tight loop uses up its hourly budget.
* **Approximate**: Concurrent requests can pass slightly over a limit.

When a request exceeds a limit, the chat endpoint returns `429` with this body:

```json
{
  "error": "Rate limit exceeded",
  "code": "RATE_LIMITED",
  "retryAfter": 42
}
```

`retryAfter` is the number of seconds until the exceeded window resets. The response has no `Retry-After` header, so read the value from the body. The AG-UI endpoint returns the same `error` and `retryAfter` fields without `code`.

## Target a version or release alias

By default a client token names no version selector, so its runs resolve like any other run of that Agent that names no version. For more information, see [What runs when you do not name a version](/user-guide/agents/agent-versioning-and-publishing#what-runs-when-you-do-not-name-a-version).

A token can instead carry one of these targets, never both:

* **`targetAlias`**: a release alias that the token follows. Runtype resolves it again on every request, so deploying a new version at that alias changes what the token runs without changing the token.
* **`targetVersionId`**: one immutable agent version. Runtype never resolves it again, so the token keeps running that exact configuration until you change it.

To set a target in the workbench, open **Access & keys**, click the token's options menu, and select **Edit token settings**. In **Release**, select **Follows alias** and enter a **Release alias**, or select **Pin a version** and enter an **Agent version ID**. **Token defaults** sets the same choice for new tokens. From code, set `targetAlias` or `targetVersionId` through the [Client Tokens API](/api-reference/runtype-api/client-tokens/list-client-tokens), the SDK, the `create_client_token` and `update_client_token` MCP tools, or the CLI's `--target-alias <name>` and `--target-version-id <id>` options.

To make a token pick up a new Agent definition that you manage with [`Runtype.agents.ensure`](/user-guide/agents/manage-agents-as-code), deploy it to `live`, for example with `Runtype.agents.ensure(def, { deploy: { alias: 'live' } })`. This works for a token with no target and for a token with `targetAlias: 'live'`.

Know these rules before you set a target:

* `targetVersionId` requires the token to carry exactly one Agent ID, and that version must belong to that Agent.
* `targetAlias` requires an organization-scoped token. A personal-account token cannot follow an alias.
* An alias that names nothing returns a `404` that identifies the alias. It does not fall back to the live configuration.
* Targeting is not available for external or Claude Managed agents, which return `422`.
* Realtime voice runs the targeted version. It resolves the alias once when the call connects, so a call keeps its version if the alias moves during the call. The exception is a GPT-Live call whose client runs the turns itself: those turns go through chat and follow the alias each turn. Set `targetVersionId` to keep such a call on one version. A targeted token cannot also open an editor draft, and returns `422` with `selector_conflict`.
* A chat request that supplies its own `agent.alias` or `agent.versionId` must exactly restate the token's target, or the request is rejected with `400`. A token with no target accepts no selector.

> **Note**
>
> `pinToVersion` is a legacy widget setting and does not select an agent version. To pin a version, set `targetVersionId`.

## Update permissions on existing sessions

Runtype rechecks the token's current allowed origins on session requests. Chat messages and tool-result resumes also recheck that the session's Flow or Agent is still attached to the token.

If you remove a Flow, Agent, or allowed origin from a token, Runtype rejects the next message in any existing conversation that depends on it. Starting a new session does not restore access. To restore access, correct the token configuration and retry.

Keep the `native` entry when an integration intentionally sends no `Origin` header. Test tokens keep their automatic local-origin allowance.

## Troubleshoot rejected requests

When the widget loads but does not respond, check the browser console and the response body in the Network tab. The following table lists the errors that a client token request can return:

| Status | Error                              | Cause                                                                                        | Fix                                                                 |
| ------ | ---------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `401`  | `Invalid client token`             | The token value is wrong, or the token was regenerated or deleted.                           | Install the current value of an active token in the embed code.     |
| `403`  | `Client token is inactive`         | The token was revoked before the session started.                                            | Create a token, or install an active one, in the embed code.        |
| `403`  | `Client token is no longer active` | The token was revoked after the session started.                                             | Create a token, or install an active one, and start a new session.  |
| `403`  | `Origin not allowed`               | The page origin is missing from the token's list or from the Surface's list.                 | Add the exact page origin to the list that rejects it.              |
| `403`  | `Chat surface is not active`       | The Surface is a draft, or it is paused and the request starts a new conversation.           | Activate the Surface.                                               |
| `400`  | `Flow not allowed for this token`  | The request names a Flow that is not attached to the token.                                  | Attach the Flow to the token, or send a Flow that the token lists.  |
| `401`  | `Session not found or expired`     | The session passed its idle timeout.                                                         | Start a new session.                                                |
| `429`  | `Session message limit reached`    | The session reached the token's messages-per-session limit.                                  | Start a new session, or raise **Messages per session**.             |
| `429`  | `Rate limit exceeded`              | A direct call to the chat or AG-UI endpoint passed the token's per-minute or per-hour limit. | Wait `retryAfter` seconds, or raise **Per minute** or **Per hour**. |

If none of these errors appear, confirm that the token still lists at least one Flow or Agent and, if the token belongs to a Surface, that the Surface has at least one enabled Flow or Agent.

## Regenerate a client token

If someone exposes a client token, or you lose a live token's value, regenerate it. Regeneration keeps the token's capabilities, allowed origins, and environment while issuing a new token value. The previous value stops working immediately.

To regenerate a token in the workbench, follow these steps:

1. Open **Access & keys** and click **Regenerate** on the token row.
2. Click **Regenerate token** to confirm.
3. Copy the new value from the **Save this token now** dialog, and update the widget embed code with it.

From code, call `Runtype.clientTokens.regenerate(id)` or run `runtype client-tokens regenerate <id>`. The response's `token` field shows the new value.

## Revoke or delete a client token

Revoke a token to stop every widget that uses it while keeping the token record. In the workbench, open **Access & keys**, click **Revoke** on the token row, and then click **Revoke token**. The row shows **revoked**. From code, set `isActive` to `false` with `Runtype.clientTokens.update(id, { isActive: false })` or the [Client Tokens API](/api-reference/runtype-api/client-tokens/list-client-tokens).

To remove a token permanently, delete it with `Runtype.clientTokens.delete(id)`, `runtype client-tokens delete <id>`, the `delete_client_token` MCP tool, or the API. Unlike regeneration, revoking or deleting a token does not issue a replacement value.

## Next steps

* [Embedding the chat widget (script tag)](/user-guide/products-surfaces/embedding-the-chat-widget-script-tag): add the selected client token to a website
* [Setting up a chat Surface](/user-guide/products-surfaces/setting-up-a-chat-surface): create and configure the Surface that hosts the widget
* [Conversation history in an embedded widget](/developer-guides/guides/client-conversation-history): let visitors return to earlier conversations
* [End-user identity and credentials](/developer-guides/guides/end-user-identity): tie widget conversations to your signed-in users
* [Client Tokens API reference](/api-reference/runtype-api/client-tokens/list-client-tokens): create, update, and regenerate tokens through the API