Developers

Whissle Voice Agents API

Create and run AI voice agents programmatically — manage agents, read call logs and transcripts, start voice sessions, and embed an agent in your own app. REST over HTTPS, JSON in and out, authenticated with an API key.

Build a voice agent appguide · v2A complete working application in about five minutes — agents declared in one file, live calls with or without a talking face, transcripts and scores. Versioned, and the code is public.

Overview

The API is workspace-scoped: every request acts within one workspace, and usage is billed to that workspace's wallet. All requests go through the Whissle gateway:

https://aws-gateway-backend.whissle.ai/bot

All endpoints below are relative to the base URL (e.g. GET /api/agents https://aws-gateway-backend.whissle.ai/bot/api/agents). Responses are JSON; timestamps are ISO-8601 UTC; IDs are UUIDs.

Cloud API — we host it

Point at aws-gateway-backend.whissle.ai/bot with a workspace key and start calling. This reference is the Cloud API. Nothing to deploy.

Host it yourself

Run the gateway (or just the GPU models) on your own infrastructure — same API, your own base URL. Self-host guide →

Migrate from another voice-AI platform

Moving off another voice-AI platform? Everything a conversational-AI agent does — prompt, greeting, voice, client & server tools, a knowledge base, an embeddable widget, and inbound/outbound phone — Whissle does too, plus its own speech recognition with emotion/intent metadata, real multilingual switching, and optional 3D avatars. You can recreate an agent in a few API calls; there is no separate runtime to stand up.

What maps to what

Your old platformWhissle
System promptsystem_prompt on the agent
First messagegreeting
Voicevoice / voice_gender (Whissle voices)
Client & server toolsbuilt-in tools + custom HTTP tools
Webhookscustom HTTP tool + post-call webhook tool
Knowledge baseper-agent KB (upload / URL), RAG built in
Widgetembeddable snippet (publishable key)
Twilio phone numberconnect Twilio → attach a number
Batch callingcampaigns

1. Get an API key

Create one in Settings → API keys and fund your wallet (sessions are metered, like the per-minute billing you're used to). See Authentication.

2. Re-create the agent

Port the prompt and first message straight across:

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/agents \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Support",
    "system_prompt": "You are Acme'\''s support agent. Be concise and warm...",
    "greeting": "Hi, thanks for calling Acme — how can I help?",
    "agent_type": "customer_support",
    "language_mode": "auto"
  }'

The LLM is managed by Whissle (there is no per-agent model field) — for most teams that is one less key to hold. Everything else about the agent is a field you can set now or PATCH later.

3. Bring your tools & knowledge

Map each existing tool to a built-in tool, or register your own API as an HTTP tool — the secret lives in a stored credential, never in the tool spec:

# register your API as a tool, then attach it to the agent
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/orgs/$ORG/tools \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "lookup_order", "kind": "http",
    "description": "Look up an order by its number.",
    "parameters": {"type":"object","properties":{"order_id":{"type":"string"}},"required":["order_id"]},
    "binding": {"url":"https://api.acme.com/orders/{order_id}","method":"GET"},
    "credential_id": "$CRED"
  }'

Bring your help-centre content with POST /api/agents/{id}/kb/upload (a file) or /kb/from-url (a page) — see Knowledge base. Retrieval is automatic in-call and returns citations.

4. Put it where your users are

  • Website — in the agent’s Actions → Embed & SDK panel, list your site, turn the widget on, and paste the one-line iframe it gives you.
  • Phone — connect Twilio and attach a number for inbound; place outbound with POST /api/calls/start.
  • Your own app — start a WebRTC session and drive it with the Whissle JS SDK.

Already using just a TTS API? It's a smaller change — just point your text-to-speech calls at /api/models/tts and keep the rest of your stack.

Authentication

Create a key in Settings → API keys (owner/admin). The secret is shown once — store it securely. Send it as a bearer token on every request:

curl https://aws-gateway-backend.whissle.ai/bot/api/agents \
  -H "Authorization: Bearer wsk_live_xxxxxxxxxxxxxxxxxxxx"

Keys are secret (wsk_…, server-side) — never expose them in a browser or commit them to source control. A key inherits the role of the member who created it and is scoped to a single workspace. Revoke a compromised key any time from the same settings page; it stops working immediately.

API keys

There are two key types. A secret key (wsk_…) is server-side and full-access — it authenticates the REST API in these docs. A publishable key (wpk_…) is domain-restricted and safe to ship to a browser (it backs the embed widget and @whissle/agents).

You can manage keys programmatically, scoped to your workspace at /api/orgs/{org_id}/api-keys:

# List your workspace keys (metadata only — never the secret). Scope: keys:read
curl https://aws-gateway-backend.whissle.ai/bot/api/orgs/{org_id}/api-keys \
  -H "Authorization: Bearer wsk_live_xxxxxxxxxxxxxxxxxxxx"

# Mint a publishable (wpk_) key for a browser/embed. Scope: keys:publish
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/orgs/{org_id}/api-keys \
  -H "Authorization: Bearer wsk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"type": "publishable", "name": "website widget"}'
# → the wpk_ secret is returned ONCE; store it.

A wsk_ key can only mint publishable (wpk_) keys, and only within its own scopes — it can never mint another full-access secret key, so a leaked key can't escalate. Minting a new wsk_, revealing a key, and rotating or revoking one are dashboard-only (owner/admin, in Settings → API keys) — a compromised key must not be able to read, replace, or revoke its siblings.

Errors & rate limits

Standard HTTP status codes:

  • 200/201 — success
  • 400 — invalid request body
  • 401 — missing / invalid / revoked API key
  • 403 — authenticated but not permitted (role/scope)
  • 402 — insufficient wallet balance (top up to continue)
  • 404 — resource not found (or not in your org)

Errors return { "detail": "message" }. Cross-tenant lookups return 404 (never 403) so resource existence isn't leaked.

Agents

An agent is a configured voice persona: prompt, greeting, voice, language, tools, avatar.

GET/api/agents

POST/api/agents

GET/api/agents/{id}

PATCH/api/agents/{id}

DELETE/api/agents/{id}

Create an agent:

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/agents \
  -H "Authorization: Bearer $WHISSLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support Agent",
    "system_prompt": "You are a friendly support agent for Acme.",
    "greeting": "Hi! Thanks for calling Acme — how can I help?",
    "agent_type": "customer_support"
  }'

Useful fields: voice, voice_gender, avatar_id (e.g. F1-HR), video_enabled, variables (template vars), tools, direction. Agent types: general, customer_support, debt_collection, appointment_scheduling, lead_qualification, survey_feedback, and more (see GET /api/agent-types).

Calls & transcripts

Every voice session (phone, browser, or API-driven) is recorded as a call.

GET/api/calls

GET/api/calls/{id}

GET/api/calls/{id}/audio/url

List recent calls, filter by agent:

curl "https://aws-gateway-backend.whissle.ai/bot/api/calls?agent_id=$AGENT_ID" \
  -H "Authorization: Bearer $WHISSLE_KEY"

A call includes status, duration_sec, the full transcript (array of { role, content }), and analysis (emotion, intent, disposition, summary). /audio/url returns a signed link to the recording.

Voice sessions (WebRTC)

Real-time voice runs over WebRTC. Your client posts an SDP offer; the server returns an answer and a pc_id. The Whissle JS SDK handles the handshake for you, or post the offer yourself:

POST/api/offer?agent_id={id}

PATCH/api/offer

POST /api/offer?agent_id=<AGENT_ID>&avatar_id=F1-HR
Authorization: Bearer $WHISSLE_KEY
Content-Type: application/json

{ "sdp": "<offer SDP>", "type": "offer" }
# → { "sdp": "<answer SDP>", "pc_id": "..." }

Optional query params: avatar_id (3D avatar), video=1, customer_id (renders prompt templates against a customer), greeting=cached (instant pre-rendered greeting clip), and stt_provider/tts_provider overrides.

Models (à la carte)

Use Whissle's models directly — no agent required. One API for language, speech-to-text, and text-to-speech, metered into your workspace wallet. Keys need the models:invoke scope (included by default).

POST/api/models/chat

POST/api/models/transcribe

POST/api/models/tts

LLM — chat completion:

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/models/chat \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{
    "messages": [
      { "role": "system", "content": "You are concise." },
      { "role": "user", "content": "Summarize the Kubernetes control plane." }
    ],
    "max_tokens": 512,
    "fast": false
  }'
# → { "text": "...", "usage": { "input_tokens": 42, "output_tokens": 118 },
#     "cost_usd": "0.00002...", "latency_ms": 640 }

model is optional (a sensible default is used); set fast: true for the cheaper small-model tier.

Speech-to-Text — upload an audio file (multipart):

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/models/transcribe \
  -H "Authorization: Bearer $WHISSLE_KEY" \
  -F "file=@call.wav" \
  -F "language=" -F "model=en-in-tech-misc" -F "diarize=false"
# → { "text": "...", "duration_seconds": 12.4, "cost_usd": "0.00124" }

Set diarize=true for speaker labels (returns a segments array, billed at the diarization rate).

Text-to-Speech — returns audio/mpeg bytes:

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/models/tts \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "text": "Hello from Whissle." }' \
  --output hello.mp3
# response headers: X-Cost-USD, X-Characters

Rates (USD): LLM $0.05/$0.19 per 1M input/output tokens (fast $0.03/$0.12); STT $0.006/min ($0.009 with diarization); TTS $18 / 1M characters. See Pricing for the full table.

Customers

Customer records personalize an agent's prompt (name, due amount, language, …).

GET/api/customers

POST/api/customers

POST/api/customers/import

PATCH/api/customers/{id}

DELETE/api/customers/{id}

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/customers \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Rahul Gupta", "phone_number": "+91...", "preferred_language": "hi-en" }'

Appointments

Configure how a booking agent schedules — operating hours, days it won't book, and the connected calendar. The bookings themselves are created by the agent's book_appointment tool during a call or chat and land on your connected calendar; this API is the configuration around that. All endpoints are workspace-scoped (appointments:read / appointments:write).

GET/api/orgs/{org_id}/appointments

PATCH/api/orgs/{org_id}/appointments

GET/api/orgs/{org_id}/appointments/hours

PUT/api/orgs/{org_id}/appointments/hours

GET/api/orgs/{org_id}/appointments/blocked-dates

POST/api/orgs/{org_id}/appointments/blocked-dates

DELETE/api/orgs/{org_id}/appointments/blocked-dates/{blocked_id}

GET/api/orgs/{org_id}/appointments/calendar

# Set the agent's operating hours (Mon–Fri 9–17)
curl -X PUT https://aws-gateway-backend.whissle.ai/bot/api/orgs/$ORG/appointments/hours \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "hours": [ {"day": "mon", "open": "09:00", "close": "17:00"} ] }'

Connecting a calendar: Google Calendar, Calendly and Cal.com are connected from Settings → Appointments (an OAuth sign-in), not over the API — a leaked key must not be able to rewire where your bookings go. GET …/calendar reports the current connection.

Knowledge base

Attach documents to an agent for grounded answers (RAG over your content).

GET/api/agents/{id}/kb

POST/api/agents/{id}/kb

POST/api/agents/{id}/kb/upload

POST/api/agents/{id}/kb/from-url

Add a snippet, upload a PDF/TXT, or ingest a URL; documents are chunked and embedded.

Session history (voice + text)

One unified history of everything an agent handled — phone calls, browser and embed voice, and text threads. Every item carries a kind discriminator (voice | text). (Distinct from starting a voice session above — this is the record of finished ones.)

GET/api/sessions

GET/api/sessions/{id}

GET/api/sessions/{id}/trace

List is newest-first and filterable: agent_id, kind (voice|text), limit (1–200), offset, and since. The /trace endpoint returns the per-turn timeline — which provider/model answered (and whether it failed over), every tool call with its args and citations, latency, token cost, and any action-integrity catches. Requires calls:read.

curl "https://aws-gateway-backend.whissle.ai/bot/api/sessions?kind=text&limit=20" \
  -H "Authorization: Bearer $WHISSLE_KEY"
# -> { "items": [ { "id": "...", "kind": "text", "agent_id": "...", ... } ],
#      "total": 137, "totals": { "voice": 41, "text": 96 } }

Tools (org-custom)

Give an agent a tool that calls your own HTTP API. A custom tool is defined once on the org and attached to any agent by name — the same tool can back many agents. (The agent also has built-in tools and, if you connect one, tools bridged from an MCP server.)

GET/api/orgs/{org}/tools

POST/api/orgs/{org}/tools

PUT/api/orgs/{org}/tools/{tool_id}

DELETE/api/orgs/{org}/tools/{tool_id}

POST/api/orgs/{org}/tools/{tool_id}/attach

parameters is a JSON Schema the model fills to call the tool; the request is made from our servers behind an SSRF guard. Listing needs tools:read; create/update/delete/attach need tools:write.

# 1. define the tool on the org
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/orgs/$ORG/tools \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "lookup_order", "description": "Look up an order by its id",
        "kind": "http",
        "parameters": { "type": "object",
          "properties": { "order_id": { "type": "string" } },
          "required": ["order_id"] },
        "binding": { "method": "GET", "url": "https://api.yourshop.com/orders/{order_id}" } }'

# 2. attach it to an agent
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/orgs/$ORG/tools/$TOOL_ID/attach \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "agent_id": "$AGENT_ID" }'

Campaigns (outbound)

Launch an outbound campaign — a batch of calls (or emails) an agent makes to your contacts, paced and compliance-gated. Every dial runs through the same rails as an agent-initiated call: do-not-call suppression, consent, per-recipient frequency caps, quiet-hours, and a wallet credit check — all fail-closed, so a campaign never dials a suppressed or unfunded contact.

POST/api/campaigns

GET/api/campaigns

GET/api/campaigns/{id}

POST/api/campaigns/{id}/{action}

Create takes { name, agent_id, … }. Key fields:calls_per_hour (1–1000, default 60) paces the dialer; customer_ids selects contacts (omit to target every contact scoped to the agent); window_start/window_end (0–23) bound the dial hours — pass both or neither (one alone reads as no window). For a program, a future start_at (ISO) and/or recurrence with a timezone makes it scheduled (the launcher enqueues when due); stop_on_dispositions halts a contact's follow-ups once an outcome is reached.channel defaults to voice; set email with email_subject/email_body for an email campaign. The create and read responses carry live progress (enqueued/sent/failed/…).

{action} is pause, resume, or cancel. Reading needs campaigns:read; creating and controlling need campaigns:write.

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/campaigns \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "March renewals", "agent_id": "$AGENT_ID",
        "calls_per_hour": 40, "window_start": 9, "window_end": 18, "timezone": "America/New_York" }'
# -> { "id": "...", "status": "running", "enqueued": 212, "progress": { ... } }

Connectors & integrations (MCP)

Two ways to give an agent capabilities beyond its built-in tools, both org-scoped:

Connectors — the credential vault

Store a per-org integration secret (an EHR/fhir endpoint, an SMTP mailbox, a Google Sheet, …), encrypted at rest — the agent uses it without ever seeing the secret. A credential is org-wide by default, or pass agent_idto scope it to one agent.

GET/api/orgs/{org}/credentials

POST/api/orgs/{org}/credentials

GET/api/orgs/{org}/credentials/{id}

PUT/api/orgs/{org}/credentials/{id}

DELETE/api/orgs/{org}/credentials/{id}

POST/api/orgs/{org}/credentials/{id}/test

Create takes { kind, name, data, agent_id? }kindcategorizes the connector (e.g. fhir, smtp) and data holds the secret fields. /test verifies the connection before an agent relies on it. Reading needs connectors:read; writing and testing need connectors:write.

Integrations — connect an MCP server

Connect an external MCP server (from the curated catalog or a pasted URL) and its tools become tools your agents can call — one connection, the whole server's toolset.

GET/api/orgs/{org}/integrations/catalog

GET/api/orgs/{org}/integrations

GET/api/orgs/{org}/integrations/connected

POST/api/orgs/{org}/integrations

POST/api/orgs/{org}/integrations/{id}/connect

POST/api/orgs/{org}/integrations/{id}/oauth/start

Create takes { name, server_url, auth_mode } where auth_mode is none, bearer, apikey, or oauth (for bearer/apikey, include the token; for oauth, drive /oauth/start). /catalog lists the app-store cards; /connected lists what's live. Reading needs integrations:read; connecting needs integrations:write (owner/admin — a connected server acts on the org's identity).

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/orgs/$ORG/integrations \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "name": "Airtable", "server_url": "https://mcp.example.com/airtable", "auth_mode": "bearer" }'

API keys

There are two key types. A secret key (wsk_…) is server-side and full-access — it authenticates the REST API in these docs. A publishable key (wpk_…) is domain-restricted and safe to ship to a browser (it backs the embed widget and @whissle/agents).

You can manage keys programmatically, scoped to your workspace at /api/orgs/{org_id}/api-keys:

# List your workspace keys (metadata only — never the secret). Scope: keys:read
curl https://aws-gateway-backend.whissle.ai/bot/api/orgs/{org_id}/api-keys \
  -H "Authorization: Bearer wsk_live_xxxxxxxxxxxxxxxxxxxx"

# Mint a publishable (wpk_) key for a browser/embed. Scope: keys:publish
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/orgs/{org_id}/api-keys \
  -H "Authorization: Bearer wsk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"type": "publishable", "name": "website widget"}'
# → the wpk_ secret is returned ONCE; store it.

A wsk_ key can only mint publishable (wpk_) keys, and only within its own scopes — it can never mint another full-access secret key, so a leaked key can't escalate. Minting a new wsk_, revealing a key, and rotating or revoking one are dashboard-only (owner/admin, in Settings → API keys) — a compromised key must not be able to read, replace, or revoke its siblings.

SMS

Send SMS from your org's number and read the delivery, opt-out and consent records. These are org-scoped — {org} is your organization id (from whoami).

POST/api/orgs/{org}/sms/send

GET/api/orgs/{org}/sms/messages

GET/api/orgs/{org}/sms/opt-outs

DELETE/api/orgs/{org}/sms/opt-outs/{phone}

GET/api/orgs/{org}/sms/consents

send takes { to_number, body, agent_id? } and always routes through the same compliance rails as an agent-initiated message: opt-out suppression (a suppressed recipient returns 403), duplicate suppression, a per-recipient burst cap, a per-message wallet debit, and the delivery log — it never goes straight to the carrier. agent_id sends from the number assigned to that agent; omit it to use the org default. Sending spends credit. messages takes limit (1–500, default 100);consents takes limit (1–1000, default 200).

Reading (messages, opt-outs, consents) needs sms:read; sending and removing an opt-out need sms:write.

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/orgs/$ORG/sms/send \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "to_number": "+15551234567", "body": "Your appointment is confirmed for Tue 3pm." }'
# -> { "sid": "SM...", "status": "queued", "to": "+15551234567" }

Compliance & do-not-call

The org's Do-Not-Call list, calling rules, enforcement audit trail, and the data-erasure endpoint — the API behind every automated call and message. Org-scoped; {org} is your organization id.

GET/api/orgs/{org}/compliance/suppressions

POST/api/orgs/{org}/compliance/suppressions

DELETE/api/orgs/{org}/compliance/suppressions/{phone}

GET/api/orgs/{org}/compliance/settings

PUT/api/orgs/{org}/compliance/settings

GET/api/orgs/{org}/compliance/events

POST/api/orgs/{org}/compliance/erase

GET/api/orgs/{org}/compliance/readiness

suppressions is the Do-Not-Call list: POST a { phone_number, reason? } to block a number, DELETE to re-enable it. settings holds the calling rules — an org that has never configured anything gets safe defaults (consent + disclosure required), never permissive ones. events is the enforcement audit log: what the rules actually did, per call. erase deletes everything held about one person on request (GDPR/CCPA). readiness lists everything still standing between the org and safe autonomous calling.

Reading (suppressions, settings, events, readiness) needs compliance:read; changing anything (POST/PUT/DELETE/erase) needs compliance:write and an owner/admin role.

curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/orgs/$ORG/compliance/suppressions \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "phone_number": "+15551234567", "reason": "customer requested no contact" }'
# -> 201 Created

Post-call webhooks

Get notified when a call finishes. Each agent can carry a guaranteed completion webhook — the outcome dispatcher fires it for every completed call, with the same payload the LLM-invoked webhook tool sends.

It is an agent config field, not a separate endpoint — set completion_webhook on a PATCH /api/agents/{id}:

curl -X PATCH https://aws-gateway-backend.whissle.ai/bot/api/agents/$AGENT_ID \
  -H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
  -d '{ "completion_webhook": {
          "url":     "https://your-app.example.com/whissle/call-finished",
          "secret":  "whsec_...",           // optional: HMAC-signs the request body
          "headers": { "X-Env": "prod" },   // optional: extra request headers
          "slim":    false                  // true = summary only, no transcript/recording
       } }'

The url is SSRF-validated when you set it — a private, loopback or metadata address is rejected with 422 at PATCH time, not at fire time. Send null or {} to clear it; setting it needs agents:write. The delivery carries the call's transcript, a signed recording_url, summary, disposition and structured result — the same object session history exposes. slim: true ships the summary only (no transcript or recording); an optional secret HMAC-signs the body so you can verify authenticity.

Usage & billing

Voice agents are a flat $0.04 / minute, all-inclusive (STT + LLM + TTS + avatar) — across phone, browser, API, and embedded sessions, debited when a session finalizes. À-la-carte model calls (/api/models/*) are metered per unit at the rates in the Models section. Both draw down the same prepaid wallet.

If the balance is empty, session-start returns 402 Payment Required — top up in Settings → Billing & usage (Stripe). Every debit and top-up is recorded in the wallet ledger with a running balance.

Embedding — put the agent on your site

Live today, no backend and no code beyond one <iframe>. In the agent’s Actions → Embed & SDK panel:

  1. Add each website that may run the agent (one origin per line, e.g. https://acme.com).
  2. Toggle Widget is live on.
  3. Copy the snippet it reveals and paste it into your page:
<iframe src="https://whissle.ai/embed/<YOUR_EMBED_KEY>"
        style="width:400px;height:600px;border:0;border-radius:16px"
        allow="microphone; geolocation" title="AI agent"></iframe>

The embed key is public (it sits in your page source), so the allowed-sites list is the security boundary: the backend only mints a session token for an origin you listed, so no one else’s site can spend your balance. A text agent renders a chat box (no mic prompt); a voice agent asks for the microphone. Sessions bill to your workspace like any other.

Want to control the widget from your own UI instead of an iframe — custom button, your own layout, WebRTC in your app? Use the Whissle JS SDK (@whissle/agents) with a publishable key (wpk_…, domain-restricted, safe in a browser). A drop-in <script> loader (cdn.whissle.ai/widget.js) is on the roadmap; the iframe + SDK cover it today.

Docs · Whissle