---
title: "Publishable Keys"
description: "Create, list, and revoke the pk_live_ publishable keys that let the embeddable web widget start browser voice sessions with your agents from your own site."
---

Publishable keys (`pk_live_...`) are **public** credentials that the
embeddable web widget uses from the browser. Unlike secret API keys,
they are scoped to a specific agent (or a webhook that will dynamically
resolve one) and gated to a list of allowed origins — safe to ship in
your front-end code. The origin list stops casual reuse on other sites;
it is not authentication, since a non-browser client can send any
`Origin` header. That is why the session settings a browser may send
are off by default (see `allowed_overrides`).

Each key is associated with exactly one of:

* `mode = "agent"`: fixed binding to an agent id. Simplest setup.
* `mode = "webhook"`: the widget calls your
  [`call.incoming` webhook](/webhooks/call-incoming) to dynamically
  resolve an agent per visitor.

## Endpoints

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/v1/publishable-key` | List publishable keys |
| `POST` | `/v1/publishable-key` | Create a publishable key |
| `GET` | `/v1/publishable-key/{key_id}` | Retrieve a key |
| `PATCH` | `/v1/publishable-key/{key_id}` | Update name / allowed domains / mode / browser overrides |
| `DELETE` | `/v1/publishable-key/{key_id}` | Deactivate a key |
| `POST` | `/v1/publishable-key/{key_id}/transfer` | Copy or move to another org |

## Publishable key object

```json
{
  "id": 9,
  "name": "Marketing site (prod)",
  "key": "pk_live_abc123...",
  "key_prefix": "pk_live_abc",
  "allowed_domains": ["example.com", "*.example.com"],
  "is_active": true,
  "is_enabled": true,
  "mode": "agent",
  "agent_id": 12,
  "agent_name": "Customer Support Agent",
  "webhook_url": null,
  "allowed_overrides": ["context"],
  "max_call_duration_seconds": 1800,
  "created_at": "2026-04-20T18:24:10.113Z",
  "updated_at": "2026-04-20T18:24:10.113Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Key id |
| `name` | string | Display name |
| `key` | string | The raw `pk_live_...` value. Publishable keys are public, so the full value is retrievable at any time |
| `key_prefix` | string | First 12 chars for display (e.g. `pk_live_abc`) |
| `allowed_domains` | array of string | Origin allowlist — exact hostnames and `*.subdomain` wildcards. `localhost` is not allowed automatically in production; list it to test locally |
| `is_active` | boolean | `false` after deactivation; a deactivated key stops working |
| `is_enabled` | boolean | Temporary disable toggle (separate from `is_active`) |
| `mode` | string | `agent` or `webhook` |
| `agent_id` | integer \| null | Required for `mode="agent"` |
| `agent_name` | string \| null | Read-only convenience — the linked agent's name |
| `webhook_url` | string \| null | Required for `mode="webhook"` — overrides the org-level webhook |
| `allowed_overrides` | array of string | Session settings the browser may send to [`POST /v1/widget/session`](/widget/overview): any of `context`, `variables`, `voice`, `language`. A request carrying one that is not listed fails with `400 override_not_allowed`. `[]` for new keys; keys created before the setting existed allow all four |
| `max_call_duration_seconds` | integer \| null | Longest a widget session may run, in seconds: 60–21600, default 1800 (30 minutes). A session ends at this or at the agent's own max call duration, whichever is shorter. `null` uses the agent's (or webhook config's) own limit alone |
| `created_at`, `updated_at` | timestamp | |

---

## List publishable keys

<CodeGroup>
```bash cURL
curl https://api.thunderphone.com/v1/publishable-key \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```
</CodeGroup>

Returns an array of [Publishable key objects](#publishable-key-object).

---

## Create a publishable key

<CodeGroup>
```bash Agent-bound key
curl -X POST https://api.thunderphone.com/v1/publishable-key \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Marketing site (prod)",
    "mode": "agent",
    "agent_id": 12,
    "allowed_domains": ["example.com", "*.example.com"]
  }'
```

```bash Webhook-mode key
curl -X POST https://api.thunderphone.com/v1/publishable-key \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Dynamic widget",
    "mode": "webhook",
    "webhook_url": "https://example.com/thunderphone-webhook",
    "allowed_domains": ["example.com"]
  }'
```
</CodeGroup>

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | no | Defaults to `"Web Widget"` |
| `mode` | string | no | `agent` (default) or `webhook` |
| `agent_id` | integer | yes (if `mode="agent"`) | |
| `webhook_url` | string | yes (if `mode="webhook"`) | HTTPS URL |
| `allowed_domains` | array of string | **yes** | At least one entry required. See rules below |
| `is_enabled` | boolean | no | Defaults to `true` |
| `allowed_overrides` | array of string | no | Any of `context`, `variables`, `voice`, `language`. Defaults to `[]` (the browser may set none) |
| `max_call_duration_seconds` | integer \| null | no | 60–21600, default 1800 (30 minutes). `null` leaves only the agent's own limit |

### Allowed-domain rules

* An exact match: `example.com`
* A wildcard match: `*.example.com` (matches `api.example.com` but not
  bare `example.com`)
* `localhost` and `127.0.0.1` are not allowed automatically in
  production. List them to test a local page against the production API
* Bare wildcards (`*` or `*.*`) are rejected with `400`
* **At least one entry is required** — passing an empty array returns
  `400 allowed_domains: "At least one allowed domain is required."`

Returns `201 Created` with the new [Publishable key object](#publishable-key-object).
The `key` field is included on creation **and** is retrievable later
via `GET` — publishable keys are public by design.

---

## Update / delete

`PATCH` accepts any subset of `name`, `allowed_domains`, `mode`,
`agent_id`, `webhook_url`, `is_enabled`, `embed_config`,
`allowed_overrides`, `max_call_duration_seconds`. Switching between modes
requires providing the new required field (`agent_id` or
`webhook_url`).

`DELETE` sets `is_active=false` — the key stops working but remains
visible in list responses so you can audit prior deployments. There is
no hard delete.

---

## Transfer a publishable key

Copy or move a publishable key to another organization where you are an admin.

An `sk_live_` API key is limited to its own organization: with a key,
`target_org_id` must be the key's organization, and any other target
returns `403`. Copy or move between organizations from a signed-in session
(`Authorization: Token <session-token>` plus `X-ThunderPhone-Org` for the
source org), as below.

<CodeGroup>
```bash cURL
curl -X POST https://api.thunderphone.com/v1/publishable-key/9/transfer \
  -H "Authorization: Token YOUR_SESSION_TOKEN" \
  -H "X-ThunderPhone-Org: 42" \
  -H "Content-Type: application/json" \
  -d '{"target_org_id": 77, "mode": "copy"}'
```
</CodeGroup>

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `target_org_id` | integer | yes | |
| `mode` | string | yes | `copy` or `move` |
| `name` | string | no | Override the copy's name |

---

## Related

<CardGroup cols={2}>
  <Card title="Widget overview" icon="window-maximize" href="/widget/overview">
    How the widget uses `pk_live_` keys at runtime.
  </Card>
  <Card title="Dynamic webhook config" icon="bolt" href="/webhooks/call-incoming">
    Shape the `call.incoming` webhook must respond with in webhook mode.
  </Card>
</CardGroup>
