> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed the web widget

> Drop a voice agent onto your marketing or support site — no phone number required.

The web widget gives your site visitors a click-to-talk conversation
with an AI agent, using the browser's microphone. It's a separate
JavaScript / React SDK with its own [SDK reference](/widget/overview)
— this guide focuses on the ThunderPhone-side setup the widget needs.

<Note>
  You can do all of this without cURL: the **Web Widgets** dashboard
  page (`/dashboard/web-widgets`) creates the widget, sets its mode
  and agent, manages allowed domains, and hands you the embed snippet.
</Note>

## Prerequisites

<Steps>
  <Step title="Create an agent">
    The agent whose prompt and voice will run the widget session. Set
    `widget_enabled: true` (the default).
  </Step>

  <Step title="Decide the routing mode">
    * `mode="agent"` — one static agent per key. Simplest.
    * `mode="webhook"` — your server picks the agent per visitor via a
      [`web.incoming` webhook](/webhooks/call-incoming). Use this for
      logged-in users, A/B tests, or per-page routing.
  </Step>

  <Step title="List the allowed domains">
    Publishable keys are origin-locked. You must name every hostname
    that will embed the widget. `localhost` / `127.0.0.1` are always
    allowed during local dev.
  </Step>
</Steps>

## Create a publishable key

<CodeGroup>
  ```bash Static agent theme={null}
  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 Dynamic via webhook theme={null}
  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":            "Support (dynamic)",
      "mode":            "webhook",
      "webhook_url":     "https://example.com/thunderphone/widget-hook",
      "allowed_domains": ["support.example.com"]
    }'
  ```
</CodeGroup>

The response includes a `key` starting with `pk_live_...`. **Publishable
keys are public by design** — safe to ship in your front-end bundle.
See [Publishable keys reference](/api-reference/publishable-keys) for
all fields.

<Warning>
  `allowed_domains` must contain at least one entry. `*.example.com`
  matches subdomains (e.g. `api.example.com`) but **not** the bare
  domain. Bare wildcards like `*` or `*.*` are rejected.
</Warning>

## Drop the widget on your site

Three integration options are covered in the
[widget SDK docs](/widget/overview):

<CardGroup cols={3}>
  <Card title="React component" icon="react" href="/widget/react">
    `<ThunderPhoneWidget publishableKey="pk_live_..." />`.
  </Card>

  <Card title="Headless hook" icon="circle-nodes" href="/widget/headless-hook">
    `useThunderPhone()` for custom UIs.
  </Card>

  <Card title="CDN script tag" icon="code" href="/widget/cdn-script-tag">
    `ThunderPhone.mount({...})` for non-bundler sites.
  </Card>
</CardGroup>

All three accept the same `publishableKey` and render the mic button
plus the in-call audio element.

Widget `context` is truncated at 12,000 characters (roughly 3,400
tokens of typical English text) and counts toward the
[prompt-size surcharge](/guides/billing-and-topups#prompt-size-surcharge).

## Widget-mode webhooks

When `mode="webhook"`, ThunderPhone calls your `webhook_url` on every
session start with a `web.incoming` payload. Return the agent
configuration you want to run for that visitor — it follows the same
[response schema](/webhooks/call-incoming#response-schema) as phone
calls:

```json theme={null}
{
  "prompt":  "You are a VIP concierge for Jane Doe.",
  "voice":   "john",
  "product": "storm-base",
  "tools":   [ /* per-customer tools */ ]
}
```

You can mix context from your own session (which customer is browsing,
which page they're on) into the prompt, and swap agents per rollout.

## Observe the sessions

Widget sessions show up in
[`GET /v1/calls`](/api-reference/calls#list-calls) with
`direction="widget"` — same transcript, recording, grading, and
billing as phone calls. Filter by `direction` to build a widget-only
dashboard.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Widget SDK reference" icon="window-maximize" href="/widget/overview">
    React / hook / CDN integration details.
  </Card>

  <Card title="Dynamic per-call config" icon="bolt" href="/guides/dynamic-call-config">
    Implement the `mode="webhook"` flow end-to-end.
  </Card>

  <Card title="Publishable keys reference" icon="key" href="/api-reference/publishable-keys">
    Every field on the key resource.
  </Card>

  <Card title="Mic sessions API" icon="microphone" href="/api-reference/mic-sessions">
    Skip the widget; drive LiveKit directly for custom UIs.
  </Card>
</CardGroup>
