Skip to Content
Builder Docs
CRM, APIs & Knowledge Bases

Public Web Embeds (Voice & Chat)

Configure widget embed tokens, enforce allowed domains, and build custom website voice (WebRTC) or headless text chat experiences using Talkar's Public Embed APIs.

Public Web Embeds (Voice & Chat)

Talkar lets you embed any Published workflow directly onto a public website or customer portal in either Browser Voice (WebRTC) or Text Chat mode. Each workflow maintains a single active Embed Token (emb_...) configured from Workflow Settings (Configure Widget) or via /api/v1/workflow/{workflow_id}/embed-token.


1. Dashboard Configuration & Origin Security

From Workflow Settings -> Add to Website -> Configure Widget:

  • Widget Type (voice | chat): Enforced strictly on the backend per token—a token configured for chat rejects WebRTC signaling, and vice versa. For chat widgets, you can also configure the workflow's Chat Inactivity Timeout (60–10500 seconds, default 1800s / 30 min).
  • Embed Mode: Choose floating (corner widget), inline (container mount), or headless (custom UI wired directly to Talkar's Public Embed APIs below).
  • Allowed Domains (allowed_domains): Restrict requests via HTTP Origin (falling back to Referer). Leave empty to allow any origin, set "*", or supply exact hosts and wildcards (e.g., app.example.com, *.example.com). Blocked origins receive HTTP 403 (Domain not allowed).
  • Usage Caps & Expiry: Token-level usage_limit and expiration (expires_in_days, default 30 days on API creation) apply across initialized sessions; usage reservations are atomic at /init.

Managing Tokens via API

Authenticated organization endpoints (X-API-Key or session auth):

  • POST /api/v1/workflow/{workflow_id}/embed-token — Create or update the workflow's active embed token (allowed_domains, settings, usage_limit, expires_in_days).
  • GET /api/v1/workflow/{workflow_id}/embed-token — Fetch the active token metadata or null.
  • DELETE /api/v1/workflow/{workflow_id}/embed-token — Deactivate the token immediately.

Context variable sanitization on /init

Visitor metadata passed in context_variables to /api/v1/public/embed/init is sanitized automatically: max 50 keys, max 64-char key names (^[^.\s|{}]+$), total 8192-byte JSON budget, string values capped at 2000 chars, and reserved internal keys stripped (provider, runtime_configuration, etc.). In voice mode, WebRTC offer frames ignore call_context_vars (allow_client_context_vars=false)—always pass context upfront in /init.


2. Public Text Chat Embed Flow

All /api/v1/public/embed/... endpoints handle browser CORS automatically for allowed origins (reflecting Access-Control-Allow-Origin on both success and error responses) and require no API key in the browser.

Loading diagram...

Step-by-Step Headless Chat Client

  1. Initialize a 1-hour session (POST /api/v1/public/embed/init): In chat mode, Talkar reserves token usage, enforces a 5 req/s init rate limit per token (429), verifies org quota (402), and synchronously generates the Start Call opening greeting in chat_session.turns.
  2. Track revision for optimistic concurrency (POST /messages): Always pass the latest expected_revision (max message length 4000 chars; capped at 50 turns per session and 5 messages/sec per run). If two tabs race and you receive 409 Conflict, call GET /api/v1/public/embed/chat/{session_token} to resync revision and turns.
  3. End cleanly without extra quota (POST /end): Calling /end completes the run idempotently, uploads the transcript artifact, and triggers post-call QA and webhooks.
Minimal headless public chat integration
const API = 'https://api.example.com/api/v1/public/embed';

// 1. Start session
const initRes = await fetch(`${API}/init`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    token: 'emb_your_public_token',
    context_variables: { page_url: window.location.href },
  }),
});
if (!initRes.ok) throw new Error(`Embed init failed: ${initRes.status}`);
const { session_token, chat_session } = await initRes.json();
let revision: number = chat_session.revision; // includes initial greeting turn

// 2. Send a visitor message with optimistic revision lock
async function sendMessage(text: string): Promise<unknown> {
  const res = await fetch(`${API}/chat/${session_token}/messages`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text, expected_revision: revision }),
  });

  if (res.status === 409) {
    // Resync latest revision after a concurrent tab update, then retry once
    const syncRes = await fetch(`${API}/chat/${session_token}`);
    if (!syncRes.ok) throw new Error(`Resync failed: ${syncRes.status}`);
    const sync = await syncRes.json();
    revision = sync.revision;
    return sendMessage(text);
  }

  if (!res.ok) throw new Error(`Chat message failed: ${res.status}`);
  const updated = await res.json();
  revision = updated.revision;
  return updated;
}

// 3. End chat when the user closes the widget
const endRes = await fetch(`${API}/chat/${session_token}/end`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ expected_revision: revision }),
});
if (!endRes.ok) throw new Error(`End chat failed: ${endRes.status}`);

3. Public Voice (WebRTC) Embed Flow

When building a custom browser voice UI on top of Talkar's WebRTC embed endpoints:

  1. Inspect widget & TURN config (GET /api/v1/public/embed/config/{token}): Check turn_enabled and force_turn_relay.
  2. Create voice session (POST /api/v1/public/embed/init): Returns { session_token, workflow_run_id, widget_type: "voice", config }. (For voice embeds, org concurrency and quota checks execute when the WebRTC offer arrives.)
  3. Fetch TURN ICE servers (GET /api/v1/public/embed/turn-credentials/{session_token}): If turn_enabled is true, returns { username, password, ttl, uris } for your RTCPeerConnection iceServers list (503 if TURN is unconfigured).
  4. Open WebRTC signaling WebSocket (WS /api/v1/ws/public/signaling/{session_token}):
    • Server validates session expiry (1h), domain allowlist (Origin / Referer), and mode == "smallwebrtc" before accepting (closes with WebSocket code 1008 on rejection).
    • Send {"type": "offer", "payload": { sdp, type: "offer", pc_id }}, trickle ICE via {"type": "ice-candidate", "payload": { pc_id, candidate }}, and optionally send {"type": "dtmf", "payload": { digit }} (0–9, *, #, A–D) or {"type": "renegotiate", ...}.
    • Listen for server frames: answer, call-ended (after which the server closes with code 1000), and error (including concurrency_limit_exceeded, workflow_run_already_active, concurrency_service_unavailable, or quota error codes).

4. Public Embed Error Reference

All public chat and embed session endpoints strictly return allowlist-filtered responses—never exposing prompts, raw stack traces, checkpoint, or internal initial_context / gathered_context.

Status / Close CodeEndpointsMeaning
400/embed/chat/*{"detail": "Not a chat session"} (voice token used on chat route) or on /messages: {"detail": {"code": "chat_completed", "message": "Conversation has ended"}} (/end is idempotent on finished chats)
402/embed/init (chat), /messages"The agent is unavailable right now" (organization wallet or quota exhausted; WS voice sends {"type": "error", "payload": {"error_type", "message"}})
403/config, /init, /turn-credentials, /embed/chat/*Token inactive or origin disallowed (/init also enforces expires_at and usage_limit; session routes also enforce 1h session expiry)
404/config, /init, /turn-credentials, /embed/chat/*Invalid embed token, invalid session_token, or chat session not found
409/chat/{session_token}/messages, /end{"detail": {"message": "Text chat session revision conflict", "expected_revision": ..., "actual_revision": ...}}
429/embed/init (chat), /messagesRate limit (5 req/s per token on init or per run on messages) or 50-turn conversation limit reached
503/turn-credentials, /init, /messagesTURN server unconfigured on /turn-credentials, or upstream managed inference unavailable
WS 1008WS /api/v1/ws/public/signaling/{session_token}Invalid/expired session, non-voice session, or Origin/Referer rejected before handshake

On this page