Agents
Create and manage voice agents over REST: configure prompts, engines, voices, languages, tools, and knowledge, then deploy, duplicate, transfer, or version them.
An agent is the configuration that drives every ThunderPhone call — the product tier, the voice, the languages, the system prompt, the acknowledgement behavior, silence/voicemail handling, the attached tools and knowledge, and whether the agent is reachable from the web widget. Phone numbers assign an inbound and/or outbound agent; outbound calls and mic sessions can target an agent directly.
Agents use a draft / deploy workflow: edits made via PATCH/PUT
are saved as a draft and do not affect production calls until you
POST /deploy. Test calls and mic sessions run against the draft (when
one exists) so you can try changes before shipping them.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v1/agents | List agents |
POST | /v1/agents | Create an agent |
GET | /v1/agents/{agent_id} | Retrieve a single agent |
PUT / PATCH | /v1/agents/{agent_id} | Save changes to the agent's draft |
POST | /v1/agents/{agent_id}/deploy | Promote the draft to the deployed config |
POST | /v1/agents/{agent_id}/discard-draft | Throw away all pending draft changes |
DELETE | /v1/agents/{agent_id} | Delete an agent |
POST | /v1/agents/{agent_id}/duplicate | Duplicate an agent |
POST | /v1/agents/{agent_id}/transfer | Copy or move an agent to another org |
GET | /v1/agents/{agent_id}/versions | List deployed configuration revisions |
GET / POST | /v1/agents/{agent_id}/consent-attestations | List or record consent-announcement attestations |
POST | /v1/agents/prompt-language-check | Detect the language of a prompt |
POST | /v1/agents/translate-prompt | Translate a prompt into another locale |
POST | /v1/agents/generate-acknowledgement-prompt | Auto-generate a Storm-mode acknowledgement prompt |
Related agent-scoped surfaces documented on their own pages: test scenarios & suites, split testing (A/B variants).
Agent object
{
"id": 12,
"name": "Customer Support Agent",
"prompt": "You are a helpful support agent for Acme Ops…",
"voice": "john",
"primary_language": "en",
"additional_languages": [],
"product": "spark",
"thinking_level": "base",
"background_track": "NONE",
"acknowledgement_prompt_mode": "auto",
"acknowledgement_prompt": "",
"acknowledgement_feedback_mode": "acknowledgement",
"inbound_speak_order": "agent_first",
"outbound_speak_order": "caller_first",
"voicemail_action": "prompt",
"voicemail_message": "",
"silence_interval_seconds": 15,
"silence_max_checkins": 3,
"silence_checkins_enabled": true,
"silence_hangup_seconds": 600,
"connect_tone_enabled": false,
"play_sound_asset": "",
"speak_uninterruptible_enabled": false,
"consent_announcement_enabled": true,
"consent_announcement_text": "This call may be recorded…",
"consent_disabled_reason": "",
"audio_context_mode": "full",
"watchdog_enabled": false,
"additional_audio_context": null,
"integrations": [],
"knowledge_bases": [],
"knowledge_documents": [],
"mcp_servers": [],
"widget_enabled": true,
"split_testing_enabled": false,
"created_at": "2026-04-20T18:24:10.113Z",
"updated_at": "2026-04-20T18:24:10.113Z",
"draft_prompt": null,
"draft_voice": null,
"draft_consent_announcement_enabled": null,
"draft_updated_at": null,
"has_draft": false
}Deployed fields
| Field | Type | Description |
|---|---|---|
id | integer | Server-assigned agent id |
name | string | Human-readable name. 1–255 chars |
prompt | string | The system prompt that governs agent behavior |
voice | string | Voice name from GET /v1/voices — e.g. john, megan. Entitled organizations can also pass custom:<public_id> for a ready custom voice |
primary_language | string | ISO 639 code, e.g. en, es. Default en. Drives the default TTS locale |
additional_languages | array of string | Extra language codes the agent supports (es or BCP-47 es-ES) |
product | string | Product tier. One of: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack |
thinking_level | string | base or extra. Derived from product for Storm tiers |
background_track | string | Ambient audio track name, or "NONE" for silence. Never null — a null write is coerced to "NONE" |
acknowledgement_prompt_mode | string | auto (we generate the ack prompt) or manual (you supply it) |
acknowledgement_prompt | string | Used when acknowledgement_prompt_mode="manual". Storm-tier only |
acknowledgement_feedback_mode | string | While a Storm agent thinks: acknowledgement (spoken filler) or tick (ticking sound) |
inbound_speak_order | string | Who talks first on inbound calls. agent_first or caller_first |
outbound_speak_order | string | Who talks first on outbound calls |
voicemail_action | string | What to do when a voicemail system answers: prompt (let the agent decide), hangup, or message (leave a message) |
voicemail_message | string | TTS template for voicemail_action="message", max 2000 chars. Supports {agent_name} and {org_name} variables |
silence_interval_seconds | integer | Seconds between "are you still there?" check-ins. 10–120, default 15 |
silence_max_checkins | integer | Check-ins before the agent hangs up. 1–10, default 3 |
silence_checkins_enabled | boolean | Whether the agent proactively checks in during caller silence. Default true |
silence_hangup_seconds | integer | Caller-idle seconds before a silent hangup when check-ins are disabled. 60–3600, default 600 |
connect_tone_enabled | boolean | Play a short connection tone before the agent begins. Default false |
play_sound_asset | string | Prompt-callable sound asset: "" (none), chime, ding, or beep |
speak_uninterruptible_enabled | boolean | Offer the built-in tool for speaking a required passage without interruption. Default false |
consent_announcement_enabled | boolean | Play the configured AI/recording announcement at call start. Default true; disabling requires an active consent attestation where applicable |
consent_announcement_text | string | Announcement played verbatim at call start; must not be blank and is limited to 2000 characters in the dashboard |
consent_disabled_reason | string | Read-only platform-managed status associated with an announcement exception. Clients cannot set it |
audio_context_mode | string | Storm audio context window: full or reduced (lower latency, less prior audio) |
watchdog_enabled | boolean | Enables live supervision of agent turns (Storm). Default false |
additional_audio_context | boolean | null | Include the last few turns of caller audio, rather than only the most recent turn, to improve corrections and spelling/number-heavy data collection at a small latency/cost overhead. Defaults on for inbound and browser/WebSocket sessions and off for outbound phone calls; null keeps that default |
integrations | array | Linked Integration objects |
knowledge_bases | array | Attached knowledge bases (full objects) |
knowledge_documents | array | Individually attached knowledge documents |
mcp_servers | array | Attached MCP servers (full objects) |
widget_enabled | boolean | Whether the agent is reachable via the embeddable web widget |
split_testing_enabled | boolean | Whether calls are split across A/B variants |
created_at, updated_at | timestamp | ISO 8601 UTC |
Draft fields
Every configurable field above has a draft_* mirror in the response
(draft_name, draft_prompt, draft_voice, draft_primary_language,
draft_additional_languages, draft_product, draft_background_track,
draft_thinking_level, draft_acknowledgement_prompt_mode,
draft_acknowledgement_prompt, draft_acknowledgement_feedback_mode,
draft_inbound_speak_order, draft_outbound_speak_order,
draft_voicemail_action, draft_voicemail_message,
draft_silence_interval_seconds, draft_silence_max_checkins,
draft_silence_checkins_enabled, draft_silence_hangup_seconds, draft_connect_tone_enabled,
draft_play_sound_asset, draft_speak_uninterruptible_enabled,
draft_consent_announcement_enabled, draft_consent_announcement_text,
draft_audio_context_mode, draft_watchdog_enabled, draft_additional_audio_context,
draft_widget_enabled, draft_split_testing_enabled), plus draft id
lists for attachments (draft_integration_ids,
draft_knowledge_base_ids, draft_knowledge_document_ids,
draft_mcp_server_ids) and two read-only markers:
| Field | Type | Description |
|---|---|---|
draft_<field> | same as field | null | null = no pending change for that field; non-null = the value staged for the next deploy |
draft_updated_at | timestamp | null | When the draft was last edited |
has_draft | boolean | True when any draft field is set |
Production call paths (inbound, outbound, widget) always read the deployed fields. Mic sessions and simulations read the draft with fallback to the deployed value.
Product tiers at a glance
| Product | Optimized for | Acknowledgement |
|---|---|---|
spark | Cost — fast and efficient for simple tasks | — |
bolt | Speed — the lowest-latency tier, balanced for most use cases | — |
storm-base | Intelligence — complex prompts and reasoning | — |
storm-base-with-ack | Intelligence | Spoken filler (or tick) while thinking |
storm-extra | Deepest reasoning | — |
storm-extra-with-ack | Deepest reasoning | Spoken filler (or tick) while thinking |
List agents
curl https://api.thunderphone.com/v1/agents \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"agents = requests.get(
"https://api.thunderphone.com/v1/agents",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
).json()const agents = await fetch("https://api.thunderphone.com/v1/agents", {
headers: { Authorization: `Bearer ${process.env.THUNDERPHONE_API_KEY}` },
}).then((r) => r.json());Returns an array of Agent objects, sorted by created_at descending.
Create an agent
curl -X POST https://api.thunderphone.com/v1/agents \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Customer Support Agent",
"prompt": "You are a helpful support agent for Acme Ops…",
"voice": "john",
"product": "spark",
"widget_enabled": true
}'agent = requests.post(
"https://api.thunderphone.com/v1/agents",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
json={
"name": "Customer Support Agent",
"prompt": "You are a helpful support agent for Acme Ops…",
"voice": "john",
"product": "spark",
"widget_enabled": True,
},
).json()| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | 1–255 chars |
prompt | string | yes | System prompt |
voice | string | yes | Voice name from /v1/voices (e.g. john). Entitled organizations can also pass custom:<public_id> for a ready custom voice; anything else is rejected with "Unsupported voice option." |
primary_language | string | no | Defaults to en |
additional_languages | array of string | no | ISO 639 (es) or BCP-47 (es-ES) codes |
product | string | no | Defaults to spark |
thinking_level | string | no | Ignored for Storm tiers (derived from product) |
background_track | string | no | Track name or "NONE" |
acknowledgement_prompt_mode | string | no | auto or manual. Only meaningful for Storm-with-ack products |
acknowledgement_prompt | string | no | Required if acknowledgement_prompt_mode="manual" |
acknowledgement_feedback_mode | string | no | acknowledgement (default) or tick |
inbound_speak_order | string | no | Defaults to agent_first |
outbound_speak_order | string | no | Defaults to caller_first |
voicemail_action | string | no | prompt (default), hangup, or message |
voicemail_message | string | no | ≤ 2000 chars |
silence_interval_seconds | integer | no | 10–120, default 15 |
silence_max_checkins | integer | no | 1–10, default 3 |
silence_checkins_enabled | boolean | no | Default true |
silence_hangup_seconds | integer | no | 60–3600, default 600; used when check-ins are disabled |
connect_tone_enabled | boolean | no | Default false |
play_sound_asset | string | no | "" (none), chime, ding, or beep |
speak_uninterruptible_enabled | boolean | no | Offer the uninterruptible speech tool. Default false |
consent_announcement_enabled | boolean | no | Defaults to true. New agents must be created with it enabled; disabling later requires the attestation workflow where applicable |
consent_announcement_text | string | no | Non-blank call-start announcement text |
audio_context_mode | string | no | full (default) or reduced |
watchdog_enabled | boolean | no | Default false |
additional_audio_context | boolean | null | no | Override whether the last few caller-audio turns are included. null keeps the direction-based default |
widget_enabled | boolean | no | Defaults to true |
split_testing_enabled | boolean | no | Default false |
integration_ids | array of UUID | no | Link reusable integrations by id |
knowledge_base_ids | array of UUID | no | Attach knowledge bases |
knowledge_document_ids | array of UUID | no | Attach individual knowledge documents |
mcp_server_ids | array of UUID | no | Attach MCP servers |
Returns 201 Created with the new Agent object. A
create deploys immediately (there is no draft yet) and records revision
1 in the version history.
Retrieve an agent
curl https://api.thunderphone.com/v1/agents/12 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"agent = requests.get(
f"https://api.thunderphone.com/v1/agents/{agent_id}",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
).json()Returns 200 OK with an Agent object, or 404 if not found.
Update an agent (writes the draft)
PATCH and PUT do not modify the deployed configuration.
Every field you send is staged into the corresponding draft_* column;
production calls keep using the deployed values until you
deploy. Sending a value identical to the deployed
value clears that field's draft (so manually reverting an edit
removes the pending change).
curl -X PATCH https://api.thunderphone.com/v1/agents/12 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "You are a refined support agent…"}'agent = requests.patch(
f"https://api.thunderphone.com/v1/agents/{agent_id}",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
json={"prompt": "You are a refined support agent…"},
).json()Accepts the same fields as create, including the
integration_ids / knowledge_base_ids / knowledge_document_ids /
mcp_server_ids lists (staged as draft attachment lists). Returns
200 OK with the Agent object — inspect the
draft_* fields and has_draft to see what is pending.
Optimistic concurrency
To safely make edits from a UI, include if_updated_at (or
updated_at) with the last updated_at you observed:
{
"prompt": "Newer prompt",
"if_updated_at": "2026-04-20T18:24:10.113Z"
}If the server-side updated_at differs, the API returns 409 Conflict
with {"detail": "Agent was modified by another user. Please refresh and try again."}
so you can refresh and retry.
Deploy the draft
Promotes every pending draft_* value to the deployed configuration,
clears the draft, and records a new revision in the
version history.
curl -X POST https://api.thunderphone.com/v1/agents/12/deploy \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"agent = requests.post(
f"https://api.thunderphone.com/v1/agents/{agent_id}/deploy",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
).json()Returns 200 OK with the updated Agent object
(has_draft is now false). Deploying with no pending draft is a
no-op that still returns 200 with the current agent.
Discard the draft
Clears every pending draft field, reverting the editor state to the deployed configuration.
curl -X POST https://api.thunderphone.com/v1/agents/12/discard-draft \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"Returns 200 OK with the Agent object.
Consent attestations
GET /v1/agents/{agent_id}/consent-attestations ·
POST /v1/agents/{agent_id}/consent-attestations
Every agent plays a call-start consent announcement (recording and AI
disclosure) by default. Disabling it on a non-grandfathered agent requires
an active attestation: an admin's recorded affirmation that your
organization will provide any legally required notice and obtain any
legally required consent for its calls while the announcement stays off
(see the call recording consent laws
guide for what that involves). Without one, staging or deploying
consent_announcement_enabled: false returns 400.
Attestations are single-use per disable cycle: re-enabling the
announcement retires the active attestation (recorded with
status_reason: "superseded-by-reenable"), so the next disable needs a
fresh affirmation. ThunderPhone may also suspend or revoke attestations,
which restores the announcement at runtime. New attestations are recorded
only under the current attestation_text_version, but an active
attestation recorded under an earlier accepted version stays valid — a
statement-copy revision never re-prompts an organization that already
attested.
GET lists the agent's attestations (newest first) and returns the
current attestation_text_version and the server-owned canonical
statement package — statement_text, its statement_sha256, and the
statement_locale it resolved for your ?locale= (English until a locale
is registered). Display statement_text verbatim to the admin, then
POST with affirmed: true, the exact version, your locale, and
assent to the package: echo statement_sha256, or send the displayed
text as rendered_text (it must hash to the canonical value). The server
stores its canonical hash — blank or mismatched statement evidence is
rejected.
Step 1 — fetch the canonical statement:
curl https://api.thunderphone.com/v1/agents/12/consent-attestations?locale=en \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"attestation_text_version": "consent-disable-v4-2026-08-03",
"statement_locale": "en",
"statement_text": "Turn off the call-recording announcement?\n…",
"statement_sha256": "7a284954b61b6610d1f9fb43ec05e82e6aec4362956fa5409deb3375cfc99035",
"results": []
}Step 2 — display the statement, collect the affirmation, and record it:
curl -X POST https://api.thunderphone.com/v1/agents/12/consent-attestations \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"affirmed": true, "attestation_text_version": "consent-disable-v4-2026-08-03", "statement_sha256": "7a284954b61b6610d1f9fb43ec05e82e6aec4362956fa5409deb3375cfc99035", "locale": "en"}'{
"id": 91,
"organization_id": 7,
"agent_id": 12,
"user_id": 3,
"attestation_text_version": "consent-disable-v4-2026-08-03",
"status": "active",
"attested_at": "2026-08-03T23:02:11.480Z",
"status_updated_at": null,
"affirmed": true,
"attestation_text_sha256": "7a284954b61b6610d1f9fb43ec05e82e6aec4362956fa5409deb3375cfc99035",
"locale": "en",
"user_role": "admin",
"status_reason": ""
}| Status | Condition |
|---|---|
201 | Attestation recorded (POST) |
400 | affirmed was not true, attestation_text_version does not match the current version, locale is blank, or the statement evidence (statement_sha256/rendered_text) is blank or does not match the canonical package |
403 | Caller is not an org admin |
Delete an agent
curl -X DELETE https://api.thunderphone.com/v1/agents/12 \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"requests.delete(
f"https://api.thunderphone.com/v1/agents/{agent_id}",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
)Returns 204 No Content. Phone numbers that reference the deleted agent
have their inbound/outbound assignment cleared.
Duplicate an agent
Creates a new agent in the same org with the same deployed configuration: name (overridable), prompt, voice, languages, product, thinking level, background track, acknowledgement settings, speak orders, voicemail settings, silence settings, connect tone, audio context mode, watchdog, widget flag, and the attached integrations, knowledge bases/documents, and MCP servers. Pending drafts and split-testing variants are not copied.
curl -X POST https://api.thunderphone.com/v1/agents/12/duplicate \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Customer Support Agent (clone)"}'clone = requests.post(
f"https://api.thunderphone.com/v1/agents/{agent_id}/duplicate",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
json={"name": "Customer Support Agent (clone)"},
).json()| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | Defaults to "<original name> (Copy)" |
Returns 201 Created with the new Agent object.
Transfer an agent to another org
Copy or move an agent to a different organization you're also a member
of. For mode="move", admin+ role is required on both orgs.
curl -X POST https://api.thunderphone.com/v1/agents/12/transfer \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"target_org_id": 77,
"mode": "copy"
}'result = requests.post(
f"https://api.thunderphone.com/v1/agents/{agent_id}/transfer",
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
json={"target_org_id": 77, "mode": "copy"},
).json()| Field | Type | Required | Description |
|---|---|---|---|
target_org_id | integer | yes | For mode="move" it must differ from the source org id |
mode | string | no | copy (default — keep source intact) or move (re-home the agent) |
name | string | no | Override the new agent's name |
Returns 201 Created for copy / 200 OK for move:
{
"mode": "copy",
"source_org_id": 42,
"target_org_id": 77,
"agent": { /* Agent object in the target org */ }
}Version history
Deploys (and creates/duplicates/transfers) record an
AgentConfigVersion snapshot of the deployed configuration.
Draft autosaves are not versioned — the history reflects what
actually shipped to production.
curl 'https://api.thunderphone.com/v1/agents/12/versions?limit=20' \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"versions = requests.get(
f"https://api.thunderphone.com/v1/agents/{agent_id}/versions",
params={"limit": 20},
headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
).json()| Query param | Default | Description |
|---|---|---|
limit | 20 | Max results. Values outside 1–100 are clamped |
Response:
{
"count": 3,
"results": [
{
"id": 501,
"revision": 3,
"created_by_id": 7,
"created_at": "2026-04-20T18:24:10.113Z",
"snapshot": { /* Deployed agent config at this revision */ },
"changed_fields": ["prompt", "voice"]
}
]
}| Field | Type | Description |
|---|---|---|
id | integer | Version row id |
revision | integer | 1-based, increments per deploy |
created_by_id | integer | null | User id that deployed the revision |
snapshot | object | Full deployed config at this revision |
changed_fields | array of string | Fields that differ from the previous revision |
Prompt helpers
These three endpoints are convenience wrappers that call the same underlying models we use in the dashboard. All three take and return JSON; none change any persisted resource.
Detect prompt language
curl -X POST https://api.thunderphone.com/v1/agents/prompt-language-check \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Hola, gracias por llamar..."}'{
"detected_language_code": "es",
"detected_language_name": "Spanish",
"mismatch": true,
"provider": "gemini"
}Translate prompt
curl -X POST https://api.thunderphone.com/v1/agents/translate-prompt \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Hola, gracias por llamar…",
"target_locale": "en-US"
}'{
"translated_prompt": "Hello, thank you for calling…",
"source_language_code": "es",
"source_language_name": "Spanish",
"provider": "gemini",
"used_fallback": false
}Generate acknowledgement prompt
Produces a short "filler while thinking" phrase appropriate for Storm-with-ack products.
curl -X POST https://api.thunderphone.com/v1/agents/generate-acknowledgement-prompt \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "You are a friendly receptionist for a dental office.",
"target_locale": "en-US"
}'{
"acknowledgement_prompt": "Sure, let me take a quick look…",
"provider": "gemini",
"used_fallback": false
}Voices
List the supported voices and languages — or browse the voice gallery with playable samples:
curl 'https://api.thunderphone.com/v1/voices?languages=en,es' \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"voices": [
{
"name": "john",
"display_name": "John",
"gender": "male",
"category": "multilingual",
"accent": "american",
"languages": ["en", "es", "fr", "…"],
"sample_url": "https://…/john.mp3?v=3"
}
],
"languages": [ /* language catalog entries */ ]
}| Query param | Description |
|---|---|
languages | Comma-separated codes — only voices supporting all of them are returned |
category | Filter by voice category |
gender | Filter by voice gender |
The name is what you set on an agent's voice field.