Skip to navigation

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.

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.

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.

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

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

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:

FieldWorkbench labelCLI flagDefaultRange
rateLimitPerMinutePer minute--rate-limit-per-minute101 to 1000
rateLimitPerHourPer hour--rate-limit-per-hour1001 to 10000
maxMessagesPerSessionMessages per session--max-messages1001 to 1000
sessionIdleTimeoutMinutesIdle timeout in minutes--session-timeout301 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
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:

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

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

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:

StatusErrorCauseFix
401Invalid client tokenThe token value is wrong, or the token was regenerated or deleted.Install the current value of an active token in the embed code.
403Client token is inactiveThe token was revoked before the session started.Create a token, or install an active one, in the embed code.
403Client token is no longer activeThe token was revoked after the session started.Create a token, or install an active one, and start a new session.
403Origin not allowedThe 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.
403Chat surface is not activeThe Surface is a draft, or it is paused and the request starts a new conversation.Activate the Surface.
400Flow not allowed for this tokenThe 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.
401Session not found or expiredThe session passed its idle timeout.Start a new session.
429Session message limit reachedThe session reached the token’s messages-per-session limit.Start a new session, or raise Messages per session.
429Rate limit exceededA 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.

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