# Update an agent template (/docs/api/voice-agents/agent-templates/updateAgentTemplate)

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

`PUT /v1/agent-media/agent-templates/{templateId}`

Update an agent template

Replace an organization template's metadata and configuration. Only ever touches the organization's own templates — a curated template, even one that exists, is a 404 here rather than a 403; from this organization's point of view it has no template with that id.

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

### Path parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `templateId` | string | Yes | The template's unique ID. |

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

Every requirement id referenced by a tool's or `knowledge`'s `requires` inside `config` must be declared in `requirements`, and every declared requirement must be referenced somewhere in `config` — a mismatch in either direction is rejected as `invalid_config`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | The template's display name. |
| `description` | string | No | Shown on the template's card in the gallery. |
| `category` | string | Yes | Groups templates in the gallery; also the type filter's values. |
| `tags` | array of string | No | — |
| `status` | enum: `draft` \| `live` | No | Ignored on input, and accepted only so a template read back from the API can be sent again unchanged. `draft` is Tandem's staging state for a curated template and only a Tandem release writes one; your organization's own templates have no draft/publish workflow, so they are always created and updated `live` whatever this field says. |
| `config` | object | Yes | An agent configuration with every organization-scoped reference replaced by a requirement to bind at instantiate time: each tool binding and the `knowledge` block carry a `requires` key, naming a requirement, in place of their `workflowId` / `integrationId` / `kbId`. Sending one of those ids here is ignored rather than rejected — it is dropped, not stored, so the binding you sent has no effect. The same goes for `sourceTemplateId`: a template records no provenance of its own. |
| `config.name` | string | Yes | Internal label; callers never hear it. |
| `config.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. |
| `config.greeting` | string | No | Spoken verbatim as the agent's first turn. |
| `config.interruptable` | boolean | No | Whether caller speech interrupts the agent mid-sentence (barge-in). |
| `config.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. |
| `config.slots` | object | Yes | — |
| `config.slots.llm` | object | Yes | — |
| `config.slots.llm.model` | string | Yes | A model `id` from the models endpoint. |
| `config.slots.llm.settings` | object | No | — |
| `config.slots.stt` | object | No | Omit for a model whose `audioIn` is true; sending one is rejected. |
| `config.slots.stt.provider` | string | Yes | — |
| `config.slots.stt.settings` | object | No | — |
| `config.slots.tts` | object | No | Required unless the model's `audioOut` is true. |
| `config.slots.tts.voiceProvider` | string | Yes | — |
| `config.slots.tts.settings` | object | Yes | — |
| `config.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`. |
| `config.slots.additionalVoices[].voiceProvider` | string | Yes | — |
| `config.slots.additionalVoices[].settings` | object | Yes | — |
| `config.greetingParts` | array of object | No | Replaces `greeting` on calls. Each `language` must be a voice language of the agent. Templates render per part. |
| `config.greetingParts[].language` | string | Yes | — |
| `config.greetingParts[].text` | string | Yes | — |
| `config.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. |
| `config.variables[].name` | string | Yes | Unique within the agent. Referenced as `{{agent.<name>}}` in the system prompt, the greetings, and a tool's constant parameters. |
| `config.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. |
| `config.variables[].description` | string | No | A note for whoever edits the agent next; never sent to the model. |
| `config.dispositions` | array of object | No | — |
| `config.dispositions[].name` | string | Yes | Unique within the agent, and becomes an exit on the AI Voice Agent node. `default` and `fallback` are reserved. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.builtinTools.transferToExternal.enabled` | boolean | No | — |
| `config.builtinTools.transferToExternal.destinations` | array of object | No | — |
| `config.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. |
| `config.builtinTools.transferToVoiceAgent.enabled` | boolean | No | — |
| `config.builtinTools.transferToVoiceAgent.contextCarryover` | enum: `none` \| `summary` \| `verbatim` | No | How much of the conversation travels to the receiving agent. |
| `config.builtinTools.transferToVoiceAgent.destinations` | array of object | No | — |
| `config.builtinTools.captureDtmf` | object | No | — |
| `config.builtinTools.captureDtmf.enabled` | boolean | No | — |
| `config.builtinTools.captureDtmf.maxDigits` | integer | No | — |
| `config.builtinTools.captureDtmf.terminator` | enum: `#` \| `*` \| `none` | No | — |
| `config.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. |
| `config.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. |
| `config.builtinTools.captureDtmf.interruptsSpeech` | boolean | No | Overrides `interruptable` for keypresses only, so an agent that plays through a disclaimer still honors "press 0". |
| `config.builtinTools.recordingControl` | object | No | One toggle exposing both `pause_recording` and `resume_recording`. |
| `config.builtinTools.recordingControl.enabled` | boolean | No | — |
| `config.builtinTools.currentTime` | object | No | — |
| `config.builtinTools.currentTime.enabled` | boolean | No | — |
| `config.builtinTools.currentTime.timezone` | string | No | An IANA timezone name (e.g. `America/Chicago`). |
| `config.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. |
| `config.builtinTools.setWorkflowVariable.enabled` | boolean | No | — |
| `config.builtinTools.setWorkflowVariable.variables` | array of object | No | — |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.dtmf.maxDigits` | integer | No | Digit count that closes the group immediately. |
| `config.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. |
| `config.thinkingCue` | one of: object \| object \| object | No | What the caller hears while a tool call runs. `none` is silence. |
| `config.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. |
| `config.vad.silenceMs` | integer | No | How long the caller must pause before the agent replies. Total end-of-turn wait is `silenceMs + endOfTurnGraceMs`. |
| `config.vad.threshold` | number | No | How confident the detector must be that it hears speech. |
| `config.vad.startMs` | integer | No | How long speech must continue before it counts as the caller's turn starting. |
| `config.vad.minVolume` | number | No | The minimum volume level treated as speech rather than background noise. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.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). |
| `config.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. |
| `config.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. |
| `config.memory` | object | No | — |
| `config.memory.depth` | enum: `none` \| `warm` \| `light` \| `continuity` | No | How much the agent recalls about a returning caller. |
| `config.memory.customizeGreeting` | boolean | No | Re-word the greeting for a recognized caller. Also requires a `depth` other than `none` and a `greeting`. |
| `config.safety` | object | No | — |
| `config.safety.redactPii` | boolean | No | Hide caller PII from the agent. An agent that must verify identities or look up accounts needs this off. |
| `config.safety.steeringRules` | array of string | No | Policies checked before each tool call. They govern actions, not speech. An empty array is treated as absent. |
| `config.skills` | object | No | — |
| `config.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. |
| `config.skills.custom` | array of object | No | Skills you write yourself. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.analysis.enabled` | boolean | No | Rejected when true with no field and no built-in enabled: there would be nothing to extract. |
| `config.analysis.builtins` | object | No | — |
| `config.analysis.builtins.sentiment` | object | No | How the caller sounded: `positive`, `neutral`, `negative`, or `unknown`. |
| `config.analysis.builtins.outcome` | object | No | Whether the call achieved what the caller wanted: `success`, `failure`, or `unknown`. |
| `config.analysis.builtins.summary` | object | No | A short prose summary of the call. |
| `config.analysis.fields` | array of object | No | Values you define yourself. |
| `config.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. |
| `config.analysis.fields[].type` | enum: `string` \| `number` \| `integer` \| `boolean` | Yes | — |
| `config.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. |
| `config.analysis.fields[].prompt` | string | Yes | What to extract, addressed to the model reading the transcript. |
| `config.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`. |
| `config.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. |
| `config.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. |
| `config.idle` | object | No | What happens when the caller goes silent mid-call. |
| `config.idle.enabled` | boolean | No | Off by default, so no existing agent starts hanging up on deploy. |
| `config.idle.timeoutMs` | integer | No | How long the caller may stay silent before this behavior triggers. |
| `config.idle.action` | enum: `reprompt` \| `endCall` | No | `reprompt` speaks `prompt` directly and waits again; `endCall` speaks it as a farewell and hangs up. |
| `config.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. |
| `config.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. |
| `config.idle.onExhausted` | enum: `endCall` \| `continue` | No | What happens once `maxReprompts` is used up. |
| `config.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. |
| `config.limits` | object | No | Caller-visible bounds on the call itself. |
| `config.limits.maxCallDurationMs` | integer | No | The call is ended once this elapses. Absent means unbounded. |
| `config.limits.warnBeforeEndMs` | integer | No | How long before `maxCallDurationMs` the warning is spoken. Must be strictly less than `maxCallDurationMs`, and required whenever `warnPrompt` is set. |
| `config.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. |
| `config.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. |
| `config.reliability.maxConsecutiveTurnFailures` | integer | No | Consecutive failed turns tolerated before recovery gives up. Falls back to 3. |
| `config.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. |
| `config.reliability.steeringJudgeTimeoutMs` | integer | No | How long the steering judge may take before it's skipped for that turn. Falls back to 2000. |
| `config.reliability.handoffMaxHistoryMessages` | integer | No | How much conversation history rides along on a workflow handoff. Falls back to 40. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.conversation.windowSize` | integer | No | `slidingWindow` only. Messages kept before the oldest are dropped. Absent means the SDK's own default of 40. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.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. |
| `config.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`. |
| `config.vocabulary.terms` | array of object | No | — |
| `config.vocabulary.terms[].term` | string | Yes | How the term is written, for example `Quinoa`. |
| `config.vocabulary.terms[].sayAs` | string | No | How the voice should say it, for example `KEEN-wah`. Spoken only; tool arguments and variables keep `term`. |
| `config.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. |
| `config.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`. |
| `config.dataCollection.fields` | array of object | Yes | Unique by `key`. |
| `config.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. |
| `config.dataCollection.fields[].label` | string | Yes | What the field is called, shown to the person and to you. |
| `config.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. |
| `config.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. |
| `config.dataCollection.fields[].prompt` | string | No | What to ask. Empty means the agent words the question from the label. |
| `config.tools` | array of one of: object \| object \| object \| object | No | — |
| `config.knowledge` | object | No | `Knowledge` with `kbId` replaced by `requires`, naming the knowledgeBase requirement that supplies it. |
| `config.knowledge.requires` | string | Yes | Names one of the template's `requirements`. Written by a `requires` key inside `config` and declared as a requirement's `id`. |
| `config.knowledge.maxResults` | integer | No | — |
| `config.knowledge.minScore` | number | No | Passages scoring below this are discarded. Not carried onto an agent created from the template: the bound knowledge base's default applies. |
| `requirements` | array of object | No | — |
| `requirements[].id` | string | Yes | The id a `requires` key inside `config` refers to. |
| `requirements[].kind` | enum: `workflow` \| `integration` \| `mcp` \| `knowledgeBase` | Yes | — |
| `requirements[].label` | string | Yes | Shown as the picker's label for this requirement. |
| `requirements[].description` | string | No | Shown under the label, if set. |
| `requirements[].optional` | boolean | No | Left unbound at instantiate time, the tools that needed it are dropped rather than the create being refused. |
| `requirements[].integrationType` | string | No | Narrows the picker, e.g. `generic` for an HTTP integration. |
| `requirements[].defaultBinding` | one of: object \| object \| object | No | The binding this requirement was templatized from. Present on an organization's own templates, so the instantiate picker can pre-select it; absent on curated ones. |
| `sourceAgentId` | string | No | The agent this template was derived from, if any. Provenance only — nothing links the template back to that agent, and there is no upgrade path from one to the other. |

## Responses

### 200 — The updated template.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `template` | object | No | — |
| `template.id` | string | Yes | — |
| `template.name` | string | Yes | — |
| `template.description` | string | No | — |
| `template.category` | string | Yes | — |
| `template.tags` | array of string | No | — |
| `template.status` | enum: `draft` \| `live` | No | Curated-only staging state. An organization's own template is always `live`. |
| `template.scope` | enum: `org` \| `curated` | Yes | Whether this is one of the organization's own templates or one of Tandem's curated ones. Determines whether it may be edited or deleted. Set by the server; not stored on the record. |
| `template.config` | object | Yes | An agent configuration with every organization-scoped reference replaced by a requirement to bind at instantiate time: each tool binding and the `knowledge` block carry a `requires` key, naming a requirement, in place of their `workflowId` / `integrationId` / `kbId`. Sending one of those ids here is ignored rather than rejected — it is dropped, not stored, so the binding you sent has no effect. The same goes for `sourceTemplateId`: a template records no provenance of its own. |
| `template.config.name` | string | Yes | Internal label; callers never hear it. |
| `template.config.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. |
| `template.config.greeting` | string | No | Spoken verbatim as the agent's first turn. |
| `template.config.interruptable` | boolean | No | Whether caller speech interrupts the agent mid-sentence (barge-in). |
| `template.config.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. |
| `template.config.slots` | object | Yes | — |
| `template.config.slots.llm` | object | Yes | — |
| `template.config.slots.stt` | object | No | Omit for a model whose `audioIn` is true; sending one is rejected. |
| `template.config.slots.tts` | object | No | Required unless the model's `audioOut` is true. |
| `template.config.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`. |
| `template.config.greetingParts` | array of object | No | Replaces `greeting` on calls. Each `language` must be a voice language of the agent. Templates render per part. |
| `template.config.greetingParts[].language` | string | Yes | — |
| `template.config.greetingParts[].text` | string | Yes | — |
| `template.config.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. |
| `template.config.variables[].name` | string | Yes | Unique within the agent. Referenced as `{{agent.<name>}}` in the system prompt, the greetings, and a tool's constant parameters. |
| `template.config.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. |
| `template.config.variables[].description` | string | No | A note for whoever edits the agent next; never sent to the model. |
| `template.config.dispositions` | array of object | No | — |
| `template.config.dispositions[].name` | string | Yes | Unique within the agent, and becomes an exit on the AI Voice Agent node. `default` and `fallback` are reserved. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.builtinTools.captureDtmf` | object | No | — |
| `template.config.builtinTools.recordingControl` | object | No | One toggle exposing both `pause_recording` and `resume_recording`. |
| `template.config.builtinTools.currentTime` | object | No | — |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.dtmf.maxDigits` | integer | No | Digit count that closes the group immediately. |
| `template.config.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. |
| `template.config.thinkingCue` | one of: object \| object \| object | No | What the caller hears while a tool call runs. `none` is silence. |
| `template.config.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. |
| `template.config.vad.silenceMs` | integer | No | How long the caller must pause before the agent replies. Total end-of-turn wait is `silenceMs + endOfTurnGraceMs`. |
| `template.config.vad.threshold` | number | No | How confident the detector must be that it hears speech. |
| `template.config.vad.startMs` | integer | No | How long speech must continue before it counts as the caller's turn starting. |
| `template.config.vad.minVolume` | number | No | The minimum volume level treated as speech rather than background noise. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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). |
| `template.config.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. |
| `template.config.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. |
| `template.config.memory` | object | No | — |
| `template.config.memory.depth` | enum: `none` \| `warm` \| `light` \| `continuity` | No | How much the agent recalls about a returning caller. |
| `template.config.memory.customizeGreeting` | boolean | No | Re-word the greeting for a recognized caller. Also requires a `depth` other than `none` and a `greeting`. |
| `template.config.safety` | object | No | — |
| `template.config.safety.redactPii` | boolean | No | Hide caller PII from the agent. An agent that must verify identities or look up accounts needs this off. |
| `template.config.safety.steeringRules` | array of string | No | Policies checked before each tool call. They govern actions, not speech. An empty array is treated as absent. |
| `template.config.skills` | object | No | — |
| `template.config.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. |
| `template.config.skills.custom` | array of object | No | Skills you write yourself. |
| `template.config.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. |
| `template.config.analysis.enabled` | boolean | No | Rejected when true with no field and no built-in enabled: there would be nothing to extract. |
| `template.config.analysis.builtins` | object | No | — |
| `template.config.analysis.fields` | array of object | No | Values you define yourself. |
| `template.config.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. |
| `template.config.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. |
| `template.config.idle` | object | No | What happens when the caller goes silent mid-call. |
| `template.config.idle.enabled` | boolean | No | Off by default, so no existing agent starts hanging up on deploy. |
| `template.config.idle.timeoutMs` | integer | No | How long the caller may stay silent before this behavior triggers. |
| `template.config.idle.action` | enum: `reprompt` \| `endCall` | No | `reprompt` speaks `prompt` directly and waits again; `endCall` speaks it as a farewell and hangs up. |
| `template.config.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. |
| `template.config.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. |
| `template.config.idle.onExhausted` | enum: `endCall` \| `continue` | No | What happens once `maxReprompts` is used up. |
| `template.config.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. |
| `template.config.limits` | object | No | Caller-visible bounds on the call itself. |
| `template.config.limits.maxCallDurationMs` | integer | No | The call is ended once this elapses. Absent means unbounded. |
| `template.config.limits.warnBeforeEndMs` | integer | No | How long before `maxCallDurationMs` the warning is spoken. Must be strictly less than `maxCallDurationMs`, and required whenever `warnPrompt` is set. |
| `template.config.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. |
| `template.config.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. |
| `template.config.reliability.maxConsecutiveTurnFailures` | integer | No | Consecutive failed turns tolerated before recovery gives up. Falls back to 3. |
| `template.config.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. |
| `template.config.reliability.steeringJudgeTimeoutMs` | integer | No | How long the steering judge may take before it's skipped for that turn. Falls back to 2000. |
| `template.config.reliability.handoffMaxHistoryMessages` | integer | No | How much conversation history rides along on a workflow handoff. Falls back to 40. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.conversation.windowSize` | integer | No | `slidingWindow` only. Messages kept before the oldest are dropped. Absent means the SDK's own default of 40. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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. |
| `template.config.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`. |
| `template.config.vocabulary.terms` | array of object | No | — |
| `template.config.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`. |
| `template.config.dataCollection.fields` | array of object | Yes | Unique by `key`. |
| `template.config.tools` | array of one of: object \| object \| object \| object | No | — |
| `template.config.knowledge` | object | No | `Knowledge` with `kbId` replaced by `requires`, naming the knowledgeBase requirement that supplies it. |
| `template.config.knowledge.requires` | string | Yes | Names one of the template's `requirements`. Written by a `requires` key inside `config` and declared as a requirement's `id`. |
| `template.config.knowledge.maxResults` | integer | No | — |
| `template.config.knowledge.minScore` | number | No | Passages scoring below this are discarded. Not carried onto an agent created from the template: the bound knowledge base's default applies. |
| `template.requirements` | array of object | Yes | — |
| `template.requirements[].id` | string | Yes | The id a `requires` key inside `config` refers to. |
| `template.requirements[].kind` | enum: `workflow` \| `integration` \| `mcp` \| `knowledgeBase` | Yes | — |
| `template.requirements[].label` | string | Yes | Shown as the picker's label for this requirement. |
| `template.requirements[].description` | string | No | Shown under the label, if set. |
| `template.requirements[].optional` | boolean | No | Left unbound at instantiate time, the tools that needed it are dropped rather than the create being refused. |
| `template.requirements[].integrationType` | string | No | Narrows the picker, e.g. `generic` for an HTTP integration. |
| `template.requirements[].defaultBinding` | one of: object \| object \| object | No | The binding this requirement was templatized from. Present on an organization's own templates, so the instantiate picker can pre-select it; absent on curated ones. |
| `template.sourceAgentId` | string | No | The agent this template was derived from, if any. |
| `template.createdBy` | string | Yes | — |
| `template.updatedBy` | string | Yes | — |
| `template.createdAt` | string (date-time) | Yes | — |
| `template.updatedAt` | string (date-time) | Yes | — |

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

### 403 — `error` is `curated_partition_read_only`: the request resolved to Tandem's curated templates, which no organization may write. Only a Tandem release publishes a curated template.

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

### 404 — The resource does not exist.

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