# Create a voice agent (/docs/api/voice-agents/agents/createAgent)

> For the complete documentation index, see [llms.txt](https://docs.tandemcx.ai/llms.txt). Any page on this site can be fetched as Markdown by appending `.md` to its URL.

`POST /v1/agent-media/agents`

Create a voice agent

Create an agent from a full configuration. Supply `agentId` to choose the id yourself, or omit it and Tandem assigns one.

A supplied id that already names an agent is refused with `409 create_upsert_retired` and nothing is written. This used to be an upsert; it cannot be one now, because a create writes the configuration to a version rather than to the agent itself, so repeating it over an existing agent would take that agent's configuration off the air and publish nothing in its place. Send configuration to that agent's version routes instead.

That also means a create interrupted partway through cannot be finished by repeating it: the agent exists, so the repeat is refused. Save a draft to it with `PUT /v1/agent-media/agents/{agentId}/versions/{version}`, or delete the agent and create it again.

**The agent is created as a draft, not published.** The configuration you send is written to version `0.1` and the agent has no `publishedVersion`, so it answers no calls until you publish that version with `POST /v1/agent-media/agents/{agentId}/versions/{version}/publish`. The response body is the agent's identity — it carries no configuration and no published pointer, both of which are absent by design. Read the configuration back from the version.

The agent is versioned from birth either way, so `PUT /v1/agent-media/agents/{agentId}` will not accept a configuration for it — every change goes to a draft and is published from there.

## Request

**Base URL:** `https://api.tandemcx.ai`

**Authentication:** `Authorization: Bearer <token>` — A Tandem bearer token. Requests are scoped to the organization the token belongs to; no organization header is required.

### Body (`application/json`) — required

Everything an agent and a template configuration share. The three fields that differ between them — `tools`, `knowledge`, `sourceTemplateId` — are added by `AgentConfig` and `AgentTemplateConfig` respectively, because a template replaces an organization-scoped id with a requirement rather than adding to it, and `allOf` can only add.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agentId` | string | No | Optional caller-chosen id. Must contain no `#` or control characters. |
| `name` | string | Yes | Internal label; callers never hear it. |
| `systemPrompt` | string | Yes | The agent's role, capabilities, and constraints. Rejected (create/update 400 `invalid_config`) when it exceeds 100,000 bytes once JSON-encoded — not the same as its raw character count, since escaping grows it. |
| `greeting` | string | No | Spoken verbatim as the agent's first turn. |
| `interruptable` | boolean | No | Whether caller speech interrupts the agent mid-sentence (barge-in). |
| `channels` | array of enum: `voice` \| `chat` | No | Which channels this agent answers on. Defaults to both, so a config that omits it behaves exactly as one written before the field existed. It changes one rule: `slots.tts` is required only while `voice` is present. A chat-only agent saves and publishes with no voice, since the chat wire never carries one. A voice that IS set is validated on either channel — a voice that is there has to be a real one. An empty array is rejected. |
| `slots` | object | Yes | — |
| `slots.llm` | object | Yes | — |
| `slots.llm.model` | string | Yes | A model `id` from the models endpoint. |
| `slots.llm.settings` | object | No | — |
| `slots.llm.settings.maxTokens` | integer | No | — |
| `slots.llm.settings.temperature` | number | No | Capped at 1 for models Tandem hosts; up to 2 for vendor-hosted models. Claude Haiku 5.5 accepts only 1; leave it unset for that model. |
| `slots.llm.settings.reasoning` | object | No | — |
| `slots.stt` | object | No | Omit for a model whose `audioIn` is true; sending one is rejected. |
| `slots.stt.provider` | string | Yes | — |
| `slots.stt.settings` | object | No | — |
| `slots.stt.settings.language` | string | No | A Transcribe **streaming** language code (e.g. `en-US`) — a narrower set than the voice languages Polly offers, and validated against that set at save: a voice-language tag such as `es` is rejected rather than forwarded, because Transcribe refuses it at connect and the agent then transcribes nothing for the rest of the call. `GET /providers/stt/aws-transcribe/options/language` lists every accepted value. Optional; when absent, it defaults to following the agent's voice language, so an operator who never sets it still gets a language-matched transcription rather than Transcribe's own `en-US` default. |
| `slots.tts` | object | No | Required unless the model's `audioOut` is true. |
| `slots.tts.voiceProvider` | string | Yes | — |
| `slots.tts.settings` | object | Yes | — |
| `slots.tts.settings.language` | string | Yes | — |
| `slots.tts.settings.voiceId` | string | Yes | — |
| `slots.tts.settings.engine` | string | Yes | — |
| `slots.tts.settings.polly` | object | No | Voice-character tuning for a `polly` voice. Sending it for any other `voiceProvider` is rejected at save. Every field is optional; an absent field leaves Polly's own default in effect. |
| `slots.tts.settings.elevenlabs` | object | No | Voice-character tuning for an `elevenlabs` voice. Sending it for any other `voiceProvider` is rejected at save. Every field is optional; an absent field leaves ElevenLabs' own default in effect. |
| `slots.additionalVoices` | array of object | No | Languages beyond the primary voice. One recognition language per base. Rejected on audio-out models and together with `slots.stt.settings.language`. |
| `slots.additionalVoices[].voiceProvider` | string | Yes | — |
| `slots.additionalVoices[].settings` | object | Yes | — |
| `slots.additionalVoices[].settings.language` | string | Yes | — |
| `slots.additionalVoices[].settings.voiceId` | string | Yes | — |
| `slots.additionalVoices[].settings.engine` | string | Yes | — |
| `slots.additionalVoices[].settings.polly` | object | No | Voice-character tuning for a `polly` voice. Sending it for any other `voiceProvider` is rejected at save. Every field is optional; an absent field leaves Polly's own default in effect. |
| `slots.additionalVoices[].settings.elevenlabs` | object | No | Voice-character tuning for an `elevenlabs` voice. Sending it for any other `voiceProvider` is rejected at save. Every field is optional; an absent field leaves ElevenLabs' own default in effect. |
| `greetingParts` | array of object | No | Replaces `greeting` on calls. Each `language` must be a voice language of the agent. Templates render per part. |
| `greetingParts[].language` | string | Yes | — |
| `greetingParts[].text` | string | Yes | — |
| `variables` | array of object | No | Agent-local values, referenced as `{{agent.<name>}}` wherever the configuration takes text. The organization-wide equivalent is `{{org.<name>}}`, managed separately. A reference to a name that does not exist is rejected at save time. |
| `variables[].name` | string | Yes | Unique within the agent. Referenced as `{{agent.<name>}}` in the system prompt, the greetings, and a tool's constant parameters. |
| `variables[].value` | string | Yes | Always a string. An empty value is allowed and means "set to nothing", which is not the same as leaving the variable out. |
| `variables[].description` | string | No | A note for whoever edits the agent next; never sent to the model. |
| `dispositions` | array of object | No | — |
| `dispositions[].name` | string | Yes | Unique within the agent, and becomes an exit on the AI Voice Agent node. `default` and `fallback` are reserved. |
| `dispositions[].description` | string | Yes | Read by the model to choose this outcome, so write it as the condition for choosing it rather than as a label. |
| `builtinTools` | object | No | The built-in call-control tools this agent may use. Every field defaults, so an agent saved without this block behaves exactly as before. No tool here requires a connected integration: the outside transfer is placed over the ordinary phone network, and everything else is internal to Tandem or local to the call. |
| `builtinTools.transferToExternal` | object | No | Dials a destination over the ordinary phone network. Rejected at save (400 `invalid_config`) when `enabled` is true and `destinations` is empty. |
| `builtinTools.transferToExternal.enabled` | boolean | No | — |
| `builtinTools.transferToExternal.destinations` | array of object | No | — |
| `builtinTools.transferToExternal.destinations[].name` | string | Yes | The value the agent picks from. Composed into the tool's enum, so the model never supplies a dial target of its own. |
| `builtinTools.transferToExternal.destinations[].description` | string | Yes | When the agent should choose this destination. |
| `builtinTools.transferToExternal.destinations[].e164` | string | Yes | Bare E.164 (`+15551234567`) — no spaces, dashes, or parentheses. Also checked against `libphonenumber-js` for a plausible country code and length, which this pattern alone does not guarantee. |
| `builtinTools.transferToVoiceAgent` | object | No | Hands the live call to another of this org's voice agents. Rejected at save (400 `invalid_config`) when `enabled` is true and `destinations` is empty. |
| `builtinTools.transferToVoiceAgent.enabled` | boolean | No | — |
| `builtinTools.transferToVoiceAgent.contextCarryover` | enum: `none` \| `summary` \| `verbatim` | No | How much of the conversation travels to the receiving agent. |
| `builtinTools.transferToVoiceAgent.destinations` | array of object | No | — |
| `builtinTools.transferToVoiceAgent.destinations[].name` | string | Yes | The value the agent picks from. Composed into the tool's enum, so the model never supplies a dial target of its own. |
| `builtinTools.transferToVoiceAgent.destinations[].description` | string | Yes | When the agent should choose this destination. |
| `builtinTools.transferToVoiceAgent.destinations[].targetAgentId` | string | Yes | — |
| `builtinTools.captureDtmf` | object | No | — |
| `builtinTools.captureDtmf.enabled` | boolean | No | — |
| `builtinTools.captureDtmf.maxDigits` | integer | No | — |
| `builtinTools.captureDtmf.terminator` | enum: `#` \| `*` \| `none` | No | — |
| `builtinTools.captureDtmf.timeoutMs` | integer | No | When `enabled` is true, capped a second below the budget a native tool call gets — 14,000 ms by default, or `reliability.toolTimeoutMs` minus 1,000 ms when that is set. Past that budget the executor's own timeout cuts the collection short first and the caller's digits are lost, so a larger value is rejected at save (`400 invalid_config`). A larger stored value is clamped to the same bound when the call loads it. |
| `builtinTools.captureDtmf.interDigitTimeoutMs` | integer | No | Must be less than `timeoutMs` when `enabled` is true, or the save is rejected (`400 invalid_config`) — a gap timer at or above the overall timeout can never fire. |
| `builtinTools.captureDtmf.interruptsSpeech` | boolean | No | Overrides `interruptable` for keypresses only, so an agent that plays through a disclaimer still honors "press 0". |
| `builtinTools.recordingControl` | object | No | One toggle exposing both `pause_recording` and `resume_recording`. |
| `builtinTools.recordingControl.enabled` | boolean | No | — |
| `builtinTools.currentTime` | object | No | — |
| `builtinTools.currentTime.enabled` | boolean | No | — |
| `builtinTools.currentTime.timezone` | string | No | An IANA timezone name (e.g. `America/Chicago`). |
| `builtinTools.setWorkflowVariable` | object | No | Offered only on a call that reached the agent from a workflow. On any other initiator the tool is omitted from the session entirely. Unlike the transfer tools above, enabling this with an empty `variables` list is accepted by the server. |
| `builtinTools.setWorkflowVariable.enabled` | boolean | No | — |
| `builtinTools.setWorkflowVariable.variables` | array of object | No | — |
| `builtinTools.setWorkflowVariable.variables[].name` | string | Yes | The value the agent picks from. Composed into the tool's enum, so the model never supplies a dial target of its own. |
| `builtinTools.setWorkflowVariable.variables[].description` | string | Yes | When the agent should choose this destination. |
| `dtmf` | object | No | Keypad input as ordinary caller input. A run of keypresses is grouped into one caller turn and delivered to the agent the way a spoken sentence is, with no tool call armed. Separate from `builtinTools.captureDtmf`, which is a tool the agent calls to ask a bounded question and block on the answer. Both may be on: while a `captureDtmf` collection is open the keypresses go into it instead, and digits already typed into an open group are carried into that collection rather than delivered twice. |
| `dtmf.enabled` | boolean | No | Off by default. Turning it on changes what every keypress on a live call does, so no already-published agent changes behavior on a deploy. |
| `dtmf.interDigitGapMs` | integer | No | Silence after a keypress that closes the group. Long enough that a caller reading a number off a card is not cut in half, short enough that a single menu digit is not a long wait. The agent is held silent for this long after the caller's last keypress, so a large value is a long silence; the agent editor caps it at 10000, while this API does not. Not bounded by the tool-execution budget the way `captureDtmf.timeoutMs` is — no tool call is held open by any of this. |
| `dtmf.terminator` | enum: `#` \| `*` \| `none` | No | Key that closes the group immediately. It is consumed, never delivered: a caller who types `1234#` sends the agent `1234`. A terminator pressed on its own closes an empty group, and an empty group is never delivered, so it sends nothing. Use `none` for an agent that wants `#` itself to reach the model. |
| `dtmf.maxDigits` | integer | No | Digit count that closes the group immediately. |
| `dtmf.interruptsSpeech` | boolean | No | Whether the first keypress of a group stops the agent mid-sentence. Overrides the agent-level `interruptable` for keypresses only, for the same reason `captureDtmf.interruptsSpeech` does: a keypress cannot be a false speech detection, so an agent that plays a disclaimer through barge-in should still honor "press 0". Setting it false delays the interruption rather than preventing one — the closed group arrives as a caller turn, and a caller turn cancels whatever the agent was saying. |
| `thinkingCue` | one of: object \| object \| object | No | What the caller hears while a tool call runs. `none` is silence. |
| `vad` | object | No | Turn-taking sensitivity. Every field is individually optional: whatever is absent falls back to the underlying voice pipeline's own default, and a partial block is a legal save. |
| `vad.silenceMs` | integer | No | How long the caller must pause before the agent replies. Total end-of-turn wait is `silenceMs + endOfTurnGraceMs`. |
| `vad.threshold` | number | No | How confident the detector must be that it hears speech. |
| `vad.startMs` | integer | No | How long speech must continue before it counts as the caller's turn starting. |
| `vad.minVolume` | number | No | The minimum volume level treated as speech rather than background noise. |
| `vad.endOfTurnGraceMs` | integer | No | The window after `silenceMs` elapses in which the caller may resume without the agent replying. Read only while `turnDetection.mode` is `silence`: the smart turn model decides end-of-turn itself and has no equivalent setting, so this value is stored but never applied in `smart` mode. |
| `vad.bargeInMinWords` | integer | No | How many words the caller must say before their speech interrupts the agent. Absent means any sound interrupts, which is the behavior every agent has today. Ignored while `interruptable` is false — there is no barge-in for it to refine. Setting it is a trade, not a free filter: interruption is then detected from the first words transcribed rather than from the voice detector's speech onset, so barge-in gets slower on genuine interruptions too, not only on coughs and backchannels. |
| `turnDetection` | object | No | Which strategy decides the caller has finished speaking. `vad` tunes what counts as the caller *starting* to speak; this decides when they stop. Absent behaves exactly as `mode: silence`, which is what every agent has always used. The **Turn detection** control on the Voice & Speech tab is being rolled out — if it isn't there, the rollout hasn't reached your organization yet. This API accepts the block either way, and the voice pipeline honors whatever is stored: the rollout gates the editor control, not the capability. |
| `turnDetection.mode` | enum: `silence` \| `smart` | No | `silence` waits out the two timers in `vad` (`silenceMs` + `endOfTurnGraceMs`). `smart` runs an on-device model that predicts end-of-turn from the caller's prosody, so a mid-sentence pause or a trailing "so…" isn't treated as the end of a turn. `smart` is not free: it costs roughly 19 MB of memory and one thread for each concurrent call using it, plus about 20 ms of model loading while the call is set up. |
| `turnDetection.smartStopMs` | integer | No | `smart` only. The longest silence the model tolerates before declaring the turn over on its own — the ceiling on a turn it mispredicted, not the wait on a normal one. It is **not** the replacement for `vad.endOfTurnGraceMs`, which `smart` mode never reads: this default of 3000 is far longer than silence mode's default total wait of 800 ms (`vad.silenceMs` 200 + `vad.endOfTurnGraceMs` 600). |
| `turnDetection.preSpeechMs` | integer | No | `smart` only. How much audio from before the detected speech onset the model gets to hear. Milliseconds, like every other `*Ms` field here. |
| `turnDetection.maxDurationSecs` | number | No | `smart` only. The longest stretch of caller audio the model scores at once; anything older is trimmed off the front. Seconds — the one field in this block that is neither milliseconds nor required to be a whole number. |
| `memory` | object | No | — |
| `memory.depth` | enum: `none` \| `warm` \| `light` \| `continuity` | No | How much the agent recalls about a returning caller. |
| `memory.customizeGreeting` | boolean | No | Re-word the greeting for a recognized caller. Also requires a `depth` other than `none` and a `greeting`. |
| `safety` | object | No | — |
| `safety.redactPii` | boolean | No | Hide caller PII from the agent. An agent that must verify identities or look up accounts needs this off. |
| `safety.steeringRules` | array of string | No | Policies checked before each tool call. They govern actions, not speech. An empty array is treated as absent. |
| `skills` | object | No | — |
| `skills.builtins` | array of string | No | Platform skills to enable, by name. List the available names with `GET /v1/agent-media/skills`. A name that is not in the catalog is dropped with a warning and the call runs without it. |
| `skills.custom` | array of object | No | Skills you write yourself. |
| `skills.custom[].name` | string | Yes | Lowercase letters, digits, and hyphens, up to 64 characters. A custom skill whose name matches a platform skill's is dropped at call time rather than replacing it. |
| `skills.custom[].description` | string | Yes | What the skill is for, and when it applies. This is what tells the agent to follow the procedure, so write it as a trigger — "Use when the caller asks to return a part" — and keep it to a sentence or two. |
| `skills.custom[].instructions` | string | Yes | The procedure itself. Written for speech: no tables, no lists to read aloud, one question at a time, and a stated limit on retries. A skill is instructions, not permissions — it cannot restrict which tools the agent may call. Use `safety.steeringRules` for that. Keep the enabled set tight: an agent is given its skills up front, but past a large combined size it switches to fetching them mid-call, which is less reliable. |
| `analysis` | object | No | Post-call analysis: a separate model pass over the finished transcript, extracting the built-in judgments and any fields you define. Whether the transcript itself is kept is a separate setting, `retention.transcripts`, and analysis does not control it. With retention on — including by default, for an agent that has not set `safety.redactPii` — the transcript is stored whether or not analysis is enabled. With retention off, enabling analysis is the only thing that stores one. |
| `analysis.enabled` | boolean | No | Rejected when true with no field and no built-in enabled: there would be nothing to extract. |
| `analysis.builtins` | object | No | — |
| `analysis.builtins.sentiment` | object | No | How the caller sounded: `positive`, `neutral`, `negative`, or `unknown`. |
| `analysis.builtins.sentiment.enabled` | boolean | No | — |
| `analysis.builtins.sentiment.prompt` | string | No | Rewords the judgment for this agent. It cannot widen the answer set — `sentiment` and `outcome` always return one of their own fixed values. |
| `analysis.builtins.outcome` | object | No | Whether the call achieved what the caller wanted: `success`, `failure`, or `unknown`. |
| `analysis.builtins.outcome.enabled` | boolean | No | — |
| `analysis.builtins.outcome.prompt` | string | No | Rewords the judgment for this agent. It cannot widen the answer set — `sentiment` and `outcome` always return one of their own fixed values. |
| `analysis.builtins.summary` | object | No | A short prose summary of the call. |
| `analysis.builtins.summary.enabled` | boolean | No | — |
| `analysis.builtins.summary.prompt` | string | No | Rewords the judgment for this agent. It cannot widen the answer set — `sentiment` and `outcome` always return one of their own fixed values. |
| `analysis.fields` | array of object | No | Values you define yourself. |
| `analysis.fields[].name` | string | Yes | The key the extracted value is returned under, in `analysis.values`. Must be unique within the agent, and cannot be `sentiment`, `outcome`, or `summary` — those are the built-ins' own names. |
| `analysis.fields[].type` | enum: `string` \| `number` \| `integer` \| `boolean` | Yes | — |
| `analysis.fields[].enum` | array of string | No | Allowed values, making this a choice field. Only meaningful when `type` is `string`; sending it on any other type is rejected. |
| `analysis.fields[].prompt` | string | Yes | What to extract, addressed to the model reading the transcript. |
| `analysis.fields[].required` | boolean | No | Whether the model must answer. An optional field the transcript gives no basis for is simply absent from `analysis.values`. |
| `retention` | object | No | What a finished call leaves behind. Independent of `analysis`: an organization can keep transcripts without paying for a model pass over them, and can analyze calls without this block set at all. |
| `retention.transcripts` | boolean | No | Whether this agent's call transcripts are stored (for 90 days). Deliberately has no default: omitting it means you have not decided, and Tandem applies the platform default — on for an ordinary agent, but off for an agent with `safety.redactPii` set, because a stored transcript holds the caller's words unredacted (that flag guards the model request, not the transcript). An explicit `true` or `false` is your decision and wins in either case. |
| `idle` | object | No | What happens when the caller goes silent mid-call. |
| `idle.enabled` | boolean | No | Off by default, so no existing agent starts hanging up on deploy. |
| `idle.timeoutMs` | integer | No | How long the caller may stay silent before this behavior triggers. |
| `idle.action` | enum: `reprompt` \| `endCall` | No | `reprompt` speaks `prompt` directly and waits again; `endCall` speaks it as a farewell and hangs up. |
| `idle.prompt` | string | No | What the agent says when idle triggers. Required when `enabled` is true and `action` is `reprompt` — a reprompt with nothing to say never re-arms, so the idle behavior fires once and then stops. Optional for `endCall`, which is a valid silent hangup. |
| `idle.maxReprompts` | integer | No | Consecutive idle periods tolerated before `onExhausted` decides. 0 means the first idle period goes straight to `onExhausted`, and since nothing reprompts, `prompt` is never spoken. 0 together with `onExhausted: continue` makes the whole idle block inert. |
| `idle.onExhausted` | enum: `endCall` \| `continue` | No | What happens once `maxReprompts` is used up. |
| `idle.disposition` | string | No | Recorded when idle ends the call. Must be one of this agent's own `dispositions`. Only an idle path that ends the call records it, so `action: reprompt` with `onExhausted: continue` never does. |
| `limits` | object | No | Caller-visible bounds on the call itself. |
| `limits.maxCallDurationMs` | integer | No | The call is ended once this elapses. Absent means unbounded. |
| `limits.warnBeforeEndMs` | integer | No | How long before `maxCallDurationMs` the warning is spoken. Must be strictly less than `maxCallDurationMs`, and required whenever `warnPrompt` is set. |
| `limits.warnPrompt` | string | No | What the agent says as the call-duration warning. Paired with `warnBeforeEndMs`: each is required whenever the other is set, because the warning timer is armed only when both are present. |
| `reliability` | object | No | Failure-handling tuning. Each field falls back to a fixed default when absent, so an empty block is exactly today's behavior. |
| `reliability.maxConsecutiveTurnFailures` | integer | No | Consecutive failed turns tolerated before recovery gives up. Falls back to 3. |
| `reliability.toolTimeoutMs` | integer | No | How long a tool call may run before it's treated as failed. Falls back to 15000. Capped at 20 seconds because the voice agent stops waiting on a tool result at that point and hands the model a timeout, so a longer budget would never take effect. Lowering this also lowers the ceiling on `captureDtmf.timeoutMs`, which must stay at least a second inside it. |
| `reliability.steeringJudgeTimeoutMs` | integer | No | How long the steering judge may take before it's skipped for that turn. Falls back to 2000. |
| `reliability.handoffMaxHistoryMessages` | integer | No | How much conversation history rides along on a workflow handoff. Falls back to 40. |
| `reliability.modelRetryMaxAttempts` | integer | No | Attempts made against the model provider before a turn is given up on. Falls back to 3, which is a deliberate change from the SDK's own default of 6: those defaults are written for a batch job, and on a phone call that backoff is dead air the caller hears, followed by a failed turn anyway. |
| `reliability.modelRetryInitialDelayMs` | integer | No | Delay before the first retry, in milliseconds. Falls back to 1000 — again a deliberate departure from the SDK's own default, which starts its backoff at 4000. |
| `reliability.modelRetryMaxDelayMs` | integer | No | Ceiling the retry backoff grows to, in milliseconds. Falls back to 4000, versus the SDK's own default ceiling of 240000 — a delay written for a batch job, not a call the caller is still on. |
| `conversation` | object | No | How the agent's message history is kept inside the model's context window on a long call. An empty block behaves like `strategy: slidingWindow` with no other fields set — a 40-message window that drops the oldest messages first. |
| `conversation.strategy` | enum: `slidingWindow` \| `summarizing` | No | `slidingWindow` drops the oldest messages once `windowSize` is exceeded. `summarizing` replaces dropping with summarizing older turns down to a shorter form instead of discarding them, at the cost of added latency on the turn where it fires. There is deliberately no `none`: an unmanaged history grows until the model's context window overflows mid-call, and the call dies. The model used to produce a summary is pinned by the platform and is not configurable here — a configurable summarizer model would run a full-size inference on the caller's critical path at the exact moment the context window overflows. |
| `conversation.windowSize` | integer | No | `slidingWindow` only. Messages kept before the oldest are dropped. Absent means the SDK's own default of 40. |
| `conversation.summaryRatio` | number | No | `summarizing` only. Fraction of the history replaced by a summary each time summarization fires. Absent means the SDK's own default of 0.3. |
| `conversation.preserveRecentMessages` | integer | No | `summarizing` only. Most recent messages kept verbatim, never folded into the summary. Absent means the SDK's own default of 10. |
| `conversation.pinFirst` | integer | No | Messages at the start of the call that are never dropped or summarized away, regardless of `strategy`. Use this to keep the caller's opening turns — why they called, an account number, a consent they gave — for the whole call. |
| `conversation.proactiveCompressionThreshold` | number | No | Fraction of the context window that triggers compression proactively, instead of waiting for an overflow. Applies regardless of `strategy`. Absent means reactive-only, which is the SDK's own default. |
| `vocabulary` | object | No | Terms the agent must hear and say correctly on a call, such as product names. Voice only. Mishearings are replaced with the term in the caller's transcript, the voice says each term as its `sayAs`, terms with `heardAs` are listed in the agent's instructions, and a published agent's terms also tune speech recognition — see `GET /v1/agent-media/agents/{agentId}/vocabulary-status`. |
| `vocabulary.terms` | array of object | No | — |
| `vocabulary.terms[].term` | string | Yes | How the term is written, for example `Quinoa`. |
| `vocabulary.terms[].sayAs` | string | No | How the voice should say it, for example `KEEN-wah`. Spoken only; tool arguments and variables keep `term`. |
| `vocabulary.terms[].heardAs` | array of string | No | How speech recognition tends to mishear it, for example `keen wah`. Replaced with `term` in the caller's transcript and listed in the agent's instructions. Not sent to speech recognition. |
| `dataCollection` | object | No | The fields this agent collects during a conversation. Setting it turns on the `record_data` tool (and, in chat, `request_data_input`). Live conversations use the published version, so publish after changing it. Omit to collect nothing. Read what was collected from `GET /v1/agent-media/calls/{conversationId}/collected` or `GET /v1/agent-media/chat-conversations/{conversationId}/collected`. |
| `dataCollection.fields` | array of object | Yes | Unique by `key`. |
| `dataCollection.fields[].key` | string | Yes | The name the agent records the value under, which also labels the value on the call or chat record. Lowercase letters, digits, and underscores, starting with a letter, at most 40 characters. Unique within the agent's fields. |
| `dataCollection.fields[].label` | string | Yes | What the field is called, shown to the person and to you. |
| `dataCollection.fields[].kind` | enum: `full_name` \| `email` \| `phone` \| `postal_address` \| `postal_code` \| `date` \| `date_of_birth` \| `number` \| `yes_no` \| `alphanumeric_code` \| `free_text` | Yes | The type of value. Each kind validates and normalizes the answer, so an email is lowercased and a phone number is stored in E.164 format. |
| `dataCollection.fields[].required` | boolean | No | Tells the agent the field matters, so it aims to collect it. Not enforced: a caller or visitor can still skip it. |
| `dataCollection.fields[].prompt` | string | No | What to ask. Empty means the agent words the question from the label. |
| `tools` | array of one of: object \| object \| object \| object \| object | No | — |
| `knowledge` | object | No | — |
| `knowledge.kbId` | string | Yes | An existing knowledge base's id. |
| `knowledge.maxResults` | integer | No | — |
| `knowledge.minScore` | number | No | Passages scoring below this are discarded. When omitted, 0.4 applies when a conversation starts. A save that changes `kbId` and sends the stored value unchanged drops it, so the new knowledge base's default applies: an agent update or version update compares against what it replaces, and a version create against its `baseVersion`. |
| `sourceTemplateId` | string | No | The template this agent was created from, if any. Set once when the agent is created from a template and preserved across updates; read-only in practice. There is no link back — editing the template does not change this agent. |

## Responses

### 201 — The created agent.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agent` | object | No | An agent as a read returns it. `publishedVersion` is the only field to branch on, and it alone decides where the live configuration is — set, fetch the version it names; absent, the configuration is on this object and is what a call serves. Only `id`, `createdAt` and `updatedAt` are guaranteed. Nothing else is marked required because a versioned agent need not carry a configuration here at all; one that was migrated onto versioning does keep its pre-migration copy, frozen, but that is a fact about how it got here rather than something to rely on. |
| `agent.id` | string | Yes | — |
| `agent.name` | string | No | Internal label; callers never hear it. |
| `agent.publishedVersion` | string | No | The version this agent serves on a live call. Absent means the agent is serving nothing — which is every agent between its create and its first publish, since a create writes a draft and publishes nothing. It is also absent for an agent that predates versioning and has not been migrated, and that one IS serving something: the configuration fields on this object. Tell the two apart by whether those fields are present. Set, it does not mean the configuration fields here are gone — an agent Tandem migrated keeps them, frozen at migration and no longer what a call serves. Read-only here; publishing a version is what moves it. |
| `agent.appearance` | object | No | An agent's hand-picked avatar parts. Every part is optional; one left out keeps what the agent's name generates. |
| `agent.appearance.hue` | integer | No | — |
| `agent.appearance.colors` | array of object \| null | No | The avatar's four colors, in order: iris, iris accent, background, and background highlight. The first replaces `hue`. A missing or `null` entry leaves that color to the agent's name. |
| `agent.appearance.colors[].hue` | number | Yes | — |
| `agent.appearance.colors[].saturation` | number | Yes | Multiplies the color's chroma; 1 is standard. |
| `agent.appearance.colors[].lightness` | number | Yes | Added to the color's lightness; 0 is standard. |
| `agent.appearance.scheme` | enum: `mono` \| `square` \| `analogous` \| `triadic` | No | Accepted and ignored. Replaced by `colors`. |
| `agent.appearance.shape` | enum: `circle` \| `squircle` \| `hex` \| `oct` | No | — |
| `agent.appearance.pattern` | enum: `glow` \| `rings` \| `ticks` \| `ticks2` \| `spiral` \| `dashed` \| `bands` \| `leds` | No | `ticks2` is two offset rows of ticks. |
| `agent.appearance.pupil` | enum: `dot` \| `ring` \| `pill` \| `star` \| `none` | No | — |
| `agent.appearance.irisTexture` | enum: `fibers` \| `honeycomb` \| `none` \| `crosshatch` \| `speckle` \| `grain` \| `sectors` \| `cells` \| `dots` \| `circuit` \| `spokes` | No | — |
| `agent.appearance.ringCount` | enum: `2` \| `3` \| `4` | No | — |
| `agent.appearance.ringWidth` | number | No | — |
| `agent.appearance.ticks` | enum: `12` \| `16` \| `24` | No | — |
| `agent.appearance.dashLength` | number | No | — |
| `agent.appearance.dashGap` | number | No | — |
| `agent.appearance.rotation` | number | No | — |
| `agent.appearance.notches` | boolean | No | Drawn on a circle bezel only. |
| `agent.appearance.lid` | enum: `round` \| `flat` \| `smile` \| `wide` | No | — |
| `agent.appearance.texture` | enum: `none` \| `dots` \| `contours` \| `stars` \| `scanlines` | No | — |
| `agent.appearance.temper` | enum: `calm` \| `steady` \| `curious` \| `jittery` | No | — |
| `agent.avatarImageUrl` | string (uri) | No | The agent's uploaded picture, as a link valid for one hour. Absent when the agent has no picture. Set with `PUT /v1/agent-media/agents/{agentId}/avatar-image`. |
| `agent.createdAt` | string (date-time) | Yes | — |
| `agent.updatedAt` | string (date-time) | Yes | — |
| `agent.systemPrompt` | string | No | The agent's role, capabilities, and constraints. Rejected (create/update 400 `invalid_config`) when it exceeds 100,000 bytes once JSON-encoded — not the same as its raw character count, since escaping grows it. |
| `agent.greeting` | string | No | Spoken verbatim as the agent's first turn. |
| `agent.interruptable` | boolean | No | Whether caller speech interrupts the agent mid-sentence (barge-in). |
| `agent.channels` | array of enum: `voice` \| `chat` | No | Which channels this agent answers on. Defaults to both, so a config that omits it behaves exactly as one written before the field existed. It changes one rule: `slots.tts` is required only while `voice` is present. A chat-only agent saves and publishes with no voice, since the chat wire never carries one. A voice that IS set is validated on either channel — a voice that is there has to be a real one. An empty array is rejected. |
| `agent.slots` | object | No | — |
| `agent.slots.llm` | object | Yes | — |
| `agent.slots.llm.model` | string | Yes | A model `id` from the models endpoint. |
| `agent.slots.llm.settings` | object | No | — |
| `agent.slots.stt` | object | No | Omit for a model whose `audioIn` is true; sending one is rejected. |
| `agent.slots.stt.provider` | string | Yes | — |
| `agent.slots.stt.settings` | object | No | — |
| `agent.slots.tts` | object | No | Required unless the model's `audioOut` is true. |
| `agent.slots.tts.voiceProvider` | string | Yes | — |
| `agent.slots.tts.settings` | object | Yes | — |
| `agent.slots.additionalVoices` | array of object | No | Languages beyond the primary voice. One recognition language per base. Rejected on audio-out models and together with `slots.stt.settings.language`. |
| `agent.slots.additionalVoices[].voiceProvider` | string | Yes | — |
| `agent.slots.additionalVoices[].settings` | object | Yes | — |
| `agent.greetingParts` | array of object | No | Replaces `greeting` on calls. Each `language` must be a voice language of the agent. Templates render per part. |
| `agent.greetingParts[].language` | string | Yes | — |
| `agent.greetingParts[].text` | string | Yes | — |
| `agent.variables` | array of object | No | Agent-local values, referenced as `{{agent.<name>}}` wherever the configuration takes text. The organization-wide equivalent is `{{org.<name>}}`, managed separately. A reference to a name that does not exist is rejected at save time. |
| `agent.variables[].name` | string | Yes | Unique within the agent. Referenced as `{{agent.<name>}}` in the system prompt, the greetings, and a tool's constant parameters. |
| `agent.variables[].value` | string | Yes | Always a string. An empty value is allowed and means "set to nothing", which is not the same as leaving the variable out. |
| `agent.variables[].description` | string | No | A note for whoever edits the agent next; never sent to the model. |
| `agent.dispositions` | array of object | No | — |
| `agent.dispositions[].name` | string | Yes | Unique within the agent, and becomes an exit on the AI Voice Agent node. `default` and `fallback` are reserved. |
| `agent.dispositions[].description` | string | Yes | Read by the model to choose this outcome, so write it as the condition for choosing it rather than as a label. |
| `agent.builtinTools` | object | No | The built-in call-control tools this agent may use. Every field defaults, so an agent saved without this block behaves exactly as before. No tool here requires a connected integration: the outside transfer is placed over the ordinary phone network, and everything else is internal to Tandem or local to the call. |
| `agent.builtinTools.transferToExternal` | object | No | Dials a destination over the ordinary phone network. Rejected at save (400 `invalid_config`) when `enabled` is true and `destinations` is empty. |
| `agent.builtinTools.transferToExternal.enabled` | boolean | No | — |
| `agent.builtinTools.transferToExternal.destinations` | array of object | No | — |
| `agent.builtinTools.transferToVoiceAgent` | object | No | Hands the live call to another of this org's voice agents. Rejected at save (400 `invalid_config`) when `enabled` is true and `destinations` is empty. |
| `agent.builtinTools.transferToVoiceAgent.enabled` | boolean | No | — |
| `agent.builtinTools.transferToVoiceAgent.contextCarryover` | enum: `none` \| `summary` \| `verbatim` | No | How much of the conversation travels to the receiving agent. |
| `agent.builtinTools.transferToVoiceAgent.destinations` | array of object | No | — |
| `agent.builtinTools.captureDtmf` | object | No | — |
| `agent.builtinTools.captureDtmf.enabled` | boolean | No | — |
| `agent.builtinTools.captureDtmf.maxDigits` | integer | No | — |
| `agent.builtinTools.captureDtmf.terminator` | enum: `#` \| `*` \| `none` | No | — |
| `agent.builtinTools.captureDtmf.timeoutMs` | integer | No | When `enabled` is true, capped a second below the budget a native tool call gets — 14,000 ms by default, or `reliability.toolTimeoutMs` minus 1,000 ms when that is set. Past that budget the executor's own timeout cuts the collection short first and the caller's digits are lost, so a larger value is rejected at save (`400 invalid_config`). A larger stored value is clamped to the same bound when the call loads it. |
| `agent.builtinTools.captureDtmf.interDigitTimeoutMs` | integer | No | Must be less than `timeoutMs` when `enabled` is true, or the save is rejected (`400 invalid_config`) — a gap timer at or above the overall timeout can never fire. |
| `agent.builtinTools.captureDtmf.interruptsSpeech` | boolean | No | Overrides `interruptable` for keypresses only, so an agent that plays through a disclaimer still honors "press 0". |
| `agent.builtinTools.recordingControl` | object | No | One toggle exposing both `pause_recording` and `resume_recording`. |
| `agent.builtinTools.recordingControl.enabled` | boolean | No | — |
| `agent.builtinTools.currentTime` | object | No | — |
| `agent.builtinTools.currentTime.enabled` | boolean | No | — |
| `agent.builtinTools.currentTime.timezone` | string | No | An IANA timezone name (e.g. `America/Chicago`). |
| `agent.builtinTools.setWorkflowVariable` | object | No | Offered only on a call that reached the agent from a workflow. On any other initiator the tool is omitted from the session entirely. Unlike the transfer tools above, enabling this with an empty `variables` list is accepted by the server. |
| `agent.builtinTools.setWorkflowVariable.enabled` | boolean | No | — |
| `agent.builtinTools.setWorkflowVariable.variables` | array of object | No | — |
| `agent.dtmf` | object | No | Keypad input as ordinary caller input. A run of keypresses is grouped into one caller turn and delivered to the agent the way a spoken sentence is, with no tool call armed. Separate from `builtinTools.captureDtmf`, which is a tool the agent calls to ask a bounded question and block on the answer. Both may be on: while a `captureDtmf` collection is open the keypresses go into it instead, and digits already typed into an open group are carried into that collection rather than delivered twice. |
| `agent.dtmf.enabled` | boolean | No | Off by default. Turning it on changes what every keypress on a live call does, so no already-published agent changes behavior on a deploy. |
| `agent.dtmf.interDigitGapMs` | integer | No | Silence after a keypress that closes the group. Long enough that a caller reading a number off a card is not cut in half, short enough that a single menu digit is not a long wait. The agent is held silent for this long after the caller's last keypress, so a large value is a long silence; the agent editor caps it at 10000, while this API does not. Not bounded by the tool-execution budget the way `captureDtmf.timeoutMs` is — no tool call is held open by any of this. |
| `agent.dtmf.terminator` | enum: `#` \| `*` \| `none` | No | Key that closes the group immediately. It is consumed, never delivered: a caller who types `1234#` sends the agent `1234`. A terminator pressed on its own closes an empty group, and an empty group is never delivered, so it sends nothing. Use `none` for an agent that wants `#` itself to reach the model. |
| `agent.dtmf.maxDigits` | integer | No | Digit count that closes the group immediately. |
| `agent.dtmf.interruptsSpeech` | boolean | No | Whether the first keypress of a group stops the agent mid-sentence. Overrides the agent-level `interruptable` for keypresses only, for the same reason `captureDtmf.interruptsSpeech` does: a keypress cannot be a false speech detection, so an agent that plays a disclaimer through barge-in should still honor "press 0". Setting it false delays the interruption rather than preventing one — the closed group arrives as a caller turn, and a caller turn cancels whatever the agent was saying. |
| `agent.thinkingCue` | one of: object \| object \| object | No | What the caller hears while a tool call runs. `none` is silence. |
| `agent.vad` | object | No | Turn-taking sensitivity. Every field is individually optional: whatever is absent falls back to the underlying voice pipeline's own default, and a partial block is a legal save. |
| `agent.vad.silenceMs` | integer | No | How long the caller must pause before the agent replies. Total end-of-turn wait is `silenceMs + endOfTurnGraceMs`. |
| `agent.vad.threshold` | number | No | How confident the detector must be that it hears speech. |
| `agent.vad.startMs` | integer | No | How long speech must continue before it counts as the caller's turn starting. |
| `agent.vad.minVolume` | number | No | The minimum volume level treated as speech rather than background noise. |
| `agent.vad.endOfTurnGraceMs` | integer | No | The window after `silenceMs` elapses in which the caller may resume without the agent replying. Read only while `turnDetection.mode` is `silence`: the smart turn model decides end-of-turn itself and has no equivalent setting, so this value is stored but never applied in `smart` mode. |
| `agent.vad.bargeInMinWords` | integer | No | How many words the caller must say before their speech interrupts the agent. Absent means any sound interrupts, which is the behavior every agent has today. Ignored while `interruptable` is false — there is no barge-in for it to refine. Setting it is a trade, not a free filter: interruption is then detected from the first words transcribed rather than from the voice detector's speech onset, so barge-in gets slower on genuine interruptions too, not only on coughs and backchannels. |
| `agent.turnDetection` | object | No | Which strategy decides the caller has finished speaking. `vad` tunes what counts as the caller *starting* to speak; this decides when they stop. Absent behaves exactly as `mode: silence`, which is what every agent has always used. The **Turn detection** control on the Voice & Speech tab is being rolled out — if it isn't there, the rollout hasn't reached your organization yet. This API accepts the block either way, and the voice pipeline honors whatever is stored: the rollout gates the editor control, not the capability. |
| `agent.turnDetection.mode` | enum: `silence` \| `smart` | No | `silence` waits out the two timers in `vad` (`silenceMs` + `endOfTurnGraceMs`). `smart` runs an on-device model that predicts end-of-turn from the caller's prosody, so a mid-sentence pause or a trailing "so…" isn't treated as the end of a turn. `smart` is not free: it costs roughly 19 MB of memory and one thread for each concurrent call using it, plus about 20 ms of model loading while the call is set up. |
| `agent.turnDetection.smartStopMs` | integer | No | `smart` only. The longest silence the model tolerates before declaring the turn over on its own — the ceiling on a turn it mispredicted, not the wait on a normal one. It is **not** the replacement for `vad.endOfTurnGraceMs`, which `smart` mode never reads: this default of 3000 is far longer than silence mode's default total wait of 800 ms (`vad.silenceMs` 200 + `vad.endOfTurnGraceMs` 600). |
| `agent.turnDetection.preSpeechMs` | integer | No | `smart` only. How much audio from before the detected speech onset the model gets to hear. Milliseconds, like every other `*Ms` field here. |
| `agent.turnDetection.maxDurationSecs` | number | No | `smart` only. The longest stretch of caller audio the model scores at once; anything older is trimmed off the front. Seconds — the one field in this block that is neither milliseconds nor required to be a whole number. |
| `agent.memory` | object | No | — |
| `agent.memory.depth` | enum: `none` \| `warm` \| `light` \| `continuity` | No | How much the agent recalls about a returning caller. |
| `agent.memory.customizeGreeting` | boolean | No | Re-word the greeting for a recognized caller. Also requires a `depth` other than `none` and a `greeting`. |
| `agent.safety` | object | No | — |
| `agent.safety.redactPii` | boolean | No | Hide caller PII from the agent. An agent that must verify identities or look up accounts needs this off. |
| `agent.safety.steeringRules` | array of string | No | Policies checked before each tool call. They govern actions, not speech. An empty array is treated as absent. |
| `agent.skills` | object | No | — |
| `agent.skills.builtins` | array of string | No | Platform skills to enable, by name. List the available names with `GET /v1/agent-media/skills`. A name that is not in the catalog is dropped with a warning and the call runs without it. |
| `agent.skills.custom` | array of object | No | Skills you write yourself. |
| `agent.skills.custom[].name` | string | Yes | Lowercase letters, digits, and hyphens, up to 64 characters. A custom skill whose name matches a platform skill's is dropped at call time rather than replacing it. |
| `agent.skills.custom[].description` | string | Yes | What the skill is for, and when it applies. This is what tells the agent to follow the procedure, so write it as a trigger — "Use when the caller asks to return a part" — and keep it to a sentence or two. |
| `agent.skills.custom[].instructions` | string | Yes | The procedure itself. Written for speech: no tables, no lists to read aloud, one question at a time, and a stated limit on retries. A skill is instructions, not permissions — it cannot restrict which tools the agent may call. Use `safety.steeringRules` for that. Keep the enabled set tight: an agent is given its skills up front, but past a large combined size it switches to fetching them mid-call, which is less reliable. |
| `agent.analysis` | object | No | Post-call analysis: a separate model pass over the finished transcript, extracting the built-in judgments and any fields you define. Whether the transcript itself is kept is a separate setting, `retention.transcripts`, and analysis does not control it. With retention on — including by default, for an agent that has not set `safety.redactPii` — the transcript is stored whether or not analysis is enabled. With retention off, enabling analysis is the only thing that stores one. |
| `agent.analysis.enabled` | boolean | No | Rejected when true with no field and no built-in enabled: there would be nothing to extract. |
| `agent.analysis.builtins` | object | No | — |
| `agent.analysis.builtins.sentiment` | object | No | How the caller sounded: `positive`, `neutral`, `negative`, or `unknown`. |
| `agent.analysis.builtins.outcome` | object | No | Whether the call achieved what the caller wanted: `success`, `failure`, or `unknown`. |
| `agent.analysis.builtins.summary` | object | No | A short prose summary of the call. |
| `agent.analysis.fields` | array of object | No | Values you define yourself. |
| `agent.analysis.fields[].name` | string | Yes | The key the extracted value is returned under, in `analysis.values`. Must be unique within the agent, and cannot be `sentiment`, `outcome`, or `summary` — those are the built-ins' own names. |
| `agent.analysis.fields[].type` | enum: `string` \| `number` \| `integer` \| `boolean` | Yes | — |
| `agent.analysis.fields[].enum` | array of string | No | Allowed values, making this a choice field. Only meaningful when `type` is `string`; sending it on any other type is rejected. |
| `agent.analysis.fields[].prompt` | string | Yes | What to extract, addressed to the model reading the transcript. |
| `agent.analysis.fields[].required` | boolean | No | Whether the model must answer. An optional field the transcript gives no basis for is simply absent from `analysis.values`. |
| `agent.retention` | object | No | What a finished call leaves behind. Independent of `analysis`: an organization can keep transcripts without paying for a model pass over them, and can analyze calls without this block set at all. |
| `agent.retention.transcripts` | boolean | No | Whether this agent's call transcripts are stored (for 90 days). Deliberately has no default: omitting it means you have not decided, and Tandem applies the platform default — on for an ordinary agent, but off for an agent with `safety.redactPii` set, because a stored transcript holds the caller's words unredacted (that flag guards the model request, not the transcript). An explicit `true` or `false` is your decision and wins in either case. |
| `agent.idle` | object | No | What happens when the caller goes silent mid-call. |
| `agent.idle.enabled` | boolean | No | Off by default, so no existing agent starts hanging up on deploy. |
| `agent.idle.timeoutMs` | integer | No | How long the caller may stay silent before this behavior triggers. |
| `agent.idle.action` | enum: `reprompt` \| `endCall` | No | `reprompt` speaks `prompt` directly and waits again; `endCall` speaks it as a farewell and hangs up. |
| `agent.idle.prompt` | string | No | What the agent says when idle triggers. Required when `enabled` is true and `action` is `reprompt` — a reprompt with nothing to say never re-arms, so the idle behavior fires once and then stops. Optional for `endCall`, which is a valid silent hangup. |
| `agent.idle.maxReprompts` | integer | No | Consecutive idle periods tolerated before `onExhausted` decides. 0 means the first idle period goes straight to `onExhausted`, and since nothing reprompts, `prompt` is never spoken. 0 together with `onExhausted: continue` makes the whole idle block inert. |
| `agent.idle.onExhausted` | enum: `endCall` \| `continue` | No | What happens once `maxReprompts` is used up. |
| `agent.idle.disposition` | string | No | Recorded when idle ends the call. Must be one of this agent's own `dispositions`. Only an idle path that ends the call records it, so `action: reprompt` with `onExhausted: continue` never does. |
| `agent.limits` | object | No | Caller-visible bounds on the call itself. |
| `agent.limits.maxCallDurationMs` | integer | No | The call is ended once this elapses. Absent means unbounded. |
| `agent.limits.warnBeforeEndMs` | integer | No | How long before `maxCallDurationMs` the warning is spoken. Must be strictly less than `maxCallDurationMs`, and required whenever `warnPrompt` is set. |
| `agent.limits.warnPrompt` | string | No | What the agent says as the call-duration warning. Paired with `warnBeforeEndMs`: each is required whenever the other is set, because the warning timer is armed only when both are present. |
| `agent.reliability` | object | No | Failure-handling tuning. Each field falls back to a fixed default when absent, so an empty block is exactly today's behavior. |
| `agent.reliability.maxConsecutiveTurnFailures` | integer | No | Consecutive failed turns tolerated before recovery gives up. Falls back to 3. |
| `agent.reliability.toolTimeoutMs` | integer | No | How long a tool call may run before it's treated as failed. Falls back to 15000. Capped at 20 seconds because the voice agent stops waiting on a tool result at that point and hands the model a timeout, so a longer budget would never take effect. Lowering this also lowers the ceiling on `captureDtmf.timeoutMs`, which must stay at least a second inside it. |
| `agent.reliability.steeringJudgeTimeoutMs` | integer | No | How long the steering judge may take before it's skipped for that turn. Falls back to 2000. |
| `agent.reliability.handoffMaxHistoryMessages` | integer | No | How much conversation history rides along on a workflow handoff. Falls back to 40. |
| `agent.reliability.modelRetryMaxAttempts` | integer | No | Attempts made against the model provider before a turn is given up on. Falls back to 3, which is a deliberate change from the SDK's own default of 6: those defaults are written for a batch job, and on a phone call that backoff is dead air the caller hears, followed by a failed turn anyway. |
| `agent.reliability.modelRetryInitialDelayMs` | integer | No | Delay before the first retry, in milliseconds. Falls back to 1000 — again a deliberate departure from the SDK's own default, which starts its backoff at 4000. |
| `agent.reliability.modelRetryMaxDelayMs` | integer | No | Ceiling the retry backoff grows to, in milliseconds. Falls back to 4000, versus the SDK's own default ceiling of 240000 — a delay written for a batch job, not a call the caller is still on. |
| `agent.conversation` | object | No | How the agent's message history is kept inside the model's context window on a long call. An empty block behaves like `strategy: slidingWindow` with no other fields set — a 40-message window that drops the oldest messages first. |
| `agent.conversation.strategy` | enum: `slidingWindow` \| `summarizing` | No | `slidingWindow` drops the oldest messages once `windowSize` is exceeded. `summarizing` replaces dropping with summarizing older turns down to a shorter form instead of discarding them, at the cost of added latency on the turn where it fires. There is deliberately no `none`: an unmanaged history grows until the model's context window overflows mid-call, and the call dies. The model used to produce a summary is pinned by the platform and is not configurable here — a configurable summarizer model would run a full-size inference on the caller's critical path at the exact moment the context window overflows. |
| `agent.conversation.windowSize` | integer | No | `slidingWindow` only. Messages kept before the oldest are dropped. Absent means the SDK's own default of 40. |
| `agent.conversation.summaryRatio` | number | No | `summarizing` only. Fraction of the history replaced by a summary each time summarization fires. Absent means the SDK's own default of 0.3. |
| `agent.conversation.preserveRecentMessages` | integer | No | `summarizing` only. Most recent messages kept verbatim, never folded into the summary. Absent means the SDK's own default of 10. |
| `agent.conversation.pinFirst` | integer | No | Messages at the start of the call that are never dropped or summarized away, regardless of `strategy`. Use this to keep the caller's opening turns — why they called, an account number, a consent they gave — for the whole call. |
| `agent.conversation.proactiveCompressionThreshold` | number | No | Fraction of the context window that triggers compression proactively, instead of waiting for an overflow. Applies regardless of `strategy`. Absent means reactive-only, which is the SDK's own default. |
| `agent.vocabulary` | object | No | Terms the agent must hear and say correctly on a call, such as product names. Voice only. Mishearings are replaced with the term in the caller's transcript, the voice says each term as its `sayAs`, terms with `heardAs` are listed in the agent's instructions, and a published agent's terms also tune speech recognition — see `GET /v1/agent-media/agents/{agentId}/vocabulary-status`. |
| `agent.vocabulary.terms` | array of object | No | — |
| `agent.vocabulary.terms[].term` | string | Yes | How the term is written, for example `Quinoa`. |
| `agent.vocabulary.terms[].sayAs` | string | No | How the voice should say it, for example `KEEN-wah`. Spoken only; tool arguments and variables keep `term`. |
| `agent.vocabulary.terms[].heardAs` | array of string | No | How speech recognition tends to mishear it, for example `keen wah`. Replaced with `term` in the caller's transcript and listed in the agent's instructions. Not sent to speech recognition. |
| `agent.dataCollection` | object | No | The fields this agent collects during a conversation. Setting it turns on the `record_data` tool (and, in chat, `request_data_input`). Live conversations use the published version, so publish after changing it. Omit to collect nothing. Read what was collected from `GET /v1/agent-media/calls/{conversationId}/collected` or `GET /v1/agent-media/chat-conversations/{conversationId}/collected`. |
| `agent.dataCollection.fields` | array of object | Yes | Unique by `key`. |
| `agent.dataCollection.fields[].key` | string | Yes | The name the agent records the value under, which also labels the value on the call or chat record. Lowercase letters, digits, and underscores, starting with a letter, at most 40 characters. Unique within the agent's fields. |
| `agent.dataCollection.fields[].label` | string | Yes | What the field is called, shown to the person and to you. |
| `agent.dataCollection.fields[].kind` | enum: `full_name` \| `email` \| `phone` \| `postal_address` \| `postal_code` \| `date` \| `date_of_birth` \| `number` \| `yes_no` \| `alphanumeric_code` \| `free_text` | Yes | The type of value. Each kind validates and normalizes the answer, so an email is lowercased and a phone number is stored in E.164 format. |
| `agent.dataCollection.fields[].required` | boolean | No | Tells the agent the field matters, so it aims to collect it. Not enforced: a caller or visitor can still skip it. |
| `agent.dataCollection.fields[].prompt` | string | No | What to ask. Empty means the agent words the question from the label. |
| `agent.tools` | array of one of: object \| object \| object \| object \| object | No | — |
| `agent.knowledge` | object | No | — |
| `agent.knowledge.kbId` | string | Yes | An existing knowledge base's id. |
| `agent.knowledge.maxResults` | integer | No | — |
| `agent.knowledge.minScore` | number | No | Passages scoring below this are discarded. When omitted, 0.4 applies when a conversation starts. A save that changes `kbId` and sends the stored value unchanged drops it, so the new knowledge base's default applies: an agent update or version update compares against what it replaces, and a version create against its `baseVersion`. |
| `agent.sourceTemplateId` | string | No | The template this agent was created from, if any. Set once when the agent is created from a template and preserved across updates; read-only in practice. There is no link back — editing the template does not change this agent. |

### 400 — The configuration was rejected. `error` is `invalid_config` and `errors` lists each offending field.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | No | — |
| `errors` | array of object | No | — |
| `errors[].field` | string | No | Dotted path to the offending field. |
| `errors[].message` | string | No | — |

### 401 — Missing, malformed, or expired bearer token.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | No | A stable machine-readable code. |
| `message` | string | No | A human-readable explanation, safe to show to an end user. Present on errors an operator can act on; browser clients display this field in preference to a generic fallback. |
| `limit` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the bound itself. |
| `length` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the measured size of the rejected value. |
| `reason` | string | No | Present on some errors (e.g. `invalid_avatar_image`) that have more than one cause: which one applied. |

### 409 — `error` is `create_upsert_retired`: an agent already exists at the supplied `agentId`, so a create cannot overwrite it. A create writes an agent's configuration to a version row and leaves the agent row carrying identity only, so overwriting would strip whatever configuration that agent is serving off its row and leave nothing published in its place. Nothing was written. Save the configuration to a draft with `PUT /v1/agent-media/agents/{agentId}/versions/{version}`, then publish it.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | No | A stable machine-readable code. |
| `message` | string | No | A human-readable explanation, safe to show to an end user. Present on errors an operator can act on; browser clients display this field in preference to a generic fallback. |
| `limit` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the bound itself. |
| `length` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the measured size of the rejected value. |
| `reason` | string | No | Present on some errors (e.g. `invalid_avatar_image`) that have more than one cause: which one applied. |

### 503 — The organization's variables and secrets could not be read, so the `{{org.*}}` references in the configuration could not be checked. `error` is `config_validation_unavailable`. Nothing was saved; retry.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | No | A stable machine-readable code. |
| `message` | string | No | A human-readable explanation, safe to show to an end user. Present on errors an operator can act on; browser clients display this field in preference to a generic fallback. |
| `limit` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the bound itself. |
| `length` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the measured size of the rejected value. |
| `reason` | string | No | Present on some errors (e.g. `invalid_avatar_image`) that have more than one cause: which one applied. |
