Open in
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.
For dashboard setup, see Voice agent settings. For prompt-callable actions, see Built-in tools and Transfer calls.
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 / DELETE | /v1/agents/{agent_id}/intro-clip | Download, upload or remove the agent's custom intro clip |
GET / POST | /v1/agents/{agent_id}/consent-attestations | List or record consent-announcement attestations |
GET / POST | /v1/phi-designation-attestation | Read or record an agent's no-PHI designation attestation |
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 |
POST | /v1/agents/steps/flatten | Render shared instructions and steps as one prompt |
POST | /v1/agents/{agent_id}/prompt-analysis | Check a prompt against the agent's configuration |
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": [],
"native_voice_switching": false,
"language_switching": "ask_first",
"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,
"max_call_duration_seconds": 3600,
"ring_duration_seconds": null,
"connect_tone_enabled": false,
"typing_sounds_enabled": false,
"typing_sound": "keys_and_mouse",
"filler_phrases_enabled": true,
"tool_start_line": "always",
"play_sound_asset": "",
"intro_clip": "none",
"intro_clip_artifact_path": "",
"intro_clip_sha256": "",
"intro_ring_count": 1,
"speak_uninterruptible_enabled": false,
"send_email_enabled": false,
"send_email_recipient": "",
"calculator_enabled": false,
"transfer_destinations": [
{"name": "Billing", "number": "+14155552671", "platform_ok": true},
{"name": "Assigned rep", "number": "{{rep_phone}}", "platform_ok": null}
],
"warm_transfer_style": "introduce",
"warm_transfer_briefing": "",
"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,
"extraction_fields": [
{
"key": "insurance_type",
"type": "enum",
"description": "Insurance type requested by the caller",
"options": ["term", "whole"],
"required": false,
"ask": true
}
],
"extraction_blocks_completion": "except_transfers",
"call_summary_enabled": false,
"outcome_labels": [],
"report_instructions": "",
"timezone": "America/New_York",
"business_hours": null,
"reusable_instructions": [],
"integrations": [],
"knowledge_bases": [],
"knowledge_documents": [],
"mcp_servers": [],
"tool_collision_warnings": [],
"widget_enabled": true,
"split_testing_enabled": false,
"steps": null,
"handoffs": [],
"warnings": [],
"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, and any agent that does not process PHI can use an ElevenLabs voice as elevenlabs:<voice_id> |
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) |
native_voice_switching | boolean | For multilingual agents using catalog voices, switch to a native voice for the detected additional language, matched by gender. Returns to the configured voice for the primary language and keeps the current voice if no match is available. Default false |
language_switching | string | How a multilingual agent handles the other person (the caller, or the person an outbound call reaches) speaking another of its languages. ask_first (default) switches at once when they ask for the language or say a full sentence in it, and asks whether to continue in it after something short, like a "sí". follow_caller switches to whatever configured language they speak, even a single word. Applies only when additional_languages is set |
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 |
max_call_duration_seconds | integer or null | Maximum call length in seconds. 60–21600 (1 minute–6 hours); null means no per-agent limit. New agents start at your organization's new-agent default (3600, one hour) unless you send a value; agents created before that default keep what they had. Every call ends after 6 hours whatever this says |
ring_duration_seconds | integer or null | Ringing time for outbound and transferred calls, in seconds. 5–120; null uses platform defaults (60s outbound, 45s warm transfer, provider default for cold transfer). Does not affect inbound ringing. |
connect_tone_enabled | boolean | Play a short connection tone before the agent begins. Default false |
typing_sounds_enabled | boolean | Play a soft keyboard sound while a custom tool runs. Platform-managed tools do not trigger it. Default false |
typing_sound | string | Which typing sound plays when typing_sounds_enabled is on: keys_and_mouse, mechanical, desktop, laptop, office, or quiet_room. Default keys_and_mouse |
filler_phrases_enabled | boolean | Long-wait updates: on a slow tool or reply, lines such as "Sorry, this is taking a little longer than expected" after the tool-start line. false stays quiet after the tool-start line, which tool_start_line controls. Default true |
tool_start_line | string | Tool-start line: what ThunderPhone says when the agent calls a tool. always: something at every tool call (the agent's own words, or a short line written for the call). only_if_slow: a short line only if the tool is still running after about 3 seconds. never: nothing from ThunderPhone, and the agent is told not to speak at tool calls. The agent's own words always play. New agents default to always; agents created before this field existed have only_if_slow. Prompt text cannot override it |
play_sound_asset | string | Prompt-callable sound asset: "" (none), chime, ding, or beep |
intro_clip | string | Prerecorded clip played at the start of inbound phone calls (and Talk browser test calls), before the agent speaks: none, stock_recording_notice ("This call may be monitored or recorded, for quality assurance purposes."), or custom (the clip uploaded with POST /v1/agents/{agent_id}/intro-clip; plays nothing until one is uploaded). Not played on outbound calls, AI test calls, web widget calls or Realtime API calls. Default none |
intro_clip_artifact_path | string | Read-only. Storage path of the uploaded custom clip, "" when there is none |
intro_clip_sha256 | string | Read-only. SHA-256 of the uploaded custom clip, "" when there is none |
intro_ring_count | integer | US rings played after the intro clip, before the agent speaks. 0–2, default 1. Only used when intro_clip is not none |
speak_uninterruptible_enabled | boolean | Legacy compatibility field. The speak_uninterruptible tool is currently unavailable, so this setting has no runtime effect. Default false |
send_email_enabled | boolean | Offer the built-in email tool when send_email_recipient is also set. Default false |
send_email_recipient | string | Fixed recipient for the built-in email tool. The model supplies only the subject and body and cannot choose or change this address |
calculator_enabled | boolean | Offer the built-in calculate tool, which adds up totals and prices exactly. Default false |
send_sms_enabled | boolean | Offer the send_sms tool when a customer-connected SMS-capable sender is available. Default false |
sms_from_number | string | Optional E.164 fallback sender on the organization's connected Twilio or Telnyx account |
sms_allowed_recipients | array of object | Up to 10 owner-configured contacts the agent may text. Each object has a unique E.164 number and a unique NFKC-normalized, case-insensitive label of 1–60 visible characters. Labels cannot look like phone numbers and are caller-visible public metadata; only labels are shown to the model |
sms_allow_other_recipients | boolean | Allow the model to text any valid E.164 number, including any number a caller dictates. Default false. This broad permission is not required for the telephone other party or sms_allowed_recipients |
transfer_destinations | array of object | The agent's transfer numbers: up to 50 {name, number} entries, each with a read-only platform_ok. The agent transfers only to these, by name; [] (the default for new agents) means it can't transfer calls |
warm_transfer_style | string | What happens when a warm transfer is answered. introduce (default, Introduce on the line): the caller is connected as soon as the person answers, then the agent introduces the caller on the line, which both hear, and drops. ask_first (Ask first): the caller waits on hold while the agent privately asks the person who answers whether they can take the call; they can decline. On yes the caller is connected and introduced on the line. whisper (Whisper): the caller waits on hold while the agent privately tells the person who answers who is calling and why, without asking; then the caller is connected and the agent drops with nothing said on the line |
warm_transfer_briefing | string | What the agent tells the person who answers a warm transfer about the caller, as an instruction to the agent, for example "The caller's name and what they need.". ≤300 characters; line breaks and repeated spaces are collapsed. "" (the default) keeps a very short briefing: who is calling and why |
sms_followup_enabled | boolean | Send one SMS from the customer carrier after an eligible completed telephone call. Default false |
sms_followup_template | string | Plain-text follow-up template. Supports call variables and extracted fields when present |
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 | API-only Storm compatibility control: full or reduced (lower latency, less prior audio). This is separate from additional_audio_context |
watchdog_enabled | boolean | Enables live supervision of agent turns (Storm). Default false |
additional_audio_context | boolean | null | Backing field for the dashboard's Extra audio context choices: true is On, false is Off, and null is Automatic (recommended). Automatic uses more context for inbound and browser/WebSocket sessions and only the latest turn for outbound phone calls |
extraction_fields | array | Complete structured-data schema extracted from each retained call transcript. Up to 40 fields. Each item requires a unique lowercase key, a type (string, number, boolean, enum, date, or datetime), and a 1–500 character description. enum fields require 2–50 unique options; ask defaults to true and required defaults to false. Writes replace the complete list. |
outcome_labels | array | How calls with a gradeable conversation are labelled in the call report (dashboard: Reporting in the agent builder, where Suggest from prompt proposes a set fitted to the agent's job). [] (default) uses the platform pair success / failure, judged as before. Otherwise send 2–12 objects: key is required, unique, and matches [a-z][a-z0-9_]{0,39}; label is required, 1–60 characters after whitespace is collapsed; description is optional (default "", up to 400 characters) and tells the grader when the label applies; color is optional, one of gray, green, red, amber, blue, purple, pink or teal (default gray), and only sets the pill colour. Keeping the keys success / failure but rewriting a label or description makes them your own definitions. Calls are judged against the labels in effect when they started. A quick hang-up has no configured outcome and shows as No conversation. Writes replace the complete list. |
report_instructions | string | Rules for the report's prose fields (one-line summary, call reason, caller-experience note, attention note), for example what must never be mentioned. ≤2,000 characters; "" uses the platform default. |
timezone | string | IANA time zone, up to 64 characters, such as America/New_York (dashboard: Time zone). The agent states the date and time, offers times and works out "today" in it, and this agent's business hours in reports use it. Set it for every agent. "" means automatic: each phone call uses the zone of the number it is on when that number's area code places it, else the zone of the agent's numbers, else the organization's, else US Pacific; reports then use the organization's zone. An agent with "" lists timezone_not_set in warnings. A call can override it with its own timezone. |
business_hours | object | null | Weekly schedule keyed by mon through sun, each a list of at most four {start, end} windows in 24-hour HH:MM with the end later than the start, deciding which of this agent's calls count as after hours. null inherits the organization's schedule; {} counts every call as business hours. |
call_summary_enabled | boolean | Generate a plain-language summary after each call (dashboard: Generate a call summary under Data to collect). Delivered as summary on telephony.complete / web.complete and on the Call object. Runs in the post-call extraction step, so extraction_blocks_completion decides whether the completion event waits for it; when it does not wait, the summary is only available on the Call object. Requires call recording; not generated for calls too short to summarize. Default false. |
reusable_instruction_ids | array | UUIDs of the organization's reusable instructions attached to this agent. Each attached instruction is appended to the end of the system prompt under a ## <name> heading at call start, in creation order. Writes replace the complete list; responses embed the attached rows as reusable_instructions (the prompt field itself is unchanged). |
extraction_blocks_completion | string | When extraction may hold customer completion and legacy webhook delivery: except_transfers (default), always, or never. Billing settlement and reservation release never wait. In a blocking mode, completion normally waits at most the 75-second extraction budget. If the worker process is lost, the production finalization drain (every five minutes) releases a past-deadline completion before starting another extraction attempt; a later success emits call.data_extracted. |
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 |
warnings | array | Read-only configuration warnings for the draft the builder and test calls use: tool, handoff and time zone problems. Never blocks a save. Default [] |
tool_collision_warnings | array | Deprecated: read warnings. The same list without timezone_not_set. |
steps | object | null | Splits the agent into steps. null (default) is a single-prompt agent. When set, prompt holds the shared instructions that apply in every step |
handoffs | array | Other agents in the organization this agent can hand the call to. Default [] |
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_native_voice_switching, draft_language_switching, 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_max_call_duration_seconds, draft_ring_duration_seconds, draft_connect_tone_enabled,
draft_typing_sounds_enabled, draft_typing_sound, draft_filler_phrases_enabled, draft_tool_start_line, draft_play_sound_asset,
draft_intro_clip, draft_intro_clip_artifact_path, draft_intro_clip_sha256, draft_intro_ring_count,
draft_speak_uninterruptible_enabled, draft_send_email_enabled,
draft_send_email_recipient, draft_calculator_enabled,
draft_send_sms_enabled, draft_sms_from_number, draft_sms_allow_other_recipients,
draft_sms_allowed_recipients, draft_transfer_destinations,
draft_warm_transfer_style, draft_warm_transfer_briefing,
draft_sms_followup_enabled, draft_sms_followup_template,
draft_consent_announcement_enabled, draft_consent_announcement_text,
draft_audio_context_mode, draft_watchdog_enabled, draft_additional_audio_context,
draft_extraction_fields, draft_extraction_blocks_completion, draft_call_summary_enabled, draft_outcome_labels, draft_report_instructions, draft_timezone, draft_business_hours, draft_reusable_instruction_ids,
draft_widget_enabled, draft_split_testing_enabled, draft_steps, draft_handoffs), 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 |
draft_max_call_duration_seconds is read-only: null means unchanged, and
0 means a pending removal of the cap. Stage removal by sending
"max_call_duration_seconds": null; deployment restores the live value to null.
draft_ring_duration_seconds follows the same read-only draft convention: null
means unchanged and 0 stages clearing the setting. Write "ring_duration_seconds": null
to restore platform defaults on deployment. Public writes must be null or 5–120.
To turn a stepped agent back into a single-prompt agent, send "steps": null.
While that change is pending the response shows "draft_steps": {}, which means
"no steps on deploy". Send the text from
steps/flatten as prompt in the same request
to keep the steps' instructions.
To restore the organization's business hours, send "business_hours": null.
When this clears an existing override, the response shows
"draft_business_hours": {"__inherit__": true} until deployment. That marker
means "inherit on deploy"; it is not a weekday schedule. Send
"timezone": "" too to go back to the automatic time zone.
Production call paths (inbound, outbound, widget) always read the deployed fields. Mic sessions and simulations read the draft with fallback to the deployed value.
Transfer numbers
transfer_destinations lists the numbers the agent may transfer calls to.
The prompt refers to each entry by name; the model sees the names and never
the numbers. See Transfer calls for prompting and
runtime behavior.
| Field | Type | Description |
|---|---|---|
name | string | Required. 1–60 characters after NFKC normalization and trimming, unique on the agent ignoring case. No line breaks, control characters, or invisible characters |
number | string | Required. A phone number, or exactly one {{variable}} with no default. Numbers without a country code are read as US numbers and are stored in E.164. A variable is filled from the call's variables when the call starts |
platform_ok | boolean | null | Read-only; ignored on write. true when calls on ThunderPhone numbers can transfer here (a US number that is not premium-rate or another special-service number). false means the number works only on transfers your own carrier carries (see Transfer calls). null for a variable entry, which is checked on each call |
Writes replace the whole list. Sending null is rejected; send [] to remove
every entry. The same validation runs on create, update, deploy, and every
other write path:
- At most 50 entries.
- Only
nameandnumberare allowed. Other keys returnUnknown field(s): …on that entry. - A number must be a valid phone number without an extension. Numbers in the fictional range 555-0100 through 555-0199 are refused.
- If the organization has no number on its own carrier, a number that a ThunderPhone number can't reach (outside the US, premium-rate, shared-cost, personal-number, and similar types) is refused with a message that says why.
- A variable must be one placeholder on its own, such as
{{rep_phone}}.{{rep_phone|+14155552671}}and text around the placeholder are refused.
Entry errors come back as a list with one object per submitted entry, in
order, and {} for entries that passed:
{
"transfer_destinations": [
{},
{"number": ["(415) 555-0134 is a fictional number: 555-0100 through 555-0199 are reserved for examples. Enter a real number."]},
{"number": ["+44 20 7123 4567 is outside the US, and calls on ThunderPhone numbers can only transfer to US numbers. To transfer there, connect your own carrier under Phone Numbers."]}
]
}Deploying re-checks the deployed list against the organization, so a non-US entry saved while the organization had a number on its own carrier blocks the deploy once it has none.
Configuration warnings
warnings flags settings worth a second look. Every entry has a code and a
human-readable message; other keys depend on the code. Create, update and
retrieve responses all carry it. The older tool_collision_warnings field
carries the same entries except timezone_not_set; it is deprecated.
code | Extra keys | Meaning |
|---|---|---|
tool_has_no_inputs | tool_name, integration_id | An attached integration declares no inputs in spec.function.parameters.properties, so the agent may call it with no arguments. Add the fields your endpoint expects. Ignore this only if the endpoint really takes no data. When the last analysis of the prompt finds the tool, this is replaced by tool_arguments_not_declared, or dropped when the prompt lists no arguments for it. |
tool_arguments_not_declared | tool_name, integration_id, arguments | The prompt tells the agent to send arguments to this tool, but its spec.function.parameters.properties doesn't declare them. The agent sends only declared inputs reliably. |
api_mcp_tool_name_collision | tool_name, mcp_server_id, mcp_server_name | An integration and an MCP tool share a name; the integration wins and the MCP tool is left out. |
handoff_target_missing, handoff_graph_truncated, handoff_tool_name_collision | see handoffs | Handoff problems. |
timezone_not_set | suggested_timezone, source | The agent has no timezone. suggested_timezone is the zone its calls use now and source says why (phone_number, organization or default). Set timezone to the zone its callers are in. |
Reading the agent never runs a prompt analysis. tool_arguments_not_declared
comes from the last analysis of the current prompt, by
the dashboard builder or by POST /v1/agents/{id}/prompt-analysis. To get it
after changing a prompt over the API, call that endpoint with the agent's
draft_prompt (or prompt when there is no draft) exactly as the agent
response returns it, then read the agent again. A prompt that differs by so
much as a trailing newline is analyzed separately.
"warnings": [
{
"code": "tool_has_no_inputs",
"tool_name": "create_ticket",
"integration_id": "6f1c2a9e-1b7d-4c55-9a0e-3f2d8b7c4e10",
"message": "The tool 'create_ticket' declares no inputs, so the agent may call it without the data your endpoint needs. Add the fields it expects to spec.function.parameters.properties. Ignore this only if the endpoint really needs no data."
}
]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 |
Steps and handoffs
Split an agent into steps when its calls move through phases, such as
greeting, identity checks and booking. Each step has its own instructions,
tools, an optional word-for-word line and values to collect. The agent moves
on by calling the built-in go_to_step tool, or when a code-checked condition
on the collected values holds. When steps is set, prompt holds the shared
instructions that apply in every step.
A handoff passes the call to another agent in the same organization. That agent brings its own prompt, steps, tools and voice, and the transcript carries over. Every other call setting (product, languages, speaking order, voicemail, consent) comes from the agent the call started on.
Agents without steps or handoffs ("steps": null, "handoffs": []) work
exactly as before.
steps
{
"start_step": "greet",
"steps": [
{
"id": "greet",
"name": "Greeting",
"instructions": "Greet the caller and ask how you can help.",
"say": "Thanks for calling Acme Dental, this is Ava.",
"transitions": [
{"to": "verify", "when": "The caller wants to book, move or cancel an appointment"}
]
},
{
"id": "verify",
"name": "Verify identity",
"instructions": "Ask for the caller's full name and date of birth.",
"tools": ["lookup_patient"],
"collect": [
{"name": "full_name", "type": "string", "description": "Caller's full name"},
{"name": "dob", "type": "date", "description": "Date of birth"}
],
"transitions": [
{"to": "minor", "condition": {"value": "age", "op": "lt", "target": 18}},
{"to": "book", "when": "Identity is confirmed"}
]
},
{"id": "minor", "name": "Minor", "instructions": "Ask for a parent or guardian."},
{"id": "book", "name": "Book", "instructions": "Offer open slots and book one."},
{
"id": "billing",
"name": "Billing questions",
"instructions": "Answer questions about bills and payments.",
"global_when": "The caller asks about a bill or payment",
"transitions": [{"to": "__previous__", "always": true}]
}
]
}start_step names the step the call begins in. Each step takes:
| Field | Type | Description |
|---|---|---|
id | string | Required. Matches [a-z][a-z0-9_]{0,39}, unique within the agent |
name | string | Required display name, 1–80 characters |
instructions | string | Required, up to 20,000 characters. Supports {{variables}} like prompt |
say | string | null | Line spoken word for word when the step is entered, up to 1,000 characters. Supports {{variables}} |
tools | array | null | null (default) offers every agent tool. A list offers only the named agent tools in this step. Built-in tools such as ending or transferring the call follow the agent's settings in every step |
collect | array | Up to 20 values to gather: name (same pattern as id, unique in the step), type (string, number, boolean or date as YYYY-MM-DD), optional description (up to 500 characters) and required (default true) |
on_enter_tool | string | null | An agent tool called automatically when the step is entered. Its result is stored as the value <tool>_result: status is the tool's HTTP status code and response is the response body |
transitions | array | Ordered moves out of the step, up to 40. Each has to and exactly one trigger: when (the model decides, up to 1,000 characters), condition (code decides) or "always": true. to is a step id, or "__previous__" to return to the step that was active before a global step. A step with no transitions is the last one |
global_when | string | null | Makes the step reachable from any step when this holds (the model decides) |
voice | string | null | Voice for this step. Ignored when the call uses a custom voice_config |
A condition is {"value", "op", "target"}. value names a collected value
or a dotted path into a tool result, such as lookup_patient_result.response.found
(a field of the response body) or lookup_patient_result.status (its HTTP status code).
op is one of eq, ne, lt, lte, gt, gte, in, not_in, exists
or missing:
eqandnetake a string, number or boolean;inandnot_intake a list of 1–50 of them. Strings compare case-insensitively after trimming.lt,lte,gtandgtetake a number or aYYYY-MM-DDdate.existsandmissingtake notarget.
A value that was never collected makes every op false except missing.
Conditions are checked in order when the agent moves on, and the first match
wins.
Limits: 100 steps and 200 KB of JSON per agent. Step tools and
on_enter_tool must name tools the agent has when you save. On an agent with
steps or handoffs, no tool may be named go_to_step or hand_off.
handoffs
[{"agent_id": 13, "when": "The caller asks about billing or a refund"}]Up to 8 entries. agent_id must be another agent in the same organization;
when (1–1,000 characters) tells the agent when to hand off. A call includes
at most 8 agents, counted breadth-first from the agent it started on. The
agent's warnings lists handoff_graph_truncated when
handoffs further out are left out, handoff_target_missing when a handoff
names an agent that no longer exists (calls skip it), and
handoff_tool_name_collision when a handed-to agent has a different tool with
the same name as an earlier agent's (that tool is left out of the call).
Validation errors
Invalid steps or handoffs return 400 with one key per field path:
{
"steps.steps[1].transitions[0].to": ["Unknown step id \"booking\"."],
"handoffs[0].agent_id": ["No agent with this id in the organization."]
}Example
Stage steps and a handoff on the draft, then deploy:
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 Ava, the front desk for Acme Dental. Be brief and friendly.",
"steps": {
"start_step": "greet",
"steps": [
{
"id": "greet",
"name": "Greeting",
"instructions": "Ask how you can help.",
"transitions": [{"to": "book", "when": "The caller wants an appointment"}]
},
{"id": "book", "name": "Book", "instructions": "Offer open slots and book one."}
]
},
"handoffs": [{"agent_id": 13, "when": "The caller asks about billing"}]
}'
curl -X POST https://api.thunderphone.com/v1/agents/12/deploy \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"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, and an agent that does not process PHI can pass an ElevenLabs voice (elevenlabs:<voice_id>); 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 |
native_voice_switching | boolean | no | Use a native catalog voice, matched by gender, after switching to an additional language. Default false |
language_switching | string | no | ask_first (default) or follow_caller. See Deployed fields |
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" |
extraction_fields | array | no | Complete replacement list of structured fields to extract after each retained call. See Deployed fields for the item schema |
extraction_blocks_completion | string | no | except_transfers (default), always, or never; controls customer completion delivery only |
outcome_labels | array | no | Complete replacement list of call-report outcome labels; [] restores Success / Failure. See Deployed fields |
report_instructions | string | no | Instructions for the call report's prose fields |
timezone | string | no | The agent's time zone; see Deployed fields. Optional, but an agent without one lists timezone_not_set in warnings |
business_hours | object | null | no | This agent's reporting hours; see Deployed fields |
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 |
max_call_duration_seconds | integer or null | no | 60–21600. Omit it to use the organization's new-agent default (3600, one hour); send null for no per-agent limit. Every call still ends after 6 hours |
ring_duration_seconds | integer or null | no | Ringing time for outbound and transferred calls, in seconds. 5–120; null uses platform defaults (60s outbound, 45s warm transfer, provider default for cold transfer). Does not affect inbound ringing. |
connect_tone_enabled | boolean | no | Default false |
typing_sounds_enabled | boolean | no | Play a soft keyboard sound while a custom tool runs. Default false |
typing_sound | string | no | keys_and_mouse, mechanical, desktop, laptop, office, or quiet_room. Default keys_and_mouse |
filler_phrases_enabled | boolean | no | Long-wait updates on slow tools and replies. false stays quiet after the tool-start line. Default true |
tool_start_line | string | no | always, only_if_slow, or never. Default always |
play_sound_asset | string | no | "" (none), chime, ding, or beep |
speak_uninterruptible_enabled | boolean | no | Legacy compatibility field with no current runtime effect. Default false |
send_email_enabled | boolean | no | Offer the built-in email tool when a recipient is configured. Default false |
send_email_recipient | string | no | Fixed email recipient chosen by the agent owner; the model cannot change it |
calculator_enabled | boolean | no | Offer the built-in calculate tool for exact arithmetic. Default false |
send_sms_enabled | boolean | no | Offer send_sms when an eligible customer-connected sender is available. Default false |
sms_from_number | string | no | E.164 fallback sender owned by the organization and confirmed SMS-capable by Twilio or Telnyx |
sms_allowed_recipients | array of object | no | Up to 10 {number, label} contacts. Numbers are unique E.164 values. Labels are NFKC-normalized, unique case-insensitively, 1–60 visible characters, cannot look like phone numbers, and are caller-visible public metadata. Only labels are shown to the model. A configured contact enables real sends in widget, realtime, mic, and simulation sessions without broad recipient permission |
sms_allow_other_recipients | boolean | no | Permit any valid E.164 recipient beyond the current telephone party and configured contacts. Default false. When enabled, the agent can text any number a caller dictates |
transfer_destinations | array of object | no | Up to 50 {name, number} transfer numbers. Default []: the agent can't transfer calls |
sms_followup_enabled | boolean | no | Send one eligible post-call SMS. Default false |
sms_followup_template | string | no | Plain-text post-call template, up to 1600 characters |
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 |
reusable_instruction_ids | array of UUID | no | Attach reusable instructions |
call_summary_enabled | boolean | no | Generate a post-call summary. Default false |
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 / reusable_instruction_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.
Intro clip
GET /v1/agents/{agent_id}/intro-clip ·
POST /v1/agents/{agent_id}/intro-clip ·
DELETE /v1/agents/{agent_id}/intro-clip
Manage the agent's custom intro clip (intro_clip: "custom"). Like every
agent edit, an upload or removal changes the draft; it reaches live calls
when you deploy.
Upload with a multipart audio field. The file must be a PCM WAV,
48,000 Hz, mono, 16-bit, 0.5–15 seconds long and at most 2 MB; anything
else is refused with 400 and an audio error. (The dashboard converts any
file or browser recording to this format for you.) The upload sets
draft_intro_clip to custom along with draft_intro_clip_artifact_path and
draft_intro_clip_sha256.
curl -X POST https://api.thunderphone.com/v1/agents/12/intro-clip \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-F "audio=@intro.wav;type=audio/wav"Returns 201 Created:
{
"artifact_path": "agents/intro-clips/7/12/3f5a…c9.wav",
"sha256": "3f5a…c9",
"duration_seconds": 4.2,
"updated_at": "2026-10-13T18:02:11.482913Z"
}updated_at is the agent's new value, for
optimistic concurrency.
Download returns the clip the draft would play (the draft's clip if one is
staged, otherwise the deployed one) as audio/wav, or 404 when there is
none.
Remove stages removal of the custom clip; if intro_clip was custom,
the draft also switches it to none. Returns 200 OK with the draft's
resulting intro_clip and updated_at:
{ "intro_clip": "none", "updated_at": "2026-10-13T18:05:40.120004Z" }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 |
PHI designation attestation
GET /v1/phi-designation-attestation?agent_id={agent_id} ·
POST /v1/phi-designation-attestation
Only organizations whose BAA includes the PHI designation clause
(baa_status: "executed_with_designation") use this endpoint. In such an
organization every agent processes PHI by default. Before an agent's
phi_processing_override can be set to no_phi, an organization admin
must record an attestation for that agent; without one, saving or deploying
the override returns 400.
GET returns the current text_version, the canonical English statement
(statement_en) and its statement_sha256, the organization's
baa_status and phi_processing, and designation_available. With
agent_id, it also reports whether that agent has an active attestation
(attested, attestation).
POST takes agent_id, accepted: true, the text_version and the
statement_sha256 from GET, and an optional display locale (default
en; the English statement governs). It returns 201 with the
attestation, or 200 with the existing one if the agent already has an
active attestation.
curl "https://api.thunderphone.com/v1/phi-designation-attestation?agent_id=12" \
-H "Authorization: Bearer sk_live_YOUR_API_KEY"{
"text_version": "phi-designation-no-phi-v1-2026-09-24",
"statement_en": "I am authorized to designate this agent as not processing protected health information (PHI). …",
"statement_sha256": "7776b7852f41e777eac228bfd5abdc4df5c958752b39ea032c86f14b02a892da",
"baa_status": "executed_with_designation",
"phi_processing": "phi",
"designation_available": true,
"attested": false,
"attestation": null
}| Status | Condition |
|---|---|
201 | Attestation recorded (POST) |
200 | The agent already has an active attestation (POST) |
400 | Unknown agent_id, accepted was not true, locale is blank, text_version is not accepted, or statement_sha256 does not match the canonical statement |
403 | API key, or caller is not an org admin (POST) |
409 | The organization's BAA has no designation clause, so there is nothing to attest |
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.
An agent that other agents hand off to, in their
deployed or draft handoffs, cannot be deleted. The request returns
409 Conflict with "code": "agent_is_handoff_target" and the referencing
agents in agents ([{"id", "name"}]). Remove those handoffs first.
Duplicate an agent
Creates a new agent in the same org with everything configured for the
source agent: all settings, both deployed and pending drafts, the same
attached integrations, knowledge bases/documents, MCP servers and reusable
instructions, and new copies of its agent-scoped webhook endpoints (same URL,
events and signing secret), calendar/CRM connections, split-testing variants,
test scenarios and suites (with schedules), and client portal listings. The
name defaults to <name> (Copy) and can be overridden. If the source has a
pending draft, the copy has one too (has_draft: true), and deploying it
ships those edits.
A few settings are reset rather than copied:
- A consent announcement turned off by attestation stays off, and the copy gets its own attestation record carrying the original's. A copy of an agent exempted without an attestation starts with the announcement on.
- A live-processing disclosure set to off goes back to the org default.
- A PHI override the org doesn't allow goes back to the org default.
Phone numbers, SIP addresses, widget publishable keys, campaigns and call history stay with the source agent. Move a number to the copy separately. Steps and handoffs are copied as they are, so the copy hands off to the same agents.
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. admin+ role is required
on both orgs.
An sk_live_ API key is limited to its own organization: with a key,
target_org_id must be the key's organization, and any other target
returns 403. Copy or move between organizations from a signed-in session
(Authorization: Token <session-token> plus X-ThunderPhone-Org for the
source org), as below.
curl -X POST https://api.thunderphone.com/v1/agents/12/transfer \
-H "Authorization: Token YOUR_SESSION_TOKEN" \
-H "X-ThunderPhone-Org: 42" \
-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": "Token YOUR_SESSION_TOKEN", "X-ThunderPhone-Org": "42"},
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 |
copy makes the same copy as Duplicate, with the
target org's rules applied: integrations, knowledge, MCP servers and reusable
instructions attach only if the same record belongs to the target org;
webhook endpoints, calendar/CRM connections, client portal listings, legacy
inline tools, and test scenarios created from real calls are not copied and
are named in not_copied; copied test-suite schedules start disabled; SMS
sending is off until the target org picks one of its own numbers; transfer
numbers the target org could not save (numbers a ThunderPhone number can't
reach, when the target org has no number on its own carrier) are left behind
and reported as transfer_destinations in not_copied; and the consent
announcement starts on, because another org's attestation does not
cover the target org.
Returns 201 Created for copy (with not_copied, a list such as
["webhook_endpoints"]) / 200 OK for move:
{
"mode": "move",
"source_org_id": 42,
"target_org_id": 77,
"agent": { /* Agent object in the target org */ },
"detached": {
"publishable_keys": 1,
"webhook_endpoints": 0,
"provider_connections": 1,
"client_portals": 0,
"transfer_destinations": 0
}
}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 endpoints are convenience wrappers that call the same underlying models we use in the dashboard. All of them take and return JSON; none change your agents or any other resource you manage.
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": "<provider id>"
}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": "<provider id>",
"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": "<provider id>",
"used_fallback": false
}Render steps as one prompt
Returns the shared instructions and steps as one prompt, in the form a
stepped agent falls back to when steps cannot run. Use it to turn a stepped
agent back into a single-prompt agent: send the result as prompt together
with "steps": null. steps is checked for shape only, and nothing is saved.
With "steps": null, prompt comes back unchanged.
curl -X POST https://api.thunderphone.com/v1/agents/steps/flatten \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "You are Ava, the front desk for Acme Dental.",
"steps": {
"start_step": "greet",
"steps": [
{
"id": "greet",
"name": "Greeting",
"instructions": "Ask how you can help.",
"transitions": [{"to": "book", "when": "The caller wants an appointment"}]
},
{"id": "book", "name": "Book", "instructions": "Offer open slots and book one."}
]
}
}'{
"prompt": "You are Ava, the front desk for Acme Dental.\n\n## Conversation flow\n1. Greeting (start here): Ask how you can help.\n - If The caller wants an appointment → go to Book\n2. Book: Offer open slots and book one.\n"
}Analyze a prompt
Runs the same advisory checks the agent builder shows under the prompt
editor. A model extracts facts from the prompt text (languages, tools it
mentions, escalation and consent guidance), and the API compares them
with the agent's configuration. Nothing is saved to the agent, so you can
send an unsaved draft. Requires the member role or higher.
curl -X POST https://api.thunderphone.com/v1/agents/123/prompt-analysis \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Eres la recepcionista de una clínica dental. Usa book_appointment para agendar citas.",
"primary_language": "en",
"additional_languages": [],
"voice": "john",
"integration_ids": [],
"mcp_server_ids": []
}'| Field | Type | Description |
|---|---|---|
prompt | string | Required. The prompt text to analyze. May be empty. Max 100,000 characters. |
primary_language | string | Language code to check against. |
additional_languages | string[] | Additional language codes to check against. |
voice | string | Voice to check language coverage against. |
integration_ids | integer[] | Integrations to treat as connected. |
mcp_server_ids | integer[] | MCP servers to treat as connected. |
integration_public_ids | string[] | The same as integration_ids, by the integration id the agent and integration endpoints return. Ignored when integration_ids is sent. |
mcp_server_public_ids | string[] | The same as mcp_server_ids, by the MCP server id. Ignored when mcp_server_ids is sent. |
Every field except prompt is optional. An omitted field uses the
agent's draft value, or the deployed value when the draft has none, so
the findings depend on the agent unless you send every field, as the
example does.
{
"facts": {
"languages": {
"declared": [],
"written_in": [{"code": "es", "share": 1.0}]
},
"tools": {
"referenced": [
{"name": "book_appointment", "kind": "identifier", "evidence": "Usa book_appointment para agendar citas"}
]
},
"escalation": {"present": false, "kind": "none", "evidence": ""},
"compliance": {"present": false, "evidence": ""}
},
"findings": [
{
"id": "languages-not-configured",
"severity": "warning",
"params": {"languages": [{"code": "es", "supported_by_voice": true}]}
},
{
"id": "primary-language-mismatch",
"severity": "warning",
"params": {"detected": "es", "primary": "en"}
},
{
"id": "tool-actions-without-tools",
"severity": "warning",
"params": {"tools": ["book_appointment"]}
},
{"id": "missing-escalation-guidance", "severity": "warning", "params": {}},
{"id": "missing-consent-or-privacy-guidance", "severity": "info", "params": {}}
]
}Finding id | Severity | Meaning |
|---|---|---|
languages-not-configured | warning | The prompt declares a language, or has at least 20% of its text in one, that the agent isn't configured for. params.languages lists each code and whether the voice supports it. |
primary-language-mismatch | warning | At least 60% of the prompt is written in params.detected, not the primary language params.primary. |
tool-actions-without-tools | warning or info | The prompt promises actions that need an external system, but no integration or MCP server is connected. warning when it names a tool identifier, info when it only describes an action. params.tools lists up to five. |
tool-arguments-not-declared | warning | The prompt tells the agent to send arguments that a connected integration does not declare in spec.function.parameters.properties. params.undeclared lists up to five tools as {name, arguments}. |
missing-escalation-guidance | warning | The prompt gives no path to a human, transfer or callback. |
missing-consent-or-privacy-guidance | info | The prompt has no consent, recording-notice, privacy or disclosure guidance. |
facts is what the model extracted; findings are computed from the
facts and the configuration on every request. New finding ids and fact
keys may be added, so ignore ones you don't recognize.
The analysis is advisory and never fails the request because the model did:
- If the model is unavailable or returns something unusable, you get
200with{"findings": [], "facts": null, "error": "unavailable"}. - Facts are cached per agent and prompt text. Repeating a prompt returns instantly, with findings recomputed against the current configuration.
- Uncached requests are throttled per agent. A new model request starts
no sooner than two seconds after the previous one started, and while
one is still running it holds a 20-second lease. A request inside
either window returns the last good result with
"stale": trueandretry_after_ms. If there is no earlier result it returns202with onlyretry_after_ms. Either way, retry after that many milliseconds.
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 */ ],
"elevenlabs": {
"provider": "ElevenLabs",
"model": "eleven_v4_turbo",
"model_label": "Eleven v4 Turbo",
"surcharge_cents": 5,
"hipaa_eligible": false,
"available": true,
"unavailable_reason": null,
"languages": ["ar", "bn", "…"],
"voices": [
{
"name": "elevenlabs:UgBBYS2sOqTuMpoF3BR0",
"display_name": "Mark",
"tagline": "Natural Conversations",
"description": "A casual, young-adult speaking in a natural manner. Perfect for Conversational AI.",
"gender": "male",
"age": "young",
"accent": "american",
"native_language": "en",
"use_case": "conversational",
"sample_url": "https://storage.googleapis.com/eleven-public-prod/…/0Oc7jiXwWN9kRTXfQsmw.mp3"
}
]
}
}| 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.
ElevenLabs voices
The elevenlabs block lists voices from ElevenLabs under ElevenLabs' own names,
on the Eleven v4 Turbo model. Every voice speaks every code in its languages,
which is the whole language catalog, and costs surcharge_cents per minute in
every language, in place of any language or premium-voice surcharge. The
gender filter applies to this block too; languages and category keep it
only when every requested language is covered or the category is elevenlabs.
ElevenLabs voices are not HIPAA eligible yet (hipaa_eligible: false):
- An agent, draft, step, split-test variant or realtime session whose PHI
designation resolves to PHI cannot select one. The request fails with
"ElevenLabs voices are not HIPAA eligible yet. Ask us to enable HIPAA for them, or pick another voice.", as does switching an agent that uses one to processing PHI. availableisfalsewithunavailable_reason: "phi"when your whole organization resolves to PHI. Individual agents may still be designated as not processing PHI; the check above is per agent.- If a call to an agent with an ElevenLabs voice runs as a PHI call anyway, it uses a standard voice for the call language and carries no ElevenLabs charge.
Related
Match API fields to the dashboard controls.
Configure platform-run actions and their limits.
Set cold and screened warm-transfer routing.
Assign agents to inbound/outbound directions on phone numbers.
Organization-wide prompt sections attached via reusable_instruction_ids.
Ground the agent in your documents.
A/B test prompt and voice variants on live traffic.
Regression-test the agent before deploying.