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 · v4A 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).

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-Characters

Rates (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's fireAffordance() 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_action tool, 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 Created

Alerts

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:

  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. 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.

Docs · Whissle