# Update a draft version (/docs/api/voice-agents/agent-versions/updateAgentVersion)

> 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/agents/{agentId}/versions/{version}`

Update a draft version

Replace a draft's configuration. A replacement, not a merge: a field you omit is cleared, including `description`. Read the version and send it back with your changes applied if you only mean to change part of it.

This never creates a version — a number that does not exist is a `404`. It also never edits the published one: the version the agent is currently serving is immutable, and editing it would change what live calls run with no record of the change. Publish a different version first, or work on a draft.

Same shape-only checking as a create, and the same admin rule for storing a `{{secrets.*}}` reference. `createdBy` is kept as it was; `updatedBy` becomes you.

## 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 |
| --- | --- | --- | --- |
| `agentId` | string | Yes | The agent's unique ID. |
| `version` | string | Yes | The version number: `<major>.<minor>`, two non-negative integers with no leading zeros and at most nine digits each — `0.1`, `1.0`, `2.13`. You choose it. Anything else is rejected with `invalid_version`, because the number is part of the version's storage key. |

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

The body a version create or update takes: the configuration, plus an optional release note.

`version` comes from the path, never the body. `name` and `sourceTemplateId` are not part of a version — send them and they are dropped rather than rejected, so a configuration read back off an agent can be posted here as-is. Change the name with `PUT /v1/agent-media/agents/{agentId}`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `description` | string | No | A release note for this version. Free text, and part of the replacement: omit it on an update and the stored note is cleared, not carried over. Over 2,000 characters is rejected with `invalid_config` naming this field. |
| `baseVersion` | string | No | Create only; ignored on update and never stored. The version this body was copied from. When the body changes `knowledge.kbId` from that version's and sends its `knowledge.minScore` unchanged, the score is dropped so the new knowledge base's default applies. Omitted or naming no version of this agent, the body is stored as sent. |
| `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. |
| `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 | No | — |
| `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`. |

## Responses

### 200 — The updated draft.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `version` | object | No | The configuration one version of an agent holds: an agent configuration without `name` or `sourceTemplateId`, both of which belong to the agent and not to any one version of it. A rename must not need a publish, and provenance recorded twenty times over is provenance that can disagree with itself. Nothing is required, because a draft is work in progress. Every other bound is the agent configuration's own, unchanged: a value a draft accepts is a value the published configuration accepts. |
| `version.version` | string | Yes | The version number, as it appears in the path. |
| `version.description` | string | No | The release note, if one was saved. A note about the version, not a field of the configuration — an agent has no `description` of its own. |
| `version.publishedAt` | string (date-time) | No | When this version was published. Absent on a version that has never been published, which is what distinguishes a draft. Present does not mean current: publishing a different version leaves this one's `publishedAt` where it is. `publishedVersion` on the agent is the one that says which version is live. |
| `version.createdBy` | string | No | Who created the version. Carried unchanged through every later edit and through publishing. Absent on a version Tandem created while migrating the agent onto versioning, which has no author to name. |
| `version.updatedBy` | string | No | Who last changed the configuration. Publishing does not touch it: a publish is not an edit, and recording it here would lose the author of the last real change. |
| `version.backfilledAt` | string (date-time) | No | When Tandem created this version while migrating the agent onto versioning. Present only on such a version and only while its configuration is still the one migration wrote: any later save or publish of the version clears it. Read-only — sending it is ignored. |
| `version.createdAt` | string (date-time) | Yes | — |
| `version.updatedAt` | string (date-time) | Yes | — |
| `version.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. |
| `version.greeting` | string | No | Spoken verbatim as the agent's first turn. |
| `version.interruptable` | boolean | No | Whether caller speech interrupts the agent mid-sentence (barge-in). |
| `version.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. |
| `version.slots` | object | No | — |
| `version.slots.llm` | object | Yes | — |
| `version.slots.llm.model` | string | Yes | A model `id` from the models endpoint. |
| `version.slots.llm.settings` | object | No | — |
| `version.slots.stt` | object | No | Omit for a model whose `audioIn` is true; sending one is rejected. |
| `version.slots.stt.provider` | string | Yes | — |
| `version.slots.stt.settings` | object | No | — |
| `version.slots.tts` | object | No | Required unless the model's `audioOut` is true. |
| `version.slots.tts.voiceProvider` | string | Yes | — |
| `version.slots.tts.settings` | object | Yes | — |
| `version.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`. |
| `version.slots.additionalVoices[].voiceProvider` | string | Yes | — |
| `version.slots.additionalVoices[].settings` | object | Yes | — |
| `version.greetingParts` | array of object | No | Replaces `greeting` on calls. Each `language` must be a voice language of the agent. Templates render per part. |
| `version.greetingParts[].language` | string | Yes | — |
| `version.greetingParts[].text` | string | Yes | — |
| `version.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. |
| `version.variables[].name` | string | Yes | Unique within the agent. Referenced as `{{agent.<name>}}` in the system prompt, the greetings, and a tool's constant parameters. |
| `version.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. |
| `version.variables[].description` | string | No | A note for whoever edits the agent next; never sent to the model. |
| `version.dispositions` | array of object | No | — |
| `version.dispositions[].name` | string | Yes | Unique within the agent, and becomes an exit on the AI Voice Agent node. `default` and `fallback` are reserved. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.builtinTools.transferToExternal.enabled` | boolean | No | — |
| `version.builtinTools.transferToExternal.destinations` | array of object | No | — |
| `version.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. |
| `version.builtinTools.transferToVoiceAgent.enabled` | boolean | No | — |
| `version.builtinTools.transferToVoiceAgent.contextCarryover` | enum: `none` \| `summary` \| `verbatim` | No | How much of the conversation travels to the receiving agent. |
| `version.builtinTools.transferToVoiceAgent.destinations` | array of object | No | — |
| `version.builtinTools.captureDtmf` | object | No | — |
| `version.builtinTools.captureDtmf.enabled` | boolean | No | — |
| `version.builtinTools.captureDtmf.maxDigits` | integer | No | — |
| `version.builtinTools.captureDtmf.terminator` | enum: `#` \| `*` \| `none` | No | — |
| `version.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. |
| `version.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. |
| `version.builtinTools.captureDtmf.interruptsSpeech` | boolean | No | Overrides `interruptable` for keypresses only, so an agent that plays through a disclaimer still honors "press 0". |
| `version.builtinTools.recordingControl` | object | No | One toggle exposing both `pause_recording` and `resume_recording`. |
| `version.builtinTools.recordingControl.enabled` | boolean | No | — |
| `version.builtinTools.currentTime` | object | No | — |
| `version.builtinTools.currentTime.enabled` | boolean | No | — |
| `version.builtinTools.currentTime.timezone` | string | No | An IANA timezone name (e.g. `America/Chicago`). |
| `version.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. |
| `version.builtinTools.setWorkflowVariable.enabled` | boolean | No | — |
| `version.builtinTools.setWorkflowVariable.variables` | array of object | No | — |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.dtmf.maxDigits` | integer | No | Digit count that closes the group immediately. |
| `version.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. |
| `version.thinkingCue` | one of: object \| object \| object | No | What the caller hears while a tool call runs. `none` is silence. |
| `version.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. |
| `version.vad.silenceMs` | integer | No | How long the caller must pause before the agent replies. Total end-of-turn wait is `silenceMs + endOfTurnGraceMs`. |
| `version.vad.threshold` | number | No | How confident the detector must be that it hears speech. |
| `version.vad.startMs` | integer | No | How long speech must continue before it counts as the caller's turn starting. |
| `version.vad.minVolume` | number | No | The minimum volume level treated as speech rather than background noise. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.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). |
| `version.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. |
| `version.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. |
| `version.memory` | object | No | — |
| `version.memory.depth` | enum: `none` \| `warm` \| `light` \| `continuity` | No | How much the agent recalls about a returning caller. |
| `version.memory.customizeGreeting` | boolean | No | Re-word the greeting for a recognized caller. Also requires a `depth` other than `none` and a `greeting`. |
| `version.safety` | object | No | — |
| `version.safety.redactPii` | boolean | No | Hide caller PII from the agent. An agent that must verify identities or look up accounts needs this off. |
| `version.safety.steeringRules` | array of string | No | Policies checked before each tool call. They govern actions, not speech. An empty array is treated as absent. |
| `version.skills` | object | No | — |
| `version.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. |
| `version.skills.custom` | array of object | No | Skills you write yourself. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.analysis.enabled` | boolean | No | Rejected when true with no field and no built-in enabled: there would be nothing to extract. |
| `version.analysis.builtins` | object | No | — |
| `version.analysis.builtins.sentiment` | object | No | How the caller sounded: `positive`, `neutral`, `negative`, or `unknown`. |
| `version.analysis.builtins.outcome` | object | No | Whether the call achieved what the caller wanted: `success`, `failure`, or `unknown`. |
| `version.analysis.builtins.summary` | object | No | A short prose summary of the call. |
| `version.analysis.fields` | array of object | No | Values you define yourself. |
| `version.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. |
| `version.analysis.fields[].type` | enum: `string` \| `number` \| `integer` \| `boolean` | Yes | — |
| `version.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. |
| `version.analysis.fields[].prompt` | string | Yes | What to extract, addressed to the model reading the transcript. |
| `version.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`. |
| `version.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. |
| `version.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. |
| `version.idle` | object | No | What happens when the caller goes silent mid-call. |
| `version.idle.enabled` | boolean | No | Off by default, so no existing agent starts hanging up on deploy. |
| `version.idle.timeoutMs` | integer | No | How long the caller may stay silent before this behavior triggers. |
| `version.idle.action` | enum: `reprompt` \| `endCall` | No | `reprompt` speaks `prompt` directly and waits again; `endCall` speaks it as a farewell and hangs up. |
| `version.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. |
| `version.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. |
| `version.idle.onExhausted` | enum: `endCall` \| `continue` | No | What happens once `maxReprompts` is used up. |
| `version.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. |
| `version.limits` | object | No | Caller-visible bounds on the call itself. |
| `version.limits.maxCallDurationMs` | integer | No | The call is ended once this elapses. Absent means unbounded. |
| `version.limits.warnBeforeEndMs` | integer | No | How long before `maxCallDurationMs` the warning is spoken. Must be strictly less than `maxCallDurationMs`, and required whenever `warnPrompt` is set. |
| `version.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. |
| `version.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. |
| `version.reliability.maxConsecutiveTurnFailures` | integer | No | Consecutive failed turns tolerated before recovery gives up. Falls back to 3. |
| `version.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. |
| `version.reliability.steeringJudgeTimeoutMs` | integer | No | How long the steering judge may take before it's skipped for that turn. Falls back to 2000. |
| `version.reliability.handoffMaxHistoryMessages` | integer | No | How much conversation history rides along on a workflow handoff. Falls back to 40. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.conversation.windowSize` | integer | No | `slidingWindow` only. Messages kept before the oldest are dropped. Absent means the SDK's own default of 40. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.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. |
| `version.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`. |
| `version.vocabulary.terms` | array of object | No | — |
| `version.vocabulary.terms[].term` | string | Yes | How the term is written, for example `Quinoa`. |
| `version.vocabulary.terms[].sayAs` | string | No | How the voice should say it, for example `KEEN-wah`. Spoken only; tool arguments and variables keep `term`. |
| `version.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. |
| `version.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`. |
| `version.dataCollection.fields` | array of object | Yes | Unique by `key`. |
| `version.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. |
| `version.dataCollection.fields[].label` | string | Yes | What the field is called, shown to the person and to you. |
| `version.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. |
| `version.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. |
| `version.dataCollection.fields[].prompt` | string | No | What to ask. Empty means the agent words the question from the label. |
| `version.tools` | array of one of: object \| object \| object \| object \| object | No | — |
| `version.knowledge` | object | No | — |
| `version.knowledge.kbId` | string | Yes | An existing knowledge base's id. |
| `version.knowledge.maxResults` | integer | No | — |
| `version.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`. |

### 400 — Nothing was saved. `error` is `invalid_agentId` or `invalid_version` for a malformed path; `version_published` when this version is the one the agent is serving, which cannot be edited; or `invalid_config`, with `errors` naming each offending field.

A rejected version write. `error` is always the machine-readable code; which of the other two keys comes with it depends on which code it is. `invalid_config` carries `errors`, one entry per offending field. Every other code refuses the request as a whole rather than a field, and carries `message` — a human-readable explanation, safe to show to an end user — instead.

Type: one of: object | object

### 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 `forbidden`: the configuration stores a `{{secrets.*}}` reference and only an organization admin may do that. Nothing was saved.

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