Embed

Drop a live agent on any website in two lines

The <speechify-agent> web component is the shortest path from “I have an agent ID” to “my users are talking to it on my landing page”. One script tag, one element, no realtime plumbing to wire up.

1<script src="https://cdn.speechify.ai/widget/agents.js"></script>
2<speechify-agent agent-id="agent_01HS..."></speechify-agent>

That’s the whole integration for a public agent. Private agents use the same tag with a server-minted session token (see below).

The widget is served from cdn.speechify.ai. The older api.speechify.ai/v1/widget/agents.js URL keeps working and is not going away, so an existing embed needs no change, but new integrations should use the CDN, which is faster, cached at the edge, and the only place versions can be pinned.

A page with a strict content-security policy needs cdn.speechify.ai in script-src, and in connect-src both api.speechify.ai and the realtime wss:// host.

That realtime host is not a fixed value you can copy from here: it is returned as the url field of the session response, and it differs by environment and region. Read it from one real session response and allowlist that origin, or allowlist wss://*.livekit.cloud if your policy permits a wildcard. Getting this wrong fails late and confusingly - the script loads, the session request succeeds, and only the call itself is blocked.

Two modes

Public agent (direct embed)

1

Make the agent public

Flip the Public toggle on the agent’s Embed tab in the console.

2

Allowlist your origins

Add the domains the widget should work on to the origin allowlist.

3

Paste the snippet

Drop the two lines into your page.

1<script src="https://cdn.speechify.ai/widget/agents.js"></script>
2<speechify-agent agent-id="agent_01HS..."></speechify-agent>

The widget calls POST /v1/agents/{id}/sessions unauthenticated; the server verifies the Origin matches your allowlist before minting a session. Use this for marketing sites, demo pages, and anywhere the agent conversation is the product itself.

Security

  • Embed only works from origins you explicitly allowlist on the agent. An empty allowlist with the public toggle on means “any origin accepted” - intended for open demos; enable deliberately.
  • No subdomain wildcards. Add each origin exactly (e.g. https://app.example.com, https://www.example.com).
  • The session endpoint is per-IP rate-limited. Repeat abuse from one source is throttled without affecting legitimate users.
  • The agent owner is always the billed principal regardless of who triggered the session.

Hostname allowlist

An additional, narrower gate applied at session-create time for public agents. Set it in the console under Embed tab → Hostname allowlist, or via PATCH /v1/agents/{id} with the hostname_allowlist field.

  • Up to 10 entries, exact hostname match (no wildcards, no subdomain matching). Scheme and port are ignored; only the Origin header’s hostname is checked.
  • Empty or omitted means “no hostname enforcement”, so the session is minted as usual. Use this gate when you want the widget to work only from a known set of production hostnames, even though the agent is public.
  • When non-empty, a request whose Origin hostname is not in the list is rejected with 403.
  • Good hostnames: example.com, app.example.com. Not accepted: *.example.com, https://example.com, example.com:8080.

Private agent (server-minted token)

Keep the agent private, mint a short-lived session token on your backend with your API key, and pass it to the widget. The API key never reaches the browser.

$curl -X POST "https://api.speechify.ai/v1/agents/${AGENT_ID}/sessions" \
> -H "Authorization: Bearer $SPEECHIFY_API_KEY" \
> -H "Content-Type: application/json"

Attributes

AttributeRequiredDescription
agent-ideither this or session-token+session-urlPublic-agent mode. Widget calls the session endpoint directly.
session-tokenwith session-urlPrivate-agent mode. Pre-minted token from your backend.
session-urlwith session-tokenRealtime URL paired with the token.
user-identityoptionalOpaque identifier for the end-user; stamped onto the conversation for later lookup.
api-baseoptionalOverride the API origin. Defaults to https://api.speechify.ai.

Per-call overrides

One embed can serve many customer segments without minting new agents. These attributes are forwarded to POST /v1/agents/{id}/sessions each time the caller starts a call, so you can update them between calls (e.g. after a user logs in) and the next call picks them up.

The four override-* attributes below are forwarded by the widget but the server currently ignores them - forward-compatible scaffolding, not a working feature. Today only dynamic-variables (and the user-identity attribute above) affect the session.

AttributeDescription
override-promptReplace the agent’s system prompt for this call only.
override-first-messageReplace the first message the agent speaks when the call opens.
override-voice-idOverride the voice by voice ID.
override-languageOverride the conversation language (BCP-47, e.g. en-US).
dynamic-variablesJSON object string passed through to the session as dynamic_variables. The shape is your contract with the agent prompt, e.g. '{"customer_tier":"pro"}'.
1<speechify-agent
2 agent-id="agent_01HS..."
3 override-prompt="You are a support bot for Acme. Greet the caller by name."
4 override-first-message="Hi Ada, how can I help?"
5 override-voice-id="carly"
6 override-language="en-US"
7 dynamic-variables='{"customer_tier":"pro","account_id":"acme-42"}'>
8</speechify-agent>

Overrides only apply when the widget mints the session itself (public-agent mode, or when you omit session-token). If you pre-mint a session on your backend with session-token + session-url, bake the overrides into that call server-side - the widget has no authority to change an already-issued session and will log a console warning if you combine the two.

Visual

AttributeDescription
avatar-image-urlReplaces the default mic orb with an image (e.g. a bot avatar). Falls back to the mic icon if the image fails to load.
avatar-orb-color-1First gradient stop for the orb background. Any CSS color.
avatar-orb-color-2Second gradient stop for the orb background. Any CSS color.
1<speechify-agent
2 agent-id="agent_01HS..."
3 avatar-image-url="https://cdn.example.com/bots/ada.png"
4 avatar-orb-color-1="#6366f1"
5 avatar-orb-color-2="#ec4899">
6</speechify-agent>

Orb colors can also be set from external CSS via the --speechify-agent-orb-1 / --speechify-agent-orb-2 custom properties - see Styling.

Copy

All visible button text is overridable so the widget can speak any language. Each attribute is optional; defaults are in English.

AttributeDefaultWhen shown
start-call-textTalk to agentIdle state, before the call starts.
listening-textListening - tap to endAgent is listening to the user.
thinking-textThinking…Agent has received the user’s input and is processing (LLM phase, before audio playback starts).
speaking-textAgent speaking - tap to endAgent is speaking back.
end-call-textfalls back to listening/speaking defaultsUnified in-call label. Used for listening + speaking when you’d rather show a single “End call” label than per-state copy.
1<speechify-agent
2 agent-id="agent_01HS..."
3 start-call-text="Parler au support"
4 listening-text="À l'écoute - touchez pour terminer"
5 speaking-text="L'agent parle - touchez pour terminer">
6</speechify-agent>

Debug

Diagnose integration issues without leaking logs into production embeds. Off by default.

AttributeDescription
debugSurfaces the widget’s integration warnings and livekit-client info-level logs in DevTools. Default: silent.
1<speechify-agent agent-id="agent_01HS..." debug></speechify-agent>

The flag is global for the page lifetime - livekit-client’s logger is a module-level singleton. The first <speechify-agent> connect on the page sets the level; subsequent connects don’t override, so a host page using livekit-client directly stays free to re-tune via its own setLogLevel call.

Diagnostics

The widget reports its own lifecycle back to Speechify so failures that happen inside a visitor’s browser are diagnosable. Everything between “someone clicked the button” and “audio is playing” is invisible in a server log: a microphone the visitor declined, a browser that blocked autoplay, a bundle that never loaded, a visitor who gave up mid-connect. Those reports are what let support answer “why did this call not start” instead of guessing.

AttributeDescription
telemetrySet to off to stop the widget reporting anything. Also settable as a property: el.telemetry = "off". Default: on.
1<speechify-agent agent-id="agent_01HS..." telemetry="off"></speechify-agent>

What is reported: the widget’s own lifecycle events (mounted, call requested, connected, first audio, ended, and the failure codes), the build the bundle was served from, milliseconds since the widget mounted, the agent and conversation ids, and a random id that correlates the events of one page-load and is discarded when the page closes. Your site’s origin is read from the request header.

What is never reported: the page URL, the page title, page content, anything the visitor typed or said, any transcript, and any identifier that persists across visits or across sites. No cookie is set and no credential is sent.

The widget also stops reporting on its own when the visitor’s browser sends Global Privacy Control, and you can switch it off for every embed of an agent at once from the agent’s Widget tab in the console. Once the widget has seen that setting it remembers it locally, so the opt-out holds even if a later config request fails.

Events

The element emits CustomEvents you can listen for with addEventListener:

EventBubblesdetail
statusno"idle", "connecting", "listening", "thinking", "speaking", "ended", "error"
messageno{ role: "user" | "assistant", text: string, timestamp: number } for each finalised transcript turn
errornoThe underlying Error instance, carrying a failure code
speechify-agent:callyes (composed){ status: "listening" | "speaking" } - fires once per toggle cycle, the first time the session reaches an in-call status. Useful for analytics: log “user started a call” exactly once per session.
1<speechify-agent id="agent" agent-id="agent_01HS..."></speechify-agent>
2<script>
3 const el = document.querySelector("#agent");
4 el.addEventListener("status", (e) => console.log("status:", e.detail));
5 el.addEventListener("message", (e) => console.log(e.detail.role, e.detail.text));
6</script>

Because speechify-agent:call bubbles (with composed: true), you can delegate-listen on an ancestor - handy when the widget is rendered by a framework and you don’t have a stable reference to the element:

1<script>
2 document.addEventListener("speechify-agent:call", (e) => {
3 analytics.track("voice_agent_call_started", { status: e.detail.status });
4 });
5</script>

Failure codes

Every failure the widget raises is an Error carrying a stable code you can branch on, plus a message written for the visitor looking at the page rather than the developer reading a log.

PropertyDescription
codeThe stable widget code, from the table below. Branch on this.
messageEnd-user copy, already suitable for display. Wording may change; don’t match on it.
serverCodeThe platform error envelope’s code, when the failure came from an API response - e.g. spend_budget_exceeded. Absent otherwise.
statusThe HTTP status, when the failure came from an API response.

Codes raised before the call is up arrive on the startAgent rejection; codes raised after it arrive on onError. A failure never takes both paths.

CodeRaisedMeaning
mic_deniedbeforeThe visitor blocked the microphone prompt. Recoverable from their browser’s site settings.
mic_not_foundbeforeNo microphone is connected.
mic_busybeforeAnother application holds the microphone.
mic_unavailablebeforeThe microphone could not be acquired for any other reason.
insecure_contextbeforeThe page is served over http. Voice calls need https.
session_rejectedbeforeThe session endpoint refused the mint - out of balance, over a spend limit, origin not allowed, or rate limited. Read serverCode for which.
session_unavailablebeforeThe session endpoint could not be reached or returned a server error.
session_malformedbeforeThe session response was missing the realtime handles.
timeoutbeforeThe call took too long to start.
connection_lostafterThe call dropped once it was up.
audio_blockedafterThe browser blocked audio playback. The visitor must tap to enable sound.
unknowneitherThe failure was not raised by the widget - typically something the host page threw.

session_rejected is deliberately one code for a family of refusals: the specific answer is the platform’s, on serverCode, and new platform codes must not turn into new widget codes an integrator has to keep up with. Branch on code for what to show the visitor, read serverCode when you need the exact reason.

1try {
2 await startAgent({ agentId: "agent_01HS..." });
3} catch (err) {
4 if (err.code === "mic_denied") showMicHelp();
5 else if (err.serverCode === "spend_budget_exceeded") notifyOps();
6 else showMessage(err.message);
7}

Programmatic API

For React/Vue/Svelte apps that don’t want the default button UI, import the ESM bundle and call startAgent directly:

1import { startAgent } from "https://cdn.speechify.ai/widget/agents.mjs";
2
3try {
4 const handle = await startAgent({
5 agentId: "agent_01HS...",
6 onStatus: (s) => console.log("status:", s),
7 onMessage: (m) => console.log(m.role, m.text),
8 // Failures once the call is up: a dropped connection, audio the browser blocked.
9 onError: (err) => console.error("call failed:", err),
10 });
11
12 // Client tools attached to the agent route here:
13 handle.registerTool("navigate_to", (args) => {
14 window.location.hash = String(args.section);
15 });
16
17 await handle.setMicEnabled(false); // mute
18 await handle.stop(); // tear down
19} catch (err) {
20 // Failures while the call is starting: a refused microphone, a page served
21 // over http, a rejected session, a transport that never came up.
22 console.error("could not start the call:", err);
23}

A failure is delivered through exactly one of those two paths, never both: startAgent rejects for anything that goes wrong before the call is up, and onError fires only for failures after it. The <speechify-agent> element is unaffected - it raises its error event for both.

Styling

The component uses Shadow DOM so your page’s CSS can’t leak in. Light theming is exposed through CSS custom properties on the host:

1speechify-agent {
2 --speechify-agent-bg: #0a0a0a;
3 --speechify-agent-fg: #ffffff;
4 --speechify-agent-accent: #6366f1;
5 --speechify-agent-muted: #71717a;
6 --speechify-agent-radius: 9999px;
7}

Need more control than that? Use the programmatic API and build your own UI on top of the returned AgentHandle.

Pin a version

The snippet above tracks the current widget release, which is what most sites want: fixes reach your page without you editing anything, and the URL is cached for five minutes so a correction lands quickly.

If your change-control process needs a bundle that never moves, request an exact version instead:

1<script src="https://cdn.speechify.ai/widget/v0.3.6/agents.js"></script>

A versioned URL is immutable. Its bytes never change, it is cached for a year, and it is therefore safe to protect with subresource integrity:

1<script
2 src="https://cdn.speechify.ai/widget/v0.3.6/agents.js"
3 integrity="sha384-…"
4 crossorigin="anonymous"></script>

Pin the version you tested, and treat the pin as something you own. A pinned bundle is immutable and cached for a year, so it never receives a later fix - including fixes to connection handling, error surfacing, and retry recovery. Check back against the current release when you review your dependencies.

Versions follow semver. A pinned major is supported for 12 months after its successor ships. Requesting a version we do not serve returns 404 rather than quietly falling back to a different build, so a bad pin fails visibly in your staging environment instead of silently drifting in production.

Which build am I running?

Every loaded bundle publishes its own identity, which is the fastest way to answer a support question:

1window.__SPEECHIFY_WIDGET_VERSION__; // e.g. "0.3.6+cb94104"

Customize the widget in the console

The agent’s Widget tab in the console customizes how the on-site widget looks and gives you the copy-paste snippet. Settings there apply everywhere you embed the agent; the HTML attributes above still override them per page.

Widget customization on the agent detail page
The Widget tab on the agent detail page
  • Widget style - Pill (a collapsed “Talk to agent” that expands) or FAB (a floating action button).
  • Theme - Dark, Light, or Auto (follows prefers-color-scheme).
  • Avatar - an animated gradient Orb (pick two gradient stops) or a static Image.
  • Button copy - override the label for each conversation stage (max 120 chars; leave blank to keep the default).
  • Live transcript - render a scrolling transcript inside the widget panel.
  • Terms & conditions - an optional consent gate shown before the conversation starts.
  • Diagnostics - whether the widget reports anonymous failures back to Speechify. See Diagnostics for exactly what is and is not collected.

What’s next