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.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:
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:
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’snumbers.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 — anddashboard_url, a deep link you can paste into a bug report. Chats already had both. GET /v1/modelsnow carriessupports_temperature.model.temperatureis accepted on every model, but only some honour it; read this before setting one.rate_per_minis 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 forsend_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-Keywas 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 isnon_deterministic. - Call
statuswas described as four values; there are twelve, and they are now listed. Chatstatusis three, andcompletedmeans “no longer active” — including a visitor closing the tab. memory.fields[].sort_orderaside, every enum we return is now written down: KB document status, widget status and position, factsource, memory-changesource_kind, activationaction,ended_reason, and both number statuses.- A memory subject’s
last_channelclaimedphone, WhatsApp, the widget. It isvoiceorwhatsapp; 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: 0ran as0.4.transcriber.provideris derived fromtranscriber.model, as the model and both backups already were. A pair that disagreed used to save and then fail the call.transcriber.provider,model.providerandvoice.modelnow 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_messagesdid 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 a200 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 asnull— see Backups - a
builtin_tools.transfer_call.numbers[]entry whosetimezoneis not an IANA zone, whose hours are an empty window, or whosenumberis not E.164 builtin_tools.hold_call.follow_up_secondsat or abovemax_secondsknowledge_base_idsoranalysis_group_idsnaming something this workspace does not holdvoice.emotionoutside the published list, or on a provider that cannot speak itvariables[].system_defined/.deletableset 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_valueson a field that is not astring, where the extractor ignores it
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.