Skip to main content

2026-09-25

A field-by-field review of all 53 operations. Most of it was wording that did not match what the API does, and those corrections need nothing from you. The removals below do.

Fields removed

On a built-in analysis value, sort_order was one of three fields you could change. The other two, description and enabled, are unaffected.

POST /v1/calls takes a version number

It took the version uuid and nothing else, while every other version route takes the number — so after publishing v4 you had to look its uuid up before you could place a call on it.
agent_version_id still works. Sending both is a 400 invalid_request: they name the same thing, so honouring one and ignoring the other would be a guess. Omit both and the agent’s live version answers, as before.

The widget is one resource, not a collection

An agent has exactly one widget, so the id in the path could only ever be one value — one you had to fetch before you could use any of the other routes. The read returns the object itself rather than a one-item array, and prepares the widget if it does not exist yet — so it never 404s. id is still on the body, which is what you want in a log or when telling one rotation from another.
widget_not_found is no longer among the reason codes on this surface: with no id to get wrong, there is nothing to miss.

Every growing list pages the same way

Four shapes reached you before this: ?skip= on agents, ?offset= on remembered callers, cursors on calls and chats, and nothing at all on knowledge bases, versions, deploy history and the analysis library — which returned every row your workspace held. They are all cursors now, and all return the same envelope:
?skip= and ?offset= are gone. Both drift: a row written while you page can appear on two pages or on none. A cursor names the boundary instead, so new rows land on page 1 and the point you are reading from does not move. An offset also gets slower the deeper you page; a cursor does not.
The four lists that were unpaged now return 20 rows by default, not everything. If you were reading one of them expecting the full set, page until has_more is false.
The lists that do NOT page are the bounded ones: numbers, WhatsApp numbers, the memory changes for a single call, and the five catalogues. Those stay bare arrays so a picker is one request. GET /v1/custom-analysis also settles its envelope key. It was {"categories": […]}, briefly {"groups": […]}, and is now data like every other list — the path rename to /v1/custom-analysis/groups stands.

Call provenance is two fields, not three

call_type is removed. It was direction and origin fused into one compound value — outbound_api is direction: outbound plus origin: api — so it could not be read without knowing how it was assembled, and it published a second vocabulary for two questions those fields already answer. direction now publishes inbound, outbound, or web for a call that happened in a browser. It used to return the raw stored value, so a widget call came back as widget_talk and a dashboard test as talk_test — internal words, and two values these docs never promised. Use origin to tell a real widget call from your own test click. A web call’s from_number and to_number are now null, which is what this page has always said they were. They were returning the sentinels "widget" and "talk". The list filter follows: ?call_type=phone|web is now ?direction=inbound|outbound|web, taking exactly the values the field publishes. An unknown one answers 400 invalid_direction.
The same change applies to the call object in every call.* webhook, which is the same shape.

Response speed takes four values, not any number

conversation.response_eagerness accepted any number from 0 to 1 and snapped it to the nearest of four presets, so 0.6 saved, ran as Balanced, and read back as 0.6 forever. It now accepts only the four, and anything else is refused rather than quietly rounded: The type has not changed - it is still a number, and an agent already set to one of these four needs nothing. response_speed_seconds in GET /v1/transcribers is keyed by the preset names, and that page gives the pairing.

response_speed_seconds is null where seconds mean nothing

GET /v1/transcribers gave every model a table of seconds per preset. On the two models that run their own end-of-turn detection those numbers were their provider’s figures, measured on a different model — byte-identical to the other models on that provider, for a behaviour they do not share. Those models answer when they are confident the caller has finished, not after a fixed wait, so no number describes them. Those rows now return null:
Every other model is unchanged. conversation.response_eagerness still applies on all of them and still takes the same four values — only the prediction is withheld where there is nothing to predict.
If you render the seconds in a picker, check for null first. Reading .patient off it now throws on two of the six models — where it previously returned a number that was wrong.

ElevenLabs voices now appear in GET /v1/voices

GET /v1/tts-capabilities listed elevenlabs with its languages while GET /v1/voices returned no ElevenLabs rows at all, so the provider looked available and there was nothing to put in voice.voice_id. Its 19 built-in voices are now published, the same ones the dashboard offers. Nothing else changed: no other provider gained or lost a voice, and ?provider=elevenlabs filters them like any other.

GET /v1/tts-capabilities names each provider’s model

Each provider is now {model, languages} rather than a bare language array:
Every voice provider ships exactly one model, voice.model on an agent is filled in from voice.provider, and until now no endpoint said what that value meant or what else was legal.

Numbers read back in E.164

A transfer number and an agent’s numbers.inbound_whatsapp were returned in whatever spelling they were stored in — +91 97935 79700 from a dashboard edit, Meta’s display format for WhatsApp. Both are normalised on read, as this page has always promised.

Renamed

The webhook payload already called the call id id, and carried a parenthetical explaining that it “matches execution_id from POST /v1/calls”. That note is gone — there is one name now.

Added

  • A call now carries agent_version — which version answered it — and dashboard_url, a deep link you can paste into a bug report. Chats already had both.
  • GET /v1/models now carries supports_temperature. model.temperature is accepted on every model, but only some honour it; read this before setting one. rate_per_min is also described correctly now: it is what the model contributes to a voice minute, not the price of a call.
  • New: GET /v1/whatsapp/templates — the approved names for send_whatsapp_template.allowed_templates. A typo there used to be stored without complaint and the agent then could not send.

Corrected, with nothing to change on your side

  • Idempotency-Key was published as optional. The server has always refused a request without it, so a generated SDK failed on its first call.
  • The knowledge-base read described kb_type: semantic, which the API never returns. It is non_deterministic.
  • Call status was described as four values; there are twelve, and they are now listed. Chat status is three, and completed means “no longer active” — including a visitor closing the tab.
  • memory.fields[].sort_order aside, every enum we return is now written down: KB document status, widget status and position, fact source, memory-change source_kind, activation action, ended_reason, and both number statuses.
  • A memory subject’s last_channel claimed phone, WhatsApp, the widget. It is voice or whatsapp; widget conversations are never recorded in memory.
  • A version compare reported changed platform lines under system_messages.*, a path this API stopped accepting in the previous release, and could name internal memory settings you cannot send. Both fixed.
  • model.temperature: 0 ran as 0.4.
  • transcriber.provider is derived from transcriber.model, as the model and both backups already were. A pair that disagreed used to save and then fail the call.
  • transcriber.provider, model.provider and voice.model now say they are read-only, which they always were.

2026-09-23

One release, four changes to the agent body. Three remove things the API could not honour; the fourth moves each platform-spoken line next to the setting that speaks it. All four are breaking — there is no window in which both shapes work, so update before you take the release.

Tools are built in the dashboard

tools and pre_call_tools are no longer part of the agent body. POST /v1/agents and PATCH /v1/agents/{agent_id} do not accept them, and no read returns them. Nothing changes for an agent that already has tools: they are stored, they still run, and the dashboard still edits them. A prompt that arms a tool the agent does not have is refused with prompt_tool_reference_invalid, so build the tool on the agent’s Tools tab first, then write a prompt that arms it. GET /v1/integrations/cal-com/event-types went with them — its only purpose was filling a booking tool’s cal_com.event_type_id.
A version compare never names a tool either. A difference always names a field you can read and write here.

whatsapp and knowledge_messages are gone

Both were settings the API published but could not properly support.
  • whatsapp (history window, debounce, opt-in/out keywords, media policy) is per-workspace WhatsApp setup, configured in the dashboard. The number an agent answers on, numbers.inbound_whatsapp, is unchanged.
  • knowledge_messages did not match what the runtime does with it — a custom line is shortened to 120 characters — so it is not published until that is settled.

Platform lines moved next to their settings

system_messages — the flat map keyed by stage — is removed. Each line the platform speaks now lives on the feature that decides when it is spoken: A hold check-in now sits beside the follow_up_seconds that decides when it is spoken, and the silence nudge beside the seconds that trigger it.
The value is unchanged — the same {mode, prompt, text} object, with the same rules. Only where it sits in the body has changed, so a client that reads a body, edits one line and sends the whole thing back needs no rewrite beyond the field name. Sending system_messages now answers 422 naming it as an unknown field.

Other refusals added in the same release

Values that used to save with a 200 and then do nothing are now refused, with a message naming the fix. The full list is in each field’s reference entry; the ones most likely to affect a working integration:
  • a backup (transcriber.fallback, model.fallback, voice.fallback) that could never be used, including one on the main engine’s own provider, which used to save and read back as null — see Backups
  • a builtin_tools.transfer_call.numbers[] entry whose timezone is not an IANA zone, whose hours are an empty window, or whose number is not E.164
  • builtin_tools.hold_call.follow_up_seconds at or above max_seconds
  • knowledge_base_ids or analysis_group_ids naming something this workspace does not hold
  • voice.emotion outside the published list, or on a provider that cannot speak it
  • variables[].system_defined / .deletable set to anything other than what the read returned
  • a memory.fields[] key declared twice, which used to keep the last one silently
  • memory.fields[].enum_values on a field that is not a string, where the extractor ignores it
Validation errors also changed shape: a 422 now uses the same envelope as every other error. See Validation errors.

Every body refuses a field it does not know

POST/PATCH /v1/agents has always answered 422 for a field it does not declare. Knowledge bases, custom analysis, widgets and memory accepted one and ignored it, so {"nam": "Support"} created a knowledge base called “Untitled” and said nothing. They now answer 422 naming the field, like the agent routes.

A widget is configured, not created or deleted

Every agent has exactly one widget, prepared the first time you read it. POST /v1/agents/{id}/widgets could therefore only ever answer 409 Widget already exists, and DELETE did not remove anything - the next read made a fresh widget with a new public key, silently breaking the snippet already embedded on your site. Both are removed. What is left is the whole lifecycle: read it, configure it, and rotate its key when you mean to.

invalid_request and document_invalid mean different things

invalid_request was doing two jobs: a malformed request (a ?version= we cannot parse, an If-Match that is not a revision) and a well-formed value the agent cannot use. They need different fixes, so they now have different codes - the second is document_invalid, which already meant exactly that for numbers. Messages are unchanged.

Every refusal carries a reason code

detail.code used to be missing on most 404s and 409s - the reason was prose only, and three codes this page lists (version_not_found, agent_not_published, not_public_api) never actually appeared. Every public refusal now carries one, and the reason codes table lists them. Messages are unchanged, so anything reading detail as text still reads the same sentence.