ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Agents

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

MethodPathDescription
GET/v1/agentsList agents
POST/v1/agentsCreate 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}/deployPromote the draft to the deployed config
POST/v1/agents/{agent_id}/discard-draftThrow away all pending draft changes
DELETE/v1/agents/{agent_id}Delete an agent
POST/v1/agents/{agent_id}/duplicateDuplicate an agent
POST/v1/agents/{agent_id}/transferCopy or move an agent to another org
GET/v1/agents/{agent_id}/versionsList deployed configuration revisions
GET / POST/v1/agents/{agent_id}/consent-attestationsList or record consent-announcement attestations
POST/v1/agents/prompt-language-checkDetect the language of a prompt
POST/v1/agents/translate-promptTranslate a prompt into another locale
POST/v1/agents/generate-acknowledgement-promptAuto-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

FieldTypeDescription
idintegerServer-assigned agent id
namestringHuman-readable name. 1–255 chars
promptstringThe system prompt that governs agent behavior
voicestringVoice name from GET /v1/voices — e.g. john, megan. Entitled organizations can also pass custom:<public_id> for a ready custom voice
primary_languagestringISO 639 code, e.g. en, es. Default en. Drives the default TTS locale
additional_languagesarray of stringExtra language codes the agent supports (es or BCP-47 es-ES)
productstringProduct tier. One of: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack
thinking_levelstringbase or extra. Derived from product for Storm tiers
background_trackstringAmbient audio track name, or "NONE" for silence. Never null — a null write is coerced to "NONE"
acknowledgement_prompt_modestringauto (we generate the ack prompt) or manual (you supply it)
acknowledgement_promptstringUsed when acknowledgement_prompt_mode="manual". Storm-tier only
acknowledgement_feedback_modestringWhile a Storm agent thinks: acknowledgement (spoken filler) or tick (ticking sound)
inbound_speak_orderstringWho talks first on inbound calls. agent_first or caller_first
outbound_speak_orderstringWho talks first on outbound calls
voicemail_actionstringWhat to do when a voicemail system answers: prompt (let the agent decide), hangup, or message (leave a message)
voicemail_messagestringTTS template for voicemail_action="message", max 2000 chars. Supports {agent_name} and {org_name} variables
silence_interval_secondsintegerSeconds between "are you still there?" check-ins. 10–120, default 15
silence_max_checkinsintegerCheck-ins before the agent hangs up. 1–10, default 3
silence_checkins_enabledbooleanWhether the agent proactively checks in during caller silence. Default true
silence_hangup_secondsintegerCaller-idle seconds before a silent hangup when check-ins are disabled. 60–3600, default 600
connect_tone_enabledbooleanPlay a short connection tone before the agent begins. Default false
play_sound_assetstringPrompt-callable sound asset: "" (none), chime, ding, or beep
speak_uninterruptible_enabledbooleanOffer the built-in tool for speaking a required passage without interruption. Default false
consent_announcement_enabledbooleanPlay the configured AI/recording announcement at call start. Default true; disabling requires an active consent attestation where applicable
consent_announcement_textstringAnnouncement played verbatim at call start; must not be blank and is limited to 2000 characters in the dashboard
consent_disabled_reasonstringRead-only platform-managed status associated with an announcement exception. Clients cannot set it
audio_context_modestringStorm audio context window: full or reduced (lower latency, less prior audio)
watchdog_enabledbooleanEnables live supervision of agent turns (Storm). Default false
additional_audio_contextboolean | nullInclude 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
integrationsarrayLinked Integration objects
knowledge_basesarrayAttached knowledge bases (full objects)
knowledge_documentsarrayIndividually attached knowledge documents
mcp_serversarrayAttached MCP servers (full objects)
widget_enabledbooleanWhether the agent is reachable via the embeddable web widget
split_testing_enabledbooleanWhether calls are split across A/B variants
created_at, updated_attimestampISO 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:

FieldTypeDescription
draft_<field>same as field | nullnull = no pending change for that field; non-null = the value staged for the next deploy
draft_updated_attimestamp | nullWhen the draft was last edited
has_draftbooleanTrue 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

ProductOptimized forAcknowledgement
sparkCost — fast and efficient for simple tasks
boltSpeed — the lowest-latency tier, balanced for most use cases
storm-baseIntelligence — complex prompts and reasoning
storm-base-with-ackIntelligenceSpoken filler (or tick) while thinking
storm-extraDeepest reasoning
storm-extra-with-ackDeepest reasoningSpoken filler (or tick) while thinking

List agents

cURL
curl https://api.thunderphone.com/v1/agents \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Python
agents = requests.get(
    "https://api.thunderphone.com/v1/agents",
    headers={"Authorization": "Bearer sk_live_YOUR_API_KEY"},
).json()
Node.js
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
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
  }'
Python
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()
FieldTypeRequiredDescription
namestringyes1–255 chars
promptstringyesSystem prompt
voicestringyesVoice 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_languagestringnoDefaults to en
additional_languagesarray of stringnoISO 639 (es) or BCP-47 (es-ES) codes
productstringnoDefaults to spark
thinking_levelstringnoIgnored for Storm tiers (derived from product)
background_trackstringnoTrack name or "NONE"
acknowledgement_prompt_modestringnoauto or manual. Only meaningful for Storm-with-ack products
acknowledgement_promptstringnoRequired if acknowledgement_prompt_mode="manual"
acknowledgement_feedback_modestringnoacknowledgement (default) or tick
inbound_speak_orderstringnoDefaults to agent_first
outbound_speak_orderstringnoDefaults to caller_first
voicemail_actionstringnoprompt (default), hangup, or message
voicemail_messagestringno≤ 2000 chars
silence_interval_secondsintegerno10–120, default 15
silence_max_checkinsintegerno1–10, default 3
silence_checkins_enabledbooleannoDefault true
silence_hangup_secondsintegerno60–3600, default 600; used when check-ins are disabled
connect_tone_enabledbooleannoDefault false
play_sound_assetstringno"" (none), chime, ding, or beep
speak_uninterruptible_enabledbooleannoOffer the uninterruptible speech tool. Default false
consent_announcement_enabledbooleannoDefaults to true. New agents must be created with it enabled; disabling later requires the attestation workflow where applicable
consent_announcement_textstringnoNon-blank call-start announcement text
audio_context_modestringnofull (default) or reduced
watchdog_enabledbooleannoDefault false
additional_audio_contextboolean | nullnoOverride whether the last few caller-audio turns are included. null keeps the direction-based default
widget_enabledbooleannoDefaults to true
split_testing_enabledbooleannoDefault false
integration_idsarray of UUIDnoLink reusable integrations by id
knowledge_base_idsarray of UUIDnoAttach knowledge bases
knowledge_document_idsarray of UUIDnoAttach individual knowledge documents
mcp_server_idsarray of UUIDnoAttach 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
curl https://api.thunderphone.com/v1/agents/12 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Python
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
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…"}'
Python
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
curl -X POST https://api.thunderphone.com/v1/agents/12/deploy \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Python
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
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.


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 packagestatement_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
curl https://api.thunderphone.com/v1/agents/12/consent-attestations?locale=en \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Response (excerpt)
{
  "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
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"}'
Response
{
  "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": ""
}
StatusCondition
201Attestation recorded (POST)
400affirmed 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
403Caller is not an org admin

Delete an agent

cURL
curl -X DELETE https://api.thunderphone.com/v1/agents/12 \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Python
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
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)"}'
Python
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()
FieldTypeRequiredDescription
namestringnoDefaults 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
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"
  }'
Python
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()
FieldTypeRequiredDescription
target_org_idintegeryesFor mode="move" it must differ from the source org id
modestringnocopy (default — keep source intact) or move (re-home the agent)
namestringnoOverride 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
curl 'https://api.thunderphone.com/v1/agents/12/versions?limit=20' \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
Python
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 paramDefaultDescription
limit20Max 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"]
    }
  ]
}
FieldTypeDescription
idintegerVersion row id
revisioninteger1-based, increments per deploy
created_by_idinteger | nullUser id that deployed the revision
snapshotobjectFull deployed config at this revision
changed_fieldsarray of stringFields 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
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..."}'
Response
{
  "detected_language_code": "es",
  "detected_language_name": "Spanish",
  "mismatch": true,
  "provider": "gemini"
}

Translate prompt

cURL
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"
  }'
Response
{
  "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
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"
  }'
Response
{
  "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"
Response
{
  "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 paramDescription
languagesComma-separated codes — only voices supporting all of them are returned
categoryFilter by voice category
genderFilter by voice gender

The name is what you set on an agent's voice field.