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.
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/botAll 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 platform | Whissle |
|---|---|
| System prompt | system_prompt on the agent |
| First message | greeting |
| Voice | voice / voice_gender (Whissle voices) |
| Client & server tools | built-in tools + custom HTTP tools |
| Webhooks | custom HTTP tool + post-call webhook tool |
| Knowledge base | per-agent KB (upload / URL), RAG built in |
| Widget | embeddable snippet (publishable key) |
| Twilio phone number | connect Twilio → attach a number |
| Batch calling | campaigns |
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— success400— invalid request body401— missing / invalid / revoked API key403— 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-CharactersRates (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 CreatedPost-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:
- Add each website that may run the agent (one origin per line, e.g.
https://acme.com). - Toggle Widget is live on.
- 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.