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).
Releases & staging
Ship agent changes the way you ship code: edit into a draft, cut a staging version to rehearse against, then promote it to production — with promotion gates that must pass first. Production keeps answering calls untouched the whole time.
GET/api/agents/{id}/releases
POST/api/agents/{id}/releases/stage
POST/api/agents/{id}/releases/promote
POST/api/agents/{id}/releases/unstage
GET/api/agents/{id}/releases/events
GET …/releases returns the three environments (production · staging · draft), the promotion gates and whether staging is promotable. stage cuts production ⊕ draft as a new version and puts it on staging (validated exactly like a publish); promote moves staging to production, refusing while a required gate fails unless an owner/admin passes { "force": true, "reason": "…" }. Every stage records its version, and marked scenarios auto-run against it (see below), so the gate fills itself.
# Stage the current draft, then promote once gates pass
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/agents/$ID/releases/stage \
-H "Authorization: Bearer $WHISSLE_KEY"
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/agents/$ID/releases/promote \
-H "Authorization: Bearer $WHISSLE_KEY"Testing & simulations
Rehearse an agent against realistic callers before they are real. A scenario is a script (persona · goal · success criteria) plus optional deterministic assertions; a run plays it against the agent’s real assembled brain and a strict judge records pass/fail. Runs are metered as usage.
GET/api/agents/{id}/scenarios
POST/api/agents/{id}/scenarios
PATCH/api/agents/{id}/scenarios/{scenario_id}
DELETE/api/agents/{id}/scenarios/{scenario_id}
POST/api/agents/{id}/scenarios/generate
GET/api/agents/{id}/scenarios/templates
POST/api/agents/{id}/scenarios/templates/{pack}/apply
GET/api/agents/{id}/scenarios/{scenario_id}/stats
POST/api/agents/{id}/simulations/run
GET/api/agents/{id}/simulations
POST/api/agents/{id}/testbed/replay
Assertions are graded alongside the judge — a run passes only if the judge passed and every assertion passed. Types: agent_says (a phrase or /regex/, with negate for “must never say”), resolves (the caller’s goal handled within N turns) and max_turns. Attach them on a scenario’s assertions field.
Flakiness. simulations/run takes repeat (1–5) to run each scenario several times, and scenarios/{id}/stats reports pass-rate and a flakiness score (0 = every decided run agreed, 1 = an even split). Templates clone a starter pack (essentials · compliance · booking) onto an agent in one call. Mark a scenario required_for_promotion and it becomes a release gate, run automatically each time you stage.
Testbed. testbed/replay takes a short conversation (ending on a caller message) and returns the agent’s next reply under a chosen env (candidate · staging · draft · production); pass { "compare": true } to also get the production reply side by side.
# Run every scenario 3× against the staged candidate
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/agents/$ID/simulations/run \
-H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
-d '{ "env": "candidate", "repeat": 3 }'Clean re-runs. A POST /api/agents/{id}/chat/turn with no conversation_id resumes your open thread for that agent — so a second trial would inherit the first trial’s memory. To force a brand-new conversation, send { "new_conversation": true } on the first turn of each trial (or a fresh session_id per run). The SDK and CLI already start a fresh conversation per run; the studio Test panel does it automatically on “Start fresh.”
Guardrails
An agent’s safety-and-control inventory — resolved to its effective value — plus a per-agent content policy you can enforce on the live reply.
GET/api/agents/{id}/guardrails
GET …/guardrails is a read-model grouped by scope (agent · type · org · platform); each configurable item names the exact field to edit. The content policy lives on the agent as content_guardrails and is written through PATCH /api/agents/{id}:
curl -X PATCH https://aws-gateway-backend.whissle.ai/bot/api/agents/$ID \
-H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
-d '{ "content_guardrails": {
"enabled": true,
"never_say": ["diagnosis", "/account number is \\d+/"],
"on_violation": "I can'"'"'t help with that — let me connect you to a person.",
"redact_pii": true } }'A never_say match (case-insensitive phrase, or /regex/) replaces the whole reply with on_violation; redact_pii masks e-mail, phone and card numbers with [redacted]. Enforced on both text and voice.
Enterprise SSO
Let a workspace sign in through its own identity provider (OIDC). Sign-in is routed by e-mail domain; the ID token is verified against the provider’s JWKS and members are provisioned on first login. Admin endpoints are org-scoped.
GET/api/orgs/{org}/sso
POST/api/orgs/{org}/sso
PATCH/api/orgs/{org}/sso/{id}
DELETE/api/orgs/{org}/sso/{id}
The public sign-in surface — GET /api/auth/sso/discover?email=…, then /api/auth/sso/{id}/start and the fixed /api/auth/sso/callback — is what the login page drives; you configure the connection (issuer, client id/secret, allowed domains, default role, JIT) through the admin endpoints above. The client secret is never returned by the API.
Security audit log
An org-scoped, read-only trail of security-relevant events — sign-ins, SSO provisioning and account changes — redacted and member-scoped. Owner/admin only.
GET/api/orgs/{org}/audit
GET/api/orgs/{org}/audit/event-types
audit is cursor-paginated (pass before from the previous page); audit/event-types lists the event vocabulary so you can build a filter. E-mails are masked and secrets are never included.
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 to prefer the smaller, lower-latency model tier — a speed/quality knob, billed at the same token rates.
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.30/$2.50 per 1M input/output tokens; STT $0.006/min ($0.009 with diarization); TTS $30 / 1M characters. See Pricing for the full table.
Voices — the TTS catalog:
GET/api/voices
Every voice the platform can speak with — grouped by language (with display labels), each carrying its engine, gender and voice_id — plus the current TTS pricing, in one response. Use it to build a voice picker instead of hard-coding ids; any valid key works, no particular scope required. The voice/voice_gender fields on an agent accept what it lists.
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" }'Human-in-the-loop actions
Gate a sensitive tool behind a human decision. Set a per-agent action_policy (a field on PATCH /api/agents/{id}) and a live call to that tool defers instead of firing — the draft is queued as a pending action, the agent tells the user it is waiting, and the tool's result card carries the choice as affordances. The default policy (auto) is unchanged: without an explicit "approve", these tools run exactly as before.
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 '{ "action_policy": { "send_email": "approve", "send_sms": "approve" } }'Holdable today: send_email and send_sms. A held card's affordances are { id, label, kind, action_id, primary? } entries — kind is approve, reject, or choice (a pick-one variant; firing a choice approves it), and at most one entry per card is primary. Four ways to fire one, all landing in the same place:
- Tap — the buttons the chat widget and the JS SDK render on the card.
- HTTP —
POST /api/embed/card-action(below), authenticated by the embed session token. - Voice — during a live session, a
{ "t": "card-action", "d": { … } }message on the data channel (the SDK'sfireAffordance()picks the right door for you). - Saying it — a spoken or typed “send it” / “discard the second one” resolves through the built-in
respond_to_pending_actiontool, which binds ordinals to cards in the order they appeared and refuses anything ambiguous. A workspace member can also bulk-approve the team's Action Inbox conversationally (“approve the top 10”, capped at 50 per request).
POST/api/embed/card-action
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/embed/card-action \
-H "Content-Type: application/json" \
-d '{ "token": "<embed session token>", "action_id": "…", "affordance_id": "aff_…",
"disposition": "approve", "source": "tap" }'
# -> { "action_id": "…", "status": "executed", "disposition": "approve", "affordance_id": "aff_…" }disposition is approve | reject; source is tap | voice | gesture. The firing is session-scoped: the action is resolved by id, org, and the session identity on the verified token — never anything in the body — so a token can only ever fire affordances on cards its own session was shown (anything else is a 404). First write wins: an already-resolved action returns 409 with the standing status, so a card open on two screens renders the state instead of an error. Approving runs the real metered send, behind the same origin, key-rotation and wallet gates as a chat turn.
Every resolution — whatever surface fired it — is emitted to connected clients as a phase: "action" tool event ({ kind, phase, tool_call_id, affordance_id, action_id, disposition, status, source }), recorded on the session so restored threads and the trace show who decided and how, and landed in the org's actions queue:
GET/api/actions
GET/api/actions/count
POST/api/actions/{action_id}/approve
POST/api/actions/{action_id}/reject
POST/api/actions/bulk-approve
GET/api/actions/scheduled
POST/api/actions/scheduled/{sched_id}/cancel
The queue is also where post-call held actions (the original Action Inbox) live, and where an operator approves from the dashboard or whissle actions. Reading needs actions:read; deciding needs actions:write.
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" }'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.
Two of those rules are enforced in the call itself: with disclosure_required on and a disclosure_text set, phone calls speak the disclosure line out loud before the greeting — deterministically, not as an instruction the model could skip — and telephony sessions can carry a hard per-call duration cap that says goodbye and ends the call when it is reached.
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 CreatedAlerts
Self-service metric thresholds with a fired-event trail — “tell me when failed calls spike” as a rule the platform watches for you, instead of a dashboard someone has to remember to open. Rules are org-scoped and can be narrowed to one agent.
GET/api/alerts/options
GET/api/alerts/rules
POST/api/alerts/rules
PUT/api/alerts/rules/{rule_id}
DELETE/api/alerts/rules/{rule_id}
POST/api/alerts/rules/{rule_id}/test
GET/api/alerts/events
options lists the metrics you can watch and the comparators they take (above / below) — build a rule form from it rather than hard-coding the vocabulary. Call metrics: count, avg_duration_sec, success_rate, pickup_rate. Quality metrics, measured over your simulation runs (see Testing & simulations): scenario_pass_rate and scenario_flakiness — page an owner when an agent regresses in rehearsal, before a customer meets it. Each metric in options carries a source of call or scenario so the form can label the sample size (calls vs runs). A rule pairs a metric with a comparator and threshold over a window_hours window (default 24; min_calls guards against firing on tiny samples, cooldown_hours stops re-firing). The platform evaluates rules on a background loop and records each firing in events; /test runs the same measurement inline so you can sanity-check a rule against current data before waiting for a tick.
curl -X POST https://aws-gateway-backend.whissle.ai/bot/api/alerts/rules \
-H "Authorization: Bearer $WHISSLE_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Success rate dipped", "metric": "success_rate",
"comparator": "below", "threshold": 80, "window_hours": 24, "min_calls": 10 }'
# Did anything fire?
curl https://aws-gateway-backend.whissle.ai/bot/api/alerts/events \
-H "Authorization: Bearer $WHISSLE_KEY"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.06 / 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.
Platform status
Is it us or is it you? The public status page at whissle.ai/status is backed by an unauthenticated API you can poll from your own monitoring — no key, open CORS, edge-cached about 30 seconds:
GET/api/status
curl https://aws-gateway-backend.whissle.ai/bot/api/status
# -> { "page": { "name": "Whissle", "generated_at": "…" },
# "overall": "operational",
# "components": [ { "key": "api", "name": "Platform API",
# "status": "operational", "description": "…" }, … ],
# "uptime": { "api": [ { "date": "2026-06-05", "status": "operational" }, … ], … },
# "incidents": [ { "id": "…", "title": "…", "severity": "minor",
# "status": "monitoring", "component_keys": ["voice"],
# "started_at": "…", "resolved_at": null,
# "updates": [ { "at": "…", "status": "…", "text": "…" } ] } ] }Seven components (api, voice, text, speech, llm, telephony, studio), each operational | degraded | outage; overall is the worst of them. uptime carries exactly 90 per-day entries per component, oldest first (no_data where unsampled). incidents lists open incidents plus those resolved in the last 90 days, newest first — severity minor | major | critical, status investigating | identified | monitoring | resolved.
Breaking change if you called this path before: the authenticated setup/configuration snapshot that used to answer GET /api/status (telephony config, environment checks for the dashboard) moved to GET /api/status/setup. The bare path is now the public status payload above.
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. When a tool is held for approval, the widget renders the card's Approve/Discard buttons itself — see Human-in-the-loop actions.
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.