Open in
Embed the web widget
Drop a voice agent onto your marketing or support site with a single script tag — visitors talk to it straight from the browser, 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 — this guide focuses on the ThunderPhone-side setup the widget needs.
Prerequisites
- Create an agent
The agent whose prompt and voice will run the widget session. Set
widget_enabled: true(the default). - Decide the routing mode
mode="agent"— one static agent per key. Simplest.mode="webhook"— your server picks the agent per visitor via aweb.incomingwebhook. Use this for logged-in users, A/B tests, or per-page routing.
- List the allowed domains
Name every hostname that will embed the widget. Browsers send the page's origin with each session request, so the list stops your key from working when someone copies it onto another site. It is not authentication: a script outside a browser can send any
Originheader.localhostis not allowed automatically in production; to test locally against the production API, addlocalhostto the list. - Choose the browser overrides
Decide which session settings your page may send. New keys accept none; see Browser overrides.
Create a publishable 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"],
"allowed_overrides": ["context"]
}'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"]
}'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 for
all fields.
Drop the widget on your site
Three integration options are covered in the widget SDK docs:
<ThunderPhoneWidget publishableKey="pk_live_..." />.
useThunderPhone() for custom UIs.
ThunderPhone.mount({...}) for non-bundler sites.
All three accept the same publishableKey and render the mic button
plus the in-call audio element.
Browser overrides
The widget can send four per-session settings from the page:
| Setting | Widget prop | What it changes |
|---|---|---|
context | context | Text from the page, appended to the agent prompt as reference |
variables | none (raw POST /v1/widget/session only) | Values for {{variable}} placeholders in the prompt |
voice | voice | The agent's voice for this session |
language | language | The language the session starts in |
Your publishable key is public, so anyone who has it can send these
settings, including from a script that fakes your domain. Each one is
therefore off until you allow it on the key: tick it under Browser
overrides when you create or edit the widget in the dashboard, or set
allowed_overrides through the
Publishable keys API. Allow only what
your page actually sends.
A session request that carries a setting the key does not allow fails
with 400 and names it:
{
"error": "override_not_allowed",
"fields": ["voice"],
"message": "This publishable key does not allow the browser to set `voice`. ..."
}The check applies in both modes. In webhook mode an allowed setting is
also forwarded to your web.incoming webhook (context arrives as
website_context). Keys created before this setting existed allow all
four, so existing embeds keep working; turn off what you don't use.
Widget context is truncated at 12,000 characters (roughly 3,400
tokens of typical English text) and counts toward the
prompt-size surcharge.
The agent is told to treat it as page content, not as instructions.
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 as phone
calls:
{
"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 with
direction="widget" — same transcript, recording, grading, and
billing as phone calls. Filter by direction to build a widget-only
dashboard.