Changelog
Notable, developer-facing changes to the Omni Octo API, MCP servers, and these docs. Newest first. Dates are UTC.
2026-10-29 — Plan mode on /v1/responses
Section titled “2026-10-29 — Plan mode on /v1/responses”tool_choice: "none"onPOST /v1/responsesruns the turn in plan mode: Octo sees its whole tool catalog and says what it would do, but calls no tool and treats no reply as a confirmation. Any other value, or none, behaves as before. A plan-mode turn reads the tools as a one-line list, not full schemas, so it is smaller and faster. See plan mode.reasoning.effortonPOST /v1/responses:low,mediumorhighfor one turn, ornoneto turn thinking off where the model allows it. See reasoning effort.
2026-10-29 — Docs: trial number, previews, paging and the event stream each in one place; Claude Code guide checked end to end
Section titled “2026-10-29 — Docs: trial number, previews, paging and the event stream each in one place; Claude Code guide checked end to end”- The shared trial number’s rules live on The trial number only; the overview, quickstart and testing pages link there.
- How long a preview stays usable, per channel, including
preview_expiredand409 preview_stale, is one row of How the channels differ. - New Lists and paging section: page sizes in a table, then the cursor rules. The ids rules on the overview are three short rules instead of one paragraph.
- The event stream is split into Connect, Resume, Frames and Limits.
- Call campaigns from Claude Code: a voice preview’s id is
rev_…, and a round that has started keeps thecontent.missionit started with, so a new mission goes in the next round (a clone). Edit a campaign says the same:call_settingsedits reach a resumed voice campaign’s calls, a newcontent.missiondoes not.
2026-10-29 — Docs: one home per rule, a recipe on every channel page, Billing and Retries pages
Section titled “2026-10-29 — Docs: one home per rule, a recipe on every channel page, Billing and Retries pages”- New pages. Billing and credits brings together the balance, what each channel costs, a campaign’s cost and spending limit, the
402refusals and the usage report. Retries, idempotency and rate limits brings togetherIdempotency-Key, replays and the limits. The old sections on the overview, Errors, SMS and WhatsApp pages now link there, and their old links still work. API errors’docs_urlfor these topics points at the new pages. - Recipes. Send a text and Send a WhatsApp template open with commands that run in order as pasted: find a sender, send, read the status, and receive the reply as
message.received. Place a call now ends with waiting for the call to end, reading the transcript and downloading the recording, and says that the recording route answers a short-lived link. - One home each. What opts a number out lists the SMS and WhatsApp opt-out words. Phone numbers states the E.164 and
%2Brule. Events has a table of which events each channel emits. The quickstart’s campaign steps live on Run a campaign.
2026-10-29 — Campaign rounds: results in counts, and the next round in one request
Section titled “2026-10-29 — Campaign rounds: results in counts, and the next round in one request”GET /v1/campaigns/{id}/results(AI-call campaigns): one aggregated read of a round. Recipients bystatusand by calloutcome(both sum torecipients), how many werereached, how many a next round would call back (unreached), calls by the agentprompt_versionthey ran on, and per Dialog Insight field what the calls extracted (counts per value for yes/no and pick-one fields;min,max,meanand five ranges for numbers; how many answered a text field). See Improve an agent between rounds.POST /v1/campaigns/{id}/clone(AI-call campaigns): the next round from an existing campaign.audience: "unreached"calls only the people it did not reach (no_answer,busy,voicemail,failed; never anyone reached on any attempt, skipped or opted out; the campaign must have finished), ;audience: "same"calls everyone it had again. The new round gets a contact list of its own, never calls anyone who asked a round to stop, andunreachednever anyone a round reached; the campaign must have finished (a draft may be copied assame), and one unfinished round of a family at a time (409 round_in_progress). Agent, sender, mission, variables, call settings, retry policy and schedule are copied; any fieldPATCHtakes is an override.start: "draft"or"auto". Send anIdempotency-Key: a retry answers the campaign the first request made.cloned_from({ id, audience, round }, ornull) on every campaign read.- MCP (
/mcp/api):get_campaign_results, andclone_campaign(needs anidempotency_key; withstart: "auto"it asks to confirm first). See API tools.
2026-10-29 — Rehearse an agent before you dial
Section titled “2026-10-29 — Rehearse an agent before you dial”POST /v1/agents/{id}/test-turns: a text rehearsal of an agent. You play the person on the line (message); the agent answers (reply) as it would on a call (channel: "voice", the default, on the model calls run on) or in the website chat ("chat"), with themissionandvariablesa voice campaign would give it. The first answer carries the agent’sopening; send itssession_idwith each nextmessage. Nobody is called or texted, nothing is stored as a conversation and no event is published; each turn bills the agent’s model tokens (usage). A session runs on theprompt_versionit started on (409 agent_changedafter an edit) and is forgotten an hour after its last request.Idempotency-Keymakes a retried turn free.not_rehearsednames what text cannot rehearse. See Rehearse an agent.- MCP (
/mcp/api):test_agent_turn(no confirm: it calls nobody).
2026-10-29 — What a campaign cost, what a call cost, and a spend cap on AI calls
Section titled “2026-10-29 — What a campaign cost, what a call cost, and a spend cap on AI calls”GET /v1/campaigns/{id}/usage: the credits a campaign has been billed so far, by category (phone,recording,speech,ai,messaging,whatsapp,other, each with an Englishlabel), its calls or messages, and an AI-call campaign’s spending limit (spend_cap). MCP toolget_campaign_usage. See Reading a campaign.billed_creditson every call (GET /v1/calls,GET /v1/calls/{id},get_call): what the call cost so far.nullfor an outbound call that ended before 2026-10-30.limits.spend_cap_creditson AI-call campaigns (it was SMS only): before each call the campaign checks the credits billed, plus the calls under way, plus one more call; when that would not fit it pauses withpaused_reason: "spend_cap", its untouched recipients still queued. Raising or removing the limit of a started campaign is a person’s act in the dashboard (403 spend_cap_raise_needs_personfor a key, which can still lower it), and a resume that would pause again at once is409 spend_cap_reached. Both carrydashboard_url. A voice preview addsspend_cap: { cap, calls_covered }. See Limits and retries.
Changed
Section titled “Changed”- A WhatsApp or voice campaign the platform pauses itself (out of credits, or its spend cap) now sends
campaign.pausedwith thatreason, as SMS campaigns did. field_not_for_channelno longer applies tolimits.spend_cap_creditson AI-call campaigns; a voicemail drop still refuses it.
2026-10-29 — Choose what calls find out (insights)
Section titled “2026-10-29 — Choose what calls find out (insights)”/v1/insights: list, create, read, change and delete insights (Dialog Insights in the dashboard), the typed fields a call’s answers are extracted into (boolean,number,text,enumwithenum_values). Answers appear in a call’sextractedunder each field’skey. Writes take owner or admin keys;PATCHtakesexpected_revision(409 revision_conflict). Nothing is re-run over past calls. See Choose what calls find out.insighton voice campaigns (POST /v1/campaigns,PATCH /v1/campaigns/{id}, read back onGET /v1/campaigns/{id}): the insight that campaign’s calls run. A change ofinsightalone is also taken while the campaign is queued or running.insightonPATCH /v1/agents/{id}(read back onGET /v1/agents/{id}): the agent’s insight, for its inbound calls and for campaign calls whose campaign names none. From now on; not part ofprompt_version.- MCP tools
list_insights,get_insight,create_insight(needs anidempotency_key) andupdate_insight(confirm first, withexpected_revision);insightonupdate_agent,create_campaign_draftandupdate_campaign_draft. See API tools.
2026-10-29 — Create, change and delete segments; page a segment’s contacts
Section titled “2026-10-29 — Create, change and delete segments; page a segment’s contacts”POST /v1/segments,PATCH /v1/segments/{id}andDELETE /v1/segments/{id}: save a filter over your contacts (tag,name,company,channel,opted_out.<channel>,birth_month,attributes.<key>, withall/any), rename it or replace its filter, and delete it. Writes need a key created by an owner or admin (403 insufficient_role). A filter with a problem is400 invalid_filter, with every problem inissues. A new filter or a delete is refused (409 segment_in_use) while a queued, running or paused campaign uses the segment. See Build a segment.GET /v1/segments/{id}/contacts: the contacts a segment matches now, newest first, with a cursor bound to the segment’s filter.filteronGET /v1/segments/{id}, in the same names.- MCP tools
create_segment,update_segment,delete_segment(asks forconfirm: truefirst) andlist_segment_contacts. See API tools.
- A segment with an
is_setoris_not_setcondition on a custom field (attributes.<key>,customField:<key>in the dashboard) failed to count, list or send. It now works.
2026-10-29 — Usage report: what the credits went on
Section titled “2026-10-29 — Usage report: what the credits went on”GET /v1/usage: the credits charged in a window, in total and grouped bycategory(phone,recording,speech,ai,messaging,whatsapp,email,numbers,other), UTCdayand/orlabel. Default window: the current billing cycle to now. See Usage and spend.GET /v1/usage/events: one row per charge, newest first, with its category, label, credits,conversation_id,agent_idandcampaign_id; andGET /v1/usage/export, the same rows as a CSV file (at most 250,000 rows, refused up front over that).- Error codes
too_many_groups,export_too_largeandexport_in_progress(Errors). - MCP tool
get_usage(API tools).
2026-10-29 — List SMS and WhatsApp sends together
Section titled “2026-10-29 — List SMS and WhatsApp sends together”GET /v1/messageswithoutchannellists your SMS and WhatsApp template sends in one list, newest first bycreated_at(to the microsecond, ties byid), each item with itschannel.to,from,campaign,statusand thecreated_*times work as they do withchannel;status=readlists WhatsApp only andstatus=undeliveredSMS only. Inbound messages and inbox replies are not in it (GET /v1/conversations/{id}/messages). See List texts.- MCP
list_messageslists both channels whenchannelis left out. See API tools.
Changed
Section titled “Changed”channelonGET /v1/messagesis optional.channel=smsandchannel=whatsappanswer as before and their cursors keep working; a cursor of one of them is refused withoutchannel, and a cursor of the list of both is refused with one (400 invalid_cursor, the hint nameschannel). A blankchannel=is400 invalid_request(“leave it out to list both”).
2026-10-29 — Campaign events named for every campaign type
Section titled “2026-10-29 — Campaign events named for every campaign type”- The event envelope’s description in the OpenAPI document said the
campaign.*events were SMS-only.campaign.started,paused,resumed,completedandcanceledcome from SMS, WhatsApp and voice campaigns;campaign.waitingandcampaign.window_openedfrom SMS campaigns only;campaign.completed’sbilled_creditsisnulloutside SMS. - The docs build’s request-example check now also checks enum values: a body field or query value the API would refuse as not one of its allowed values fails the build.
2026-10-29 — The inbox by newest message, and a poll for what changed
Section titled “2026-10-29 — The inbox by newest message, and a poll for what changed”sort=last_message_atonGET /v1/conversations: the conversation with the newest message first, as the dashboard’s inbox shows it (a call by when it started). The default stayscreated_at. See Newest activity first.updated_afteronGET /v1/conversations: the conversations that changed at or after a time, oldest change first, each one again later in the walk if it changes while you page. Poll with it instead of running a webhook endpoint: keep the largestupdated_at, ask again from 2 minutes before it, and page to the end. It takes nosort. See Poll for changes.- MCP
list_conversationstakessortandupdated_after.
Changed
Section titled “Changed”- A conversation’s
last_message_atnow counts every message the thread shows: your AI agent’s replies, campaign sends and automated messages too (before, only the person’s messages and the replies a teammate or/v1sent). It is null for a call. - A conversation’s
updated_atnow moves on every change the object or its detail shows: a new message by anyone, the status, the contact it is linked to, the name, phone or email, the agent, the labels and the summary. It never moves backwards. Before this release it did not move for some of these: start anupdated_afterpoll from the release time or later.
2026-10-29 — Contacts: filter by tag or attribute, and sync what changed
Section titled “2026-10-29 — Contacts: filter by tag or attribute, and sync what changed”GET /v1/contactstakestag(one tag, exact and case-sensitive),attribute_key(has that attribute) with an optionalattribute_value(its value as text), andupdated_after(an ISO 8601 time: contacts changed at or after it, oldest change first, byupdated_atthenid). They combine with each other and withquery, not withphoneoremail. The MCP toollist_contactstakes them too. See Sync changes.
Changed
Section titled “Changed”- A contact’s
updated_atnow also moves when one of its numbers or emails is changed (in the dashboard or by Octo), when a workflow captures a number or email onto it, when an import adds a number to a contact you already had, and when a carrier reports that the number opted out. - The default order of
GET /v1/contactsand the cursors it already answered are unchanged.
2026-10-28 — DeepSeek V4.1 Flash
Section titled “2026-10-28 — DeepSeek V4.1 Flash”- Model
deepseek-flash(DeepSeek V4.1 Flash) on/v1/responses,/v1/chat/completionsandGET /v1/models.
Changed
Section titled “Changed”deepseek-v4-flashstays in the list and keeps working. DeepSeek now serves it with V4.1 Flash, and it is billed at the same rate asdeepseek-flash, which is higher than before.
2026-10-28 — Call transcripts, call summaries, agent filters, one campaign recipient
Section titled “2026-10-28 — Call transcripts, call summaries, agent filters, one campaign recipient”GET /v1/calls/{id}/transcript: what was said on a call, oldest turn first. Each turn hasspeaker(agent, orcallerfor the other end of the line),textandat_ms(milliseconds afterstarted_at, when the call was answered), with the call’sstatus,summaryandprompt_version. Any API key. See Voice → Read the transcript.summaryon every call object (GET /v1/calls,GET /v1/calls/{id},POST /v1/calls,POST /v1/calls/{id}/end): what the call was about, once it was analysed.?agent=(anag-…id or UUID) onGET /v1/callsandGET /v1/conversations: one AI agent’s calls and conversations. An unknown or deleted agent is404 agent_not_found.GET /v1/campaigns/{id}/recipients/{recipient_id}: one campaign recipient by the list’sidor itscr-…id, withcampaign_id,contact_id,created_atandcompleted_at; a voice recipient addsduration_sec, its call’stranscript,extractedandsummary. See Voice → One campaign recipient.short_id(cr-…) on every campaign recipient inGET /v1/campaigns/{id}/recipients.- MCP API tools:
get_call_transcriptandget_campaign_recipient;list_callsandlist_conversationstakeagent. See API tools.
- The docs said a call’s transcript is only in the dashboard. It is on
GET /v1/calls/{id}/transcriptnow, and the Conversations reference and the MCP tool descriptions point there.
2026-10-28 — Read and change an agent’s prompt, with versions
Section titled “2026-10-28 — Read and change an agent’s prompt, with versions”PATCH /v1/agents/{id}changes an agent’sprompt,greeting,model(chat,voice),voiceandlanguage(owner and admin keys). Sendexpected_version(theprompt_versionyou read): when someone changed the agent meanwhile, nothing changes and the answer is409 version_conflictwithcurrent_version. A request that changes nothing answers the agent as it is. The answer carrieswarnings(model_replaced,voice_language_mismatch).voicemail_messageis refused (field_not_supported): voicemail is a campaign setting. See Improve an agent between rounds.- Every change of an agent’s prompt, greeting, model, voice or language, from the API, the dashboard, Octo or a Live signal, makes a new
prompt_version.GET /v1/agents/{id}/prompt-versionslists them (newest first, withsourceand whatchanged);GET /v1/agents/{id}/prompt-versions/{version}reads one with its texts. - Calls carry
prompt_version: the version they ran on, recorded when they started (a campaign’s calls: the version it started with;nullfor calls before today).GET /v1/callstakes?prompt_version=(combine it withcampaign). - MCP (
/mcp/api):update_agent(asks to confirm first, then needsconfirm: trueandexpected_version),list_agent_prompt_versions,get_agent_prompt_version;get_agentanswers the new fields.
Changed
Section titled “Changed”GET /v1/agents/{id}now answers the agent’sprompt,greeting,model,voice,language,prompt_versionandupdated_at; before, it answered only the placeholders its texts use.GET /v1/agentsitems addprompt_versionandupdated_at. The texts are what the agent’s owner wrote: treat them as data.
2026-10-28 — Call settings on voice campaigns and calls: turn phone-menu handling off
Section titled “2026-10-28 — Call settings on voice campaigns and calls: turn phone-menu handling off”call_settingson voicePOST /v1/campaigns,PATCH /v1/campaigns/{id}andPOST /v1/calls, read back (every field effective) onGET /v1/campaigns/{id}and the list (nullon SMS and WhatsApp):phone_menu(auto,always,never),record_calls,max_hold_seconds,voicemail(actionhang_uporplay_recording,recording,detect_by_agent) anduse_knowledge_base. To turn phone-menu (IVR) handling off, send"call_settings": { "phone_menu": "never" }. On a paused campaign the new settings reach the calls placed after it resumes;max_hold_secondsis fixed once a campaign has started. See Call settings./v1/voicemail-recordings: upload (multipartfile, MP3, M4A or OGG up to 5 MB), list, read with a link to listen, and delete (refused with409 voicemail_recording_in_usewhile an unfinished campaign plays it). A campaign or call plays one withcall_settings.voicemail: { "action": "play_recording", "recording": "vm-…" }. See Voicemail recordings.- MCP tool
list_voicemail_recordings;create_campaign_draft,update_campaign_draftandplace_calltakecall_settings. See API tools.
Changed
Section titled “Changed”- A new voice campaign, and a
POST /v1/calls, with nocall_settings.phone_menunow runsauto: the agent listens first, talks when a person answers and presses keys when a phone menu answers. Before, every/v1call expected a phone menu first (always), so a person who answered waited for a reply. Send"phone_menu": "always"for the old behaviour. - Voice campaigns created through
/v1before this change keepalways, as they ran;GET /v1/campaigns/{id}now shows it ascall_settings.phone_menu: "always". To change one, pause it,PATCHcall_settings.phone_menu, and resume it. - A voice start refused because the campaign plays a voicemail recording it does not name now points at
call_settings.voicemail.recording(it saidcontent.voicemail_drop).
2026-10-28 — Call campaigns from Claude Code; /mcp/api easier to find
Section titled “2026-10-28 — Call campaigns from Claude Code; /mcp/api easier to find”- New guide, Call campaigns from Claude Code: draft, preview and start a voice campaign over the
/mcp/apitools, wait for its calls withwait_for_events(or Claude Code’s Monitor and the event stream), read the outcomes, and run the next round. - The docs home, the quickstart, the Messaging API overview and Settings → API Keys & Webhooks in the dashboard now show the
/mcp/apiserver and itsclaude mcp addcommand.
- Connect an MCP client said
/mcp/apihad 17 tools; the list is on API tools. - Two route names missing their
{id}:PATCH /v1/webhooks/{id}(choosing a webhook’sapi_version) andPATCH /v1/campaigns/{id}(content.mediaafterPOST /v1/media). - The docs build now checks every
/v1request example against the API’s OpenAPI document (path, method, query parameters and body fields).
2026-10-28 — Poll the event log, follow one campaign, and resume from MCP
Section titled “2026-10-28 — Poll the event log, follow one campaign, and resume from MCP”GET /v1/events?wait=<0..25>answers one JSON page instead of a stream:{ object: "list", data, has_more, next_cursor, replay_truncated, render_errors }, as soon as there is an event or afterwaitseconds with none. Withoutafterit starts from now and returns your firstnext_cursor.limit(1 to 200, default 100) sizes the page. At most 5 waiting polls per key, 20 per workspace (429 too_many_waits). See Poll instead of stream.?campaign=<ca-… or UUID>onGET /v1/events, stream and poll: only that campaign’s events. Withouttypes, its texts’ status events and its calls’ events are still left out, as with"*".- MCP tools
wait_for_events(the poll, always v2, owner or admin keys) andresume_campaign(confirm andidempotency_key, likestart_campaign). See API tools.
Changed
Section titled “Changed”- A WhatsApp or voice campaign’s
campaign.completedandcampaign.canceledare now written exactly once: one event id per campaign, and one that could not be written when the campaign ended is written 5 to 10 minutes later instead of being lost. Its*_atis then the time it was written. See the event rules. POST /v1/campaigns/{id}/resumehonoursIdempotency-Keyon WhatsApp and voice campaigns too (it was ignored): a retry with the same key answers the campaign as it is now and never resumes it twice, and the same key with another body is422 idempotency_key_reuse. If you reused one key for two different resumes within 24 hours, use a new key per resume.
2026-10-28 — The daily voice budget follows the workspace’s own day
Section titled “2026-10-28 — The daily voice budget follows the workspace’s own day”Changed
Section titled “Changed”- The daily voice-minute budget now resets at midnight in the workspace’s timezone, not 00:00 UTC. Existing workspaces got the zone their campaign schedules use; a workspace with none stays on UTC until its first scheduled voice campaign, or until an owner sets it in Settings → Voice.
- A scheduled voice campaign is judged against the day it starts. Its calls hold minutes on that day, not on the day it was launched.
- Calls are projected at this workspace’s own average billed minutes per call over the last 30 days (at least 1.5 minutes; 3 minutes until there are 20 calls), not a flat 3 minutes.
400 daily_voice_minutes_exceededon a campaign preview or start carriesmetered(minutes that day already billed),heldandheld_by(queued or scheduled campaigns holding minutes that day, byca-…id),day,time_zone,resets_atandper_call_minutes.usedkeeps its value: it already counted queued campaigns’ calls along with billed minutes, and is now documented asmetered+held; readmeteredfor minutes actually billed. See Errors.
2026-10-27 — “OpenAI Realtime + your voice” voice pipeline
Section titled “2026-10-27 — “OpenAI Realtime + your voice” voice pipeline”- A voice agent can run a fourth pipeline, picked on its Configure tab: OpenAI’s realtime model (
gpt-realtime-miniorgpt-realtime) hears the caller and writes the reply, and the agent’s own TTS voice speaks it. There is no separate speech recognizer, so replies come sooner. A call that has to get through a phone menu runs the standard pipeline instead. The/v1API does not expose an agent’s pipeline; it shows up in a voice campaign preview’scost_estimate.per_call_components, which for such an agent pricesrealtimeandttsand nostt. See Campaigns.
2026-10-26 — WhatsApp replies as events, media uploads, read-by-id, more MCP tools
Section titled “2026-10-26 — WhatsApp replies as events, media uploads, read-by-id, more MCP tools”Breaking
Section titled “Breaking”message.receivednow also fires for WhatsApp messages people send you.data.channelissmsorwhatsapp. On v2 the field was alwayssms; on 2026-10-01 it is a new optional field, and absent meanssms. If your handler assumes SMS, checkchannel. See WhatsApp → Hear replies.
message.receivedcarriesquick_reply:{ payload }for a WhatsApp button tap, otherwisenull. It is optional on 2026-10-01.POST /v1/mediauploads an MMS picture or video and answers{ object: "uploaded_media", media, content_type, size, transcoded, preview_url, preview_expires_in, expires_at }.- Use the returned
mediaasmediaonPOST /v1/messages, ascontent.mediaon campaigns, or asmediaon a conversation reply. - An upload that is never used is deleted after 7 days.
- The SMS guide now lists MOV and WebM too.
- Use the returned
- New single-object reads:
GET /v1/templates/{id}GET /v1/agents/{id}, withvariablesGET /v1/segments/{id}, withcontactsGET /v1/sms-templatesandGET /v1/sms-templates/{id}
- New fields: a WhatsApp template has
uuid,created_atandall_buttons(every button, with a URL button’surlanddynamic). An agent hasdescription,languageandcreated_at. - An SMS preview waits up to 5 s before answering. A
buildingpreview carriespoll_after_ms,progressandRetry-After. See Polling an SMS preview. PATCH /v1/lists/{id}renames a contact list.POST /v1/listsacceptscontact_ids(up to 1,000), the same asPOST /v1/lists/{id}/contactsdoes. A request that names people but would add none of them is400 empty_audience_input, and no list is created.GET /v1/eventsanswers anOcto-Event-Versionheader naming the payload version of its frames. The stream still defaults to2026-10-01; pass?api_version=v2, the format new webhook endpoints get. Payload versions now lists every default in one table.- MCP API tools (50 in all):
- campaigns:
list_campaigns,list_campaign_recipients,update_campaign_draft(a draft or a paused campaign),delete_campaign_draft(asks forconfirm: truefirst); - sends and calls:
list_messages,list_calls; - catalog:
list_segments,get_segment,get_template,get_agent,list_sms_templates,get_sms_template; - lists:
rename_contact_list; - webhooks:
list_webhook_deliveries(owner or admin keys). - See API tools.
- campaigns:
Changed
Section titled “Changed”PATCH /v1/campaigns/{id}can change an SMS draft’sfrom(E.164,pn-…orch-…).- It is refused the same way as on create (
invalid_from,from_not_sms_capable,trial_sender_not_allowed). - A number not cleared yet is accepted; preview and start then report
sender_not_cleared. - A paused SMS campaign still answers
400 field_not_editable.
- It is refused the same way as on create (
GET /v1/campaignstakesstatus=preparing: a one-request SMS create whose preview is still being built.status=draftno longer matches those campaigns.- List items now read
preparing, asGET /v1/campaigns/{id}already did. - If you listed drafts to find campaigns still preparing, filter by
preparing.
- A
401 invalid_api_keynow hasparam: "authorization"and a message naming the cause: no key, the key inX-API-Key, anAuthorizationheader withoutBearer, or a key that is not valid.type,codeand the status are unchanged, and the key is never repeated. - Corrected: the Conversations reference said a call’s transcript is on
GET /v1/calls/{id}. It is not. That route carries the outcome and the extracted answers; the transcript is in the dashboard. - The dashboard has a new look: four themes (Day, Night, Rose and Ocean) on a full-window gradient, with the Octo panel and the sidebar as glass layers and every page on one solid pane. Your saved theme carries over and now follows you to other devices. If your system’s “reduce transparency” setting is on, every surface is solid.
- The Octo panel is rebuilt as one panel that works the same in the corner, full screen and on a phone.
- A new header shows the animated Octo, with its state read aloud for screen readers.
- One composer replaces the separate ones: a progress ring around the dictation mic shows how far a job has got, and the mic types what you say into the box.
- Messages and cards (plans, confirmations, created resources) are restyled, and a finished turn is read aloud once.
- An empty chat shows three suggestions to start from.
- On a phone the panel opens as a sheet.
- While a past conversation loads, you can type but Send waits until it has loaded.
- Nothing changes in
/v1or the MCP tools.
- Sending back a
nullthatGET /v1/campaigns/{id}returned for another channel’s field ("retry": nullon an SMS campaign) is accepted on create andPATCH, and changes nothing. - A body that leaves out
channelanswers"`channel` is required: `sms` or `whatsapp`."withparam: "channel". - WhatsApp: a person who stops marketing messages from WhatsApp’s own menu is now opted out of WhatsApp, as with a STOP. A send Meta refuses with 131050 records it as well.
Changes to the dashboard API (/api):
- Email channel routes answer
404for a malformed channel, thread, draft or attachment id (was500). The thread routes acceptthr-…ids and answer404for a thread that is not on the URL’s channel. POST /api/conversations/click-to-callanswers a disabled agent with409 { "error": "agent_disabled", "message": "…" }.GET /api/billing/usagecursors keep microseconds, so no row is skipped. A malformed cursor is400 invalid_cursor.GET /api/billing/usage/exportrefuses more than 250,000 events with400 export_too_largeinstead of truncating.GET /api/segments/{id}/memberspages byafter/nextCursor(offsetis400), and the total is returned only withincludeTotal=true.DELETE /api/segments/{id}answers409 segment_in_usewhile a queued, running or paused campaign uses the segment.
2026-10-24 — Webhook delivery history, WhatsApp opt-outs, docs corrections
Section titled “2026-10-24 — Webhook delivery history, WhatsApp opt-outs, docs corrections”GET /v1/webhooks/{id}/deliveriesandGET /v1/webhooks/{id}/deliveries/{delivery_id}: see which deliveries to an endpoint succeeded, are retrying or were given up, what your server answered (response_status,error,response_excerpt) and the exact body sent (payload). Kept for 7 days; replay fromGET /v1/events?after=<seq - 1>. See what was delivered.- WhatsApp: a person’s taps on your template’s quick-reply buttons now reach the conversation and your agent like a typed reply, with the button’s payload. Reactions and stickers show in the conversation too.
- WhatsApp: a tap on a “Stop” / “Stop promotions” button, or STOP, STOP ALL, UNSUBSCRIBE or OPT OUT sent as the whole message, opts the person out of WhatsApp. Later WhatsApp sends to them are refused with
409 opted_outuntil they send START. It does not stop texts or calls. See WhatsApp → Opt-outs.
- Octo no longer redeploys, edits or crawls for a deleted agent; a deleted agent stops answering inbound messages and calls even if a deployment still points at it.
Changed
Section titled “Changed”- Corrected: texts are Twilio-only too. The Errors page said single texts work on every provider. They do not: like SMS campaigns, API texts send from Twilio numbers only for now, and a Telnyx or Vonage
fromis refused (400 from_not_sms_capableon a text,422 sender_provider_unsupportedon a campaign). If you planned to text from a Telnyx or Vonage number, use a Twilio number. - Corrected: the compliance acknowledgment gates campaign starts only. The account reference said a send could be refused over it. Single texts, WhatsApp messages and calls never check it; only a campaign preview and start do.
- Corrected:
message.receivedis SMS only. The Webhooks page listed it for “replies and inbound texts” with no channel. WhatsApp messages people send you are not events yet: read them withGET /v1/conversations. If you built a WhatsApp inbound handler onmessage.received, it will never fire. - The shared trial number calls any phone number on your profile or a teammate’s (
GET /v1/meuser_phone), not only your own; the Overview, Testing, Errors and Place a call pages, and the API’sfix/hinttext, now say so. billed_creditsis the cost of an SMS; WhatsApp sends answernull(see WhatsApp pricing).opted_outon a text also covers a number on your do-not-contact list, not only one that texted STOP.- MMS uploads take MOV and WebM videos as well as MP4; every video is sent as an MP4 of at most 500 KB.
- An SMS preview older than 30 minutes, or replaced by a newer one, reads
preview_expired; a start from it is refused409 preview_stale. - An
Idempotency-Keycollides only with a request of the same kind (the same route).POST /v1/contacts,POST /v1/lists(and adds to a list) andPOST /v1/conversations/{id}/messagesare keyed too, now listed in the Idempotency table. - Authentication names the whole messaging API and links its own rate limits.
- MCP
start_campaignneeds apreview_idon every channel, unlike/v1, where WhatsApp and voice may start without one. - The campaign draft examples send an
Idempotency-Key; the SMS preview example is areadypreview withsamples; the send examples showtextandmedia.
Changes to the dashboard API (/api):
POST /api/lk/tokennow requires a signed-in session and an agent of your workspace. It answers401without one,403 session_requiredfor an API key,404 agent_not_foundfor an agent id that is not a live agent of your workspace, and409 agent_disabledfor a turned-off agent.
2026-10-22 — Test calls let the agent ask you
Section titled “2026-10-22 — Test calls let the agent ask you”Changed
Section titled “Changed”- A call with
source: "agent-test"now keeps the ask-a-human tool on unless you passskipInputGathering: true: when the person on the call asks something only you can answer, the question reaches you on the dashboard. Phone-menu navigation still defaults to off for test calls.
2026-10-22 — Any agent can call from the trial number
Section titled “2026-10-22 — Any agent can call from the trial number”Changed
Section titled “Changed”POST /v1/callsfrom the shared trial number (pfn-…) accepts any agent of your workspace asagent. The agent chosen for the trial number in the dashboard is now the default whenagentis left out; naming a different agent of your workspace used to be400 invalid_from. The route guard and the trial caps are unchanged.- MCP
list_phone_numbersaddsdeployed_agents,can_dialandinbound_agentper voice and SMS channel, andonly_activenow keeps every number that can dial, whether or not an agent answers it.outbound_agentsandoutbound_enabledstill return the same values for one more release; move todeployed_agentsandcan_dial.
2026-10-20 — Octo builds a list from an SMS campaign’s recipients
Section titled “2026-10-20 — Octo builds a list from an SMS campaign’s recipients”- Octo tool
create_list_from_campaign: a contact list of an SMS campaign’s recipients, filtered bystatus,clickedandreplied(for example everyone who got the text but did not click), withpreviewOnlyto count first. People who opted out are always left out. Listed in the tool catalog. No/v1change.
2026-10-19 — Octo sends a WhatsApp template by name
Section titled “2026-10-19 — Octo sends a WhatsApp template by name”- MCP
send_whatsapp_templatenames its template bytemplateName(pluslanguagewhen that name is approved in more than one language), asPOST /v1/messagesalready does withtemplate.name.templateIdstill works. A name that matches several templates, or none, is refused before any confirmation is asked for, listing the matches or the closest name. - Send a WhatsApp template: how to send a template that has no variables, as a single message or in a campaign.
- Run a campaign: the example preview id now has
the launch hash, so a start copied from the guide no longer answers
409 preview_stale. The guide also shows therev_…id a WhatsApp or voice preview returns.
2026-10-19 — SMS confirmations follow the conversation’s language
Section titled “2026-10-19 — SMS confirmations follow the conversation’s language”Changed
Section titled “Changed”- On
POST /v1/responsesandPOST /v1/chat/completions, the text of a pending SMS campaign confirmation (people, segments, credits, the time it takes, a scheduled start, a re-count, a test send) is now written in the conversation’s language, as the closing “Reply YES …” line already was. The action code,YES, the sender and every number are unchanged; numbers are formatted for the language (“1.240” in Portuguese), so do not parse the English sentence.
2026-10-19 — A deleted agent is an unknown agent
Section titled “2026-10-19 — A deleted agent is an unknown agent”- A deleted agent’s id is refused like an unknown one, and nothing is
dialed:
POST /v1/calls(agent) andPOST /v1/campaigns/PATCH /v1/campaigns/{id}(content.agent) answer404 agent_not_found, the same body as for an id that names nothing. MCPplace_call,create_campaign_draft,preview_campaignandstart_campaignfollow. - A voice campaign whose agent was deleted after it was saved: a
PATCHof its content answers404 agent_not_found, a preview listsagent_not_found, and a start refuses it. A campaign whose agent is deleted while it runs skips its remaining calls asagent_disabled. POST /v1/callswithoutagent, from a number whose own agent was deleted, answers400 agent_required, as for a number with no agent.
Changes to the dashboard API (/api):
- Agent ids are now checked against the workspace.
POST /api/conversations/click-to-callanswers404 { error: "agent_not_found" }for anagentIdthat is not a live agent of this workspace (was: the call was placed, or400/409), and accepts anag-…id as well as a UUID. POST /api/email-channel/{channelId}/threads/{threadId}/modeandPOST /api/simulation/startanswer404for such an agent.
2026-10-19 — MCP can start campaigns again
Section titled “2026-10-19 — MCP can start campaigns again”- On the messaging MCP server,
preview_campaign(SMS) andstart_campaignanswered404 not_foundfor every preview, so no campaign could be started through MCP. Preview ids gained a launch hash (snap_…_<ms>_<hash>), and the MCP server’s id check had not learned the new shape. Nothing about the ids changes for REST callers.
2026-10-19 — A send linked to its inbox message, paged search, one primary per kind
Section titled “2026-10-19 — A send linked to its inbox message, paged search, one primary per kind”- A conversation message carries
message_id, itsGET /v1/messages/{id}id; a search match addsapi_message_id. A message addstextandmedia. GET /v1/conversations/searchpages: newest first, withafter.GET /v1/contacts/{id}/channelspages withlimitandafter.GET /v1/sendersaddssms.channel_idandvoice.channel_id.- Preview samples add
contact_short_id(thect-…id;contact_idstays the UUID); the error envelope declareschannels,cap,used,remaining,resets_at,allowed_countryandcloses_at.
Changed
Section titled “Changed”- A contact has one primary phone number and one primary email: making an
email primary leaves the primary number as it is, and a number’s SMS,
calls and WhatsApp are one address.
primaryis what the contact’sphoneoremailshows. - A contact’s
updated_atmoves when a number or email is added or removed, and on an opt-out or opt-in. - Search results come newest first;
scoreis the search index’s relevance when it scored a match, else0. {{contact.id}}on a WhatsApp or AI-call campaign is400 invalid_variableon that variable, with a hint.
- A conversation opened by
POST /v1/messagesanswers itsphone, and every conversation fillsname,phoneandemailfrom its contact. - A preview with no audience lists
sender_not_clearedtoo. - An SMS campaign’s
variableskeep the text’s own|fallback. invalid_rowscounts thecontact_idsan import skipped.- Example idempotency keys are unique; keys are per workspace.
2026-10-19 — A WhatsApp opt-out keeps its Idempotency-Key
Section titled “2026-10-19 — A WhatsApp opt-out keeps its Idempotency-Key”POST /v1/messageswithchannel: "whatsapp"now keeps a refusal that came after the message was stored, and a retry with the sameIdempotency-Keyreplays it (Idempotency-Replayed: true). That is409 opted_out, and the refusals for a number pending verification or disconnected, a suspended workspace or a blocked destination. Before, the key was freed, so a retry stored a second not-sent message. A refusal that stored nothing (the template’s variables, billing, the channel, a sending rate) still frees the key. How retries work.
2026-10-19 — The inbox: conversations, replies, search
Section titled “2026-10-19 — The inbox: conversations, replies, search”- Work the inbox, a new guide to
/v1/conversations. GET /v1/conversationslists your inbox on every channel, andGET /v1/conversations/{id}reads one, with itssummaryandlabels.GET /v1/conversations/{id}/messagespages a conversation’s messages, with who wrote each (author).POST /v1/conversations/{id}/messagesreplies in the thread, through the same checks as a reply from the inbox. On SMS it sendsmessage.sentwithauthor: "api".PATCH /v1/conversations/{id}hands a conversation to a person, returns it to the AI agent, or closes it.GET /v1/conversations/searchfinds messages by their words.POST /v1/calls/{id}/endends a call in progress, andGET /v1/calls/{id}/voicemaildownloads its voicemail.- Seven new MCP API tools for the inbox, including
reply_to_conversationandend_callbehindconfirm: true.
2026-10-18 — An auto-start nobody can receive keeps its draft
Section titled “2026-10-18 — An auto-start nobody can receive keeps its draft”POST /v1/campaignswithstart: "auto"and a WhatsApp or voice audience nobody can be sent to keeps the campaign as a draft, as an SMS one already did. The answer is still422 no_recipientswithskipped_by_reason, and now names the draft incampaign_id(withshort_idandstate: "refused"); before, no campaign was created. Fix the audience and start the draft, or delete it. A retry with the sameIdempotency-Keyanswers the same refusal for the same draft and creates no second campaign; it used to create the campaign once the audience was fixed.GET /v1/campaigns/{id}reports the refusal inrefusal.
2026-10-18 — Contacts: several numbers, tags, opt-outs, search
Section titled “2026-10-18 — Contacts: several numbers, tags, opt-outs, search”- Keep contacts, a new guide to
/v1/contacts. GET /v1/contacts?query=searches names, companies, tags, numbers and emails.- A contact carries
tags;POST /v1/contactstakes them, andPOST /v1/contacts/{id}/tagsandDELETE /v1/contacts/{id}/tags/{tag}change them. Tag workflows start for them. POST /v1/contacts/{id}/dncopts a contact out of texts, calls, WhatsApp or email, or back in. An opt-in never clears the person’s own STOP (409 stopped_by_person)./v1/contacts/{id}/channelslists, adds and removes a contact’s phone numbers and emails.GET /v1/contact-fieldslists the custom contact fields.POST /v1/lists/{id}/contactstakescontact_ids, contacts you already have.- Ten new MCP API tools for contacts and lists,
including
delete_contact_listbehindconfirm: true.
2026-10-18 — An auto-start nobody can receive is refused
Section titled “2026-10-18 — An auto-start nobody can receive is refused”POST /v1/campaignswithstart: "auto"and a WhatsApp or voice audience nobody can be sent to is422 no_recipients(withskipped_by_reason), and no campaign is created. A voice campaign used to complete empty; a WhatsApp one answered400 audience_has_no_whatsapp_contacts.start: "draft"is unchanged.- An opt-out on a contact’s old number stays on the number when
PATCH /v1/contacts/{id}changes or removes it, however old the opt-out.
2026-10-18 — Contacts and lists, previews that run every start check
Section titled “2026-10-18 — Contacts and lists, previews that run every start check”- Breaking:
startis required onPOST /v1/campaigns("draft"or"auto"); without it the answer is400 start_requiredand nothing is created. - Breaking: an id of the wrong kind is
400 invalid_request, not404, on every campaign{id}route,GET /v1/calls/{id},GET /v1/messages/{id}, thecampaignfilters,/v1/mediaand/v1/recordings. See Ids. - Breaking:
/v1/events,/v1/mediaand/v1/recordingsanswer the/v1error envelope, refuse unknown or repeated query parameters, and refuse anafterthat is not a whole number (400 invalid_cursor; it used to replay seven days). - Breaking: webhook endpoint
created_atandupdated_atare ISO 8601 strings, not Unix seconds. See the webhooks reference. - Breaking: a WhatsApp template
idwith another template’snameorlanguageis400; a misspelledaudience.contacts[]key is400 unknown_field; WhatsApp previews’no_whatsappisno_phone_number; a WhatsApp or voice start that reaches nobody is422 no_recipients;GET /v1/sendershas the new statusesprovider_unsupported(SMS) andtrial_agent_not_chosen(voice). - New: Contacts and
Contact lists on
/v1, and Balance and compliance (GET /v1/balance,GET /v1/compliance,POST /v1/compliance/accept). Six new MCP API tools read them. - A preview runs every check
its start makes, lists every problem with a
hint, and goes stale when the sender, template, agent, compliance acceptance or audience changes. Voice previews addcost_estimate; WhatsApp samples addbuttons. - A campaign reaches a contact through the number on any phone channel; WhatsApp and voice skip a recipient with a blank variable, as SMS does.
GET /v1/callsfilters bytoandstatus; WhatsAppGET /v1/messagestakesfromandcampaign.- A new Quickstart, and Environments and How the channels differ on the overview.
2026-10-17 — Calls need an agent, v2 on the event stream
Section titled “2026-10-17 — Calls need an agent, v2 on the event stream”- Behaviour change:
POST /v1/callswithoutagent, from a number with no agent of its own, is400 agent_requiredand dials nothing. It used to call the real person with a built-in test agent. Name an agent fromGET /v1/agents. - The event stream sends v2
with
?api_version=v2, the same body a v2 webhook endpoint receives. The default is still2026-10-01. - Outbound test calls now send
call.ringingbeforecall.answered. - MCP API tools: waits end within
wait_seconds, and every method other than POST is405. schedule.end_atmust be at least 1 ms afterstart_at, and a schedule reads back with the fraction of a second you wrote.- Reference: clearer
Idempotency-Key, message and call fields, required fields first, a WhatsApp example, links to each page’s error codes, andpaused_reasonvalues.
2026-10-17 — Messaging API docs as task guides, and a field reference
Section titled “2026-10-17 — Messaging API docs as task guides, and a field reference”- Task guides: Send a text, Send a WhatsApp template, Place a call, Run a campaign Receive webhooks and the new Test without spending. Each is short and links to the one page that states each rule.
- The Quickstart is now Call your own phone. The old URL still works.
- Errors says what every code means and how to fix it, one table per resource, with every rate limit in Rate limits.
- Reference is generated from the OpenAPI file: every field of every endpoint, with its type, values and limits, on one page per resource. The rules that span several campaign fields are on Reference → Campaigns.
- Events lists every v2 event type field by field, with what each event promises. The legacy format’s tables are on Events, 2026-10-01 format.
- The webhook mechanics (registering, verifying, retries, the event stream) are documented once, in Receive webhooks; the OpenAI-compatible Events & webhooks page keeps the event catalog and links there.
2026-10-17 — Webhook payloads v2
Section titled “2026-10-17 — Webhook payloads v2”- Payload versions per endpoint. Every webhook endpoint has an
api_version. Endpoints that existed before this release (v1.84.0.0) keep2026-10-01and receive exactly the same bytes as before. A new endpoint (API or dashboard) getsv2unless you ask for"api_version": "2026-10-01";PATCHswitches it. TheGET /v1/eventsstream still sends2026-10-01. See Payload versions, with an upgrade recipe. - v2 events use the
/v1words and ids:object,api_versionandlivemodeon the envelope;channeleverywhere;canceled; message events keyed by the/v1message id (message.sentaddsmessage_idsfor a split reply); onemessage.status_updatedshape for SMS and WhatsApp witherror.messageandread_at;campaign.completedcountsin theGET /v1/campaigns/{id}buckets, as of completion; short ids and UUIDs side by side,sender_id; and your ownmetadataon message, call and campaign events. The OpenAPI document lists every v2 event underwebhooks. /v1/webhooksanswers like the rest of/v1(breaking): the/v1error envelope withcodeandrequest_id, strict bodies and query strings (400 unknown_field/unknown_parameter), a paged list (has_more,next_cursor,after; it used to stop at 200 endpoints),405withAllow,400for an id of another kind, and the per-key request limit of the messaging routes (300 reads, 100 writes a second) instead of 60 a minute.
2026-10-17 — The Messaging API as MCP tools
Section titled “2026-10-17 — The Messaging API as MCP tools”- New MCP server: API tools at
https://api.omniocto.com/mcp/api. 13 tools over the Messaging API (send_message,place_call,get_message,get_call,create_campaign_draft,preview_campaign,start_campaign,get_campaign,pause_campaign,list_senders,list_templates,list_agents,list_contact_lists), with a workspace API key. Each tool makes the/v1request you would make, so errors, rate limits and idempotency are the API’s own. send_message,place_callandstart_campaignneedconfirm: trueand anidempotency_key; without them they answerconfirmation_requiredand send nothing.start_campaignneeds apreview_id.create_campaign_draftneeds anidempotency_keytoo.- There is no test mode yet: every message and call is live.
/mcpand/mcp/toolsare unchanged.
2026-10-17 — Messaging API: rate limits, schedules that end, small fixes
Section titled “2026-10-17 — Messaging API: rate limits, schedules that end, small fixes”- Rate limits per API key (breaking: a
429is now possible): the messaging routes take 300 reads (GET) and 100 writes a second per key. Every answer carriesX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(Unix seconds); past the limit the answer is429 rate_limitedwithRetry-After. SMS sends keep their own, stricter send limit (10 a second), whose429carries its ownRetry-After. See Rate limits. schedule.end_atis enforced (breaking): atend_ata campaign starts no new text or call, a call or text under way finishes, everyone not reached readsskipped, and the campaign turnscompletedwith the newcompleted_reason: "schedule_ended". A date is the end of that day intz.end_atmust be afterstart_atand now, and a start after it has passed is400 invalid_scheduleonschedule.end_at. A voice recipient the campaign never called (the end, DNC) readsskipped.- Schedules are checked (breaking: some schedules accepted before are now
400):hoursstartis00:00–23:59andend00:00–24:00(25:99was taken); a window that starts and ends at the same time isinvalid_schedule; a laterstartthanendis an overnight window. Astart_atwith an offset is taken (it was refused).start_atandend_atread back as instants with the zone’s offset. - Cursors belong to their filters (breaking: every cursor answered before
this release is refused once; start again without
after): a cursor reused with other filters (to,status,campaign, the times, …) is400 invalid_cursorwith a hint. The same filter written another way keeps the cursor. field_not_for_channel: a field of another channel’s campaigns (content.missionon SMS,limits.spend_cap_creditson voice,retryon WhatsApp, …) is one400on create and PATCH, withparamandfields(it wasunknown_field,field_not_editableorinvalid_request).- Templates list their words:
GET /v1/templatesanswersbodyandheader_text. - A call’s
missiontakes 4,000 characters (it was 500), as a campaign’s. unmapped_variableputs its example inhint, and only a reference that works ("header.name": "{{contact.name}}", never{{contact.header.name}}); a position or a copy code gets none.?to=+1…with an unencoded+(read as a space) gets ahintsaying so, with the%2Bform (to,from).- PATCH
variablesmerges in one database write: two PATCHes of different names at the same moment both land.
2026-10-17 — Messaging API: list everything sent, metadata, ids that join
Section titled “2026-10-17 — Messaging API: list everything sent, metadata, ids that join”GET /v1/messageslists everything you sent on a channel:toandcampaignare optional filters now, on SMS and WhatsApp. Both channels come newest first bycreated_at, ties byid. Breaking for SMS: the SMS list was ordered by hand-over time (texts not handed over yet first); it is nowcreated_at, newest first, and a cursor from the old order is refused once with400 invalid_cursor(start again withoutafter). WhatsApp and call cursors keep working.metadataon single sends and calls:POST /v1/messages(both channels) andPOST /v1/callstakemetadata, stored and returned on the message or call. One limit everywhere: string values only, at most 20 keys, keys 1 to 40 characters, values at most 500. Campaignmetadatanow has the same limits (it had none but the value length); past one is400 invalid_requestwithparammetadataormetadata.<key>.{}is the same as nometadata, including for anIdempotency-Keyretry.- Ids that join: every call object carries
short_id(cv-…, the id the dashboard shows;idis unchanged). Campaign recipients carrymessage_id(SMS and WhatsApp: theidGET /v1/messagesanswers) and, on voice,call_short_id.message_idisnullfor WhatsApp messages sent before 2026-10-16 and when the message could not be recorded.
2026-10-16 — Calls: call.* events and an outcome for every call
Section titled “2026-10-16 — Calls: call.* events and an outcome for every call”- Call webhooks:
call.ringing,call.answered, then exactly one ofcall.completed(statuscompleted,voicemail,no_answerorbusy) orcall.failed(a system or carrier failure). Each carriescall_id(the call’sid),conversation_id,direction,from,to,agent_id,campaign_id,statusandoccurred_at;call.answeredaddsanswered_at; the terminal two addanswered_at,outcome,duration_sec,ended_atanddisconnect_reason. A campaign’s calls reach only endpoints that name the types. See Call events. - Every ended call gets an
outcomewithin 5 minutes,unknownwhen there was nothing to classify (a newoutcomeword; inbound calls, which are not classified, read it too). A call that stops reporting is closed asfailed/unknown, normally within 20 minutes, up to 2 hours when the liveness signal is unavailable or the call ran on an older voice agent. After the terminal event a call’sstatusandoutcomenever change. - A call nobody answered (SIP no-answer) or that was declined now reads
no_answer/busyinstead offailed.
2026-10-16 — Messaging API: PATCH merges variables, campaigns name their audience
Section titled “2026-10-16 — Messaging API: PATCH merges variables, campaigns name their audience”PATCH /v1/campaigns/{id}mergesvariables: a name you send is set,"name": nullremoves it, and the rest keep their values (it replaced the whole map). A PATCH that changes the template, text, mission or agent and leaves names with no placeholder is400 unknown_variablewith every such name infields; remove them withnullin the same request. The guide has one table of what each field does on PATCH.- Every campaign answers
audience:{ "type": "list" | "segment" | null, "id" }. The create answer’s import counts moved toaudience.import.refusalis now{ status, error }, the/v1error a start would answer. - Every problem at once: a preview lists every unfilled placeholder on
every channel, and
unknown_variable/unmapped_variablecarryfields. A ready voice preview hassamples(the mission and greeting filled in). - Cursors belong to their list: a cursor of another list (or of another
campaign’s recipients) is
400 invalid_cursor. Earlier cursors, and the raw recipients cursors, are no longer taken: start from the first page. - The shared trial number as the
fromof a text, a WhatsApp message or a campaign is422 trial_sender_not_allowed(was400 invalid_from). - An unknown key inside
audienceis400 unknown_fieldwith a did-you-mean; an id of the wrong kind (sg-…asaudience.list) is400 invalid_requestnaming the prefix. A template name in more than one language withoutlanguageis400 invalid_request(was404). A voice draft may leave outcontent. Calloutcomevalues are listed in Place calls.413carriesrequest_id.
2026-10-16 — Messaging API: lists, segments, cursors and stricter previews
Section titled “2026-10-16 — Messaging API: lists, segments, cursors and stricter previews”GET /v1/listsandGET /v1/segmentslist thecl-…andsg-…ids a campaign’saudiencetakes.GET /v1/agentsandGET /v1/templatesnow page withlimit(default 50) andafter, like every other list; agents carryuuidand senderschannel_uuids, the ids events use. Templates carryvariable_keys, the keys a campaign’svariablestakes.- An SMS preview goes stale on any edit, its name included, as WhatsApp and
voice previews do. SMS preview ids are now
snap_…. - Starting a finished campaign is
409 campaign_completed(orcampaign_canceled), before any preview check; previewing a campaign that is not a draft or paused is409 campaign_not_previewable. - A preview reports every problem it finds before counting (
error.also). retry.onno longer takesbusy(it was dropped silently: a busy line counts asno_answer).content.voicemail_dropis400 voicemail_drop_unavailable(it was saved but could not play).- Campaigns answer
started_atandcompleted_at;variablesis{}when empty; list items have no channel block. A replayed create answers the first answer word for word; an SMS message’screated_atis the same on create, replay and read. A write with a query parameter is400 unknown_parameter.GET /v1/meaddsuser_phone.
2026-10-15 — The messaging API: texts, WhatsApp, calls and campaigns under /v1
Section titled “2026-10-15 — The messaging API: texts, WhatsApp, calls and campaigns under /v1”- One API for every channel.
POST /v1/messagessends a text ("channel": "sms") or an approved WhatsApp template ("channel": "whatsapp"),POST /v1/callshas an AI agent call a number with amission, andPOST /v1/campaignssends texts, templates or calls to a whole audience.GET /v1/senderslists your numbers with their readiness on each channel, andGET /v1/menames the workspace and key. Bodies are snake_case and strict (400 unknown_field), every object has onestatus, and every refusal is one envelope,{ "error": { "type", "code", "message", "param", … } }. Start with the Quickstart: an AI agent calls your own phone from the shared trial number. - Campaigns share one body and one lifecycle.
contentis the only part that changes by channel."start": "auto"(the default) checks and starts in one request;"start": "draft"lets you preview and then start a preview. SMS previews freeze the audience; WhatsApp and voice previews do not, and starting one after the campaign was edited is409 preview_stale.variablesare template strings such as"{{contact.shoe_size|unknown}}". See Campaigns. - One rule for placeholders on every channel. A
variablesname the content has no placeholder for is400 unknown_variablewith a did-you-mean. A placeholder nothing fills makes the previewstatus: "failed"(unmapped_variable,field), and a start, or a create that starts,422 unmapped_variable: SMS, WhatsApp and voice alike. - Edits check what a create checks.
PATCHrefuses another number’s template (422 template_not_on_sender), an unknown agent or template, and can set a draft’saudience.listoraudience.segment. A start missing something answers422 launch_fields_missingwithmissingin/v1field names. Campaigns returnlimitsandretryas written, and take a WhatsApp template bynameandlanguageas a single send does. - Strict query strings and clearer refusals. An unknown query parameter is
400 unknown_parameter;GET /v1/templatesandGET /v1/agentstakelimit. Did-you-mean hints use edit distance at the field’s own path, validation messages are plain sentences, a campaign contact’s badphoneis400 invalid_numberwith its E.164 form,schedule.tzmust be an IANA zone, and every/v1response (an unknown path included) carriesX-Request-Idand, for errors, the JSON envelope. - WhatsApp statuses are events.
message.status_updatednow fires for WhatsApp messages too (sent,delivered,read,failed), and every payload carrieschanneland the publicstatus. - Campaign events for every channel. WhatsApp and voice campaigns now send
campaign.started,campaign.paused,campaign.resumed,campaign.canceledandcampaign.completed.campaign_typecan bewhatsapp_template,voice_aiorvoicemail_drop, andbilled_creditsisnullfor those. See Delivery & webhooks. - New guides: Send SMS,
Send WhatsApp, Place calls,
Campaigns, Delivery & webhooks
and Errors. The dashboard’s own
/apiroutes are still there for the dashboard, in the Dashboard API reference.
Changes to the dashboard API (/api):
POST /api/wa-templates/{id}/sendanswers402(was422) forworkspace_suspendedandroute_blocked.POST /api/campaignsanswers409 create_in_progress(wasidempotency_conflict) while a create with the sameIdempotency-Keyis still running.POST /api/seax/campaignsis unchanged.POST /api/conversations/click-to-callcalled with an API key defaultssourcetomanual.message.status_updatedpayloads gainchannelandstatus, and a WhatsApp variant (channel: "whatsapp").campaign.*events also fire for WhatsApp and voice campaigns, withcampaign_typewhatsapp_template,voice_aiorvoicemail_drop, andbilled_creditsnullfor those.
2026-10-14 — A single text’s to must be E.164; compliance refusals carry code
Section titled “2026-10-14 — A single text’s to must be E.164; compliance refusals carry code”POST /api/messaging/sendsrefuses atothat is not E.164. A number such as415-555-0100or+1 415 555 0100used to be read in the sender’s country, sent and billed. It is now400 invalid_numberwithparam: "to", and nothing is sent or charged. When the number is clear, thehintsays which E.164 we think you meant (Did you mean +14155550100?). Campaigncontacts[].phonewas already strict; acontactsCsvfile is still read loosely./api/workspaces/{id}/outbound-compliancerefusals carrycode. A stale version is409withcode: "version_mismatch"(erroris unchanged); the other refusals there carryinvalid_request,insufficient_role,forbiddenornot_found.- Send SMS with the API now shows the phone-number
list and billing summary as they answer, how to tell a number you brought
from your own Twilio account (
mode: "byo"),workspaceIdfor the compliance call, one rule for naming the sender, the exactIdempotency-Keycharacters,400 empty_audience_input, the full webhook endpoint object, listing, changing (event_typesreplaces the list) and deleting endpoints, thereplay_truncatedframe and how to start the event stream near the present.
2026-10-13 — WhatsApp messages from your own account cost 0.1 credits
Section titled “2026-10-13 — WhatsApp messages from your own account cost 0.1 credits”- Every WhatsApp message sent from your own WhatsApp Business account now
costs 0.1 credits, the same as a text from your own Twilio account. That
covers template sends, campaign messages, inbox and agent replies, and
replies inside Meta’s free 24-hour window. Meta still bills its own charges
to you directly. The usage line is
whatsapp.byo_platform, shown as “WhatsApp platform fee (your WhatsApp account)”. - A WhatsApp campaign the balance cannot cover does not start. Starting
one (
POST /api/campaigns/{id}/start, orPOST /api/campaignswith a launch) answers402 insufficient_creditswithshortCreditswhen the balance is below 0.1 × recipients. A create that launches also returns thecampaignIdof the draft it wrote: start it again once the balance covers it.
2026-10-11 — Restoring a BYO number’s call routing refuses what it cannot do safely
Section titled “2026-10-11 — Restoring a BYO number’s call routing refuses what it cannot do safely”POST /api/phone-numbers/{id}/voice/restore-previousnow answers with the release’s codes when it cannot give the calls back under the number’s own record:409 release_in_progress(another change to the number’s calls is running; retry in a minute; this replaces409 call_setup_in_progresshere),503 release_not_ready(a passing problem; retry), and503 routing_unrecoverable(what is needed to record the give-back is missing; contact support). When the number’s record says its calls were already given back it answers409 nothing_to_restore, and when it says they were moved elsewhere since,409 call_routing_changed. Nothing is changed on any of them. The body is{ "error", "message" }; showmessageto people.
2026-10-11 — The integration webhook is retired; its events are webhook events
Section titled “2026-10-11 — The integration webhook is retired; its events are webhook events”- Two more webhook and
/v1/eventstypes:meta_response.completed(a backgroundPOST /v1/responsesfinished or failed) andmeta_chat.updated(a chat with Octo got a new reply)."*"endpoints receive them: answer 2xx to anytypeyou do not handle. To receive only these, list them by name. See Octo events. - Their payloads are snake_case now (
response_id,conversation_id,message_count, …), like every other event, and a failed response’s event carries itserror. /api/users/me/integrationsis retired.GET,PUTandPATCHanswer410with{ "error": "integration_webhook_retired", "message", "docs_url" }. That one-URL webhook had no retries or delivery history; add a webhook under Settings → API Keys & Webhooks or withPOST /v1/webhooksinstead. Its two call-session events are not carried over: nothing sends them any more.- If you received
meta_response.completedon the old webhook: add a webhook endpoint subscribed to it (Settings → API Keys & Webhooks, orPOST /v1/webhooks). The body is now the standard event envelope (id,type,workspace_id,created_at,seq,data) instead of the old{ event, occurredAt, payload }, and it is signed like every other webhook (webhook-id,webhook-timestamp,webhook-signature, keyed by the endpoint’s own secret) instead ofx-webhook-signature: sha256=…. See Verify a delivery.
2026-10-10 — Webhooks in the dashboard; change a secret, send a test event
Section titled “2026-10-10 — Webhooks in the dashboard; change a secret, send a test event”- Webhooks are owners and admins only.
/v1/webhooksnow refuses a key whose owner has the Agent role in the workspace with403 insufficient_role, on every route. A webhook receives every event of the workspace, so the same rule applies in the dashboard. GET /v1/eventsis owners and admins only too, for the same reason: the feed carries every event of the workspace, message text included. An Agent-role key gets403 insufficient_role.POST /v1/webhooks/{id}/rotate-secretreturns a new signing secret, once. The old one stops working at once; deliveries after the change, retries included, are signed with the new one.POST /v1/webhooks/{id}/testsends one samplewebhook.testevent and returns what your server answered (status,duration_ms, the start of the reply). Nothing is stored or counted; 10 per workspace per minute. See Send a test event.- A webhook
urlis at most 2,048 characters. - Manage webhooks without the API under Settings → API Keys & Webhooks (the page was called API Keys): add, pause, turn back on, change the secret, test, and read the last 7 days of deliveries.
- A lost campaign event is now written later. If a
campaign.*status event could not be written when the campaign changed, it is written within about 15 minutes, once. Its*_attime is when it was written, not when the campaign changed, and a latecampaign.resumedhasprevious_reason: null.campaign.waitingandcampaign.window_openedare not repaired;GET /api/campaigns/{id}stays the source of truth.
2026-10-10 — Campaign events for sending hours
Section titled “2026-10-10 — Campaign events for sending hours”- Two more webhook and
/v1/eventstypes:campaign.waiting(an SMS campaign reached the end of its sending hours, or started outside them, and waits;resumes_atsays when its hours next start andtzthe time zone they are set in) andcampaign.window_opened(its hours started again and it goes on sending). Waiting is not a pause: the campaign’sstatusstaysqueuedorrunning. A pause or cancel ends the wait: the campaign sends its own event and that wait never gets acampaign.window_opened; resumed while its hours are still closed, it sends a newcampaign.waiting. Both events can be up to about a minute after the hours end or start. See Campaign events. "*"endpoints receive them too, like the othercampaign.*events. Answer 2xx to anytypeyou do not handle.
2026-10-10 — Campaign events: know when an SMS campaign stops
Section titled “2026-10-10 — Campaign events: know when an SMS campaign stops”- Five new webhook and
/v1/eventstypes:campaign.started,campaign.paused,campaign.resumed,campaign.completedandcampaign.canceled, one per status change of an SMS campaign. A pause says why (reason: out of credits, its credit limit, the carrier’s daily limit, too many failures, the carrier refusing the number, a needed review, and so on) and what to do (message).campaign.completedcarries the final totals and the credits charged. List them by name to receive only these. See Campaign events. - Existing
"*"endpoints start receiving them. An endpoint subscribed to everything gets these five types from now on: ignore (and answer 2xx to) anytypeyou do not handle, or a receiver that rejects unknown types will have its deliveries given up and, after 20 in a row, be disabled.
2026-10-10 — Importing your own Twilio number asks before taking its calls
Section titled “2026-10-10 — Importing your own Twilio number asks before taking its calls”POST /api/phone-numbers/byocan answer409 call_routing_choice_needed. When the number’s incoming calls already go somewhere (a voice URL, a TwiML app or another SIP trunk), the import no longer moves them to Omni Octo unasked. The 409 names where they go (currentVoiceRouting; the full URL for owners and admins only) and suggestsdefaultCallRouting: "texts_only". SendcallRouting: "texts_only"(texts here, calls left alone) or"texts_and_calls"(owners and admins; the current call setup is saved first and can be restored). Before, such an import answered 201. A number whose calls go nowhere imports as before. See the Dashboard API reference.- Other new answers on that route:
403 calls_takeover_needs_admin,409 call_routing_changed(the setup changed in Twilio during the import; read it again),409 call_setup_in_progress,503 call_routing_recovery_failed. - A texts-only number cannot place calls. Calls and voice campaigns from it
are refused with
calls_not_routed. - Releasing an imported number gives back its call setup as well as its SMS
webhook (
callRouting.outcomein the DELETE response).
2026-10-10 — SMS docs match the shipped behaviour; spend.recorded in credits
Section titled “2026-10-10 — SMS docs match the shipped behaviour; spend.recorded in credits”spend.recordedfields are now in credits.amount_usdis nowamount_creditsandbalance_usdis nowbalance_credits(numbers, in workspace credits). Nothing emits this event yet; update a subscriber before it does.- SMS prices and reports are credits only. No SMS response carries a
dollar amount. A number brought from your own Twilio account is billed by
Twilio separately (
byoCarrier: truemarks it). GET /api/campaigns/{id}/records(SMS). Newsort,dirand sealedafterpaging (400 invalid_cursor; de-duplicate byid), plus each row’s latest reply (replyText,lastReplyAt) andclicks.- Clicks. Scanner and preview fetches do not count, and repeat visits within 30 minutes are one click.
not_receivingnumbers (incoming texts diverted to a Twilio Messaging Service) cannot send:422 sender_not_clearedfrom the API (senderStatuson a single send,statuson a campaign start). Octo’s single-text tool answerssender_not_receiving; a workflowsms.sendstep isskippedwith that reason.- Scheduling.
schedule.startAtas a day or an instant with offset; Octo and MCP tools takeschedule.startLocalplus an IANAtzand refuse a past start.
2026-10-03 — MCP tools for Dialog Insight batch runs
Section titled “2026-10-03 — MCP tools for Dialog Insight batch runs”- New MCP tool:
dialog_insight.analyze_batch. Runs a Dialog Insight over many existing conversations in the background. Passdialog_insight_idand exactly one ofconversation_ids(up to 1,000),campaign_id(voice campaigns only) oragent_id(optionally narrowed withactivity_since, an ISO 8601 instant, to conversations active since then; refused with the other selectors). Every selector takes only conversations that have ended: closed, a finished call, or a chat with no activity for 30 minutes, so a chat that went quiet counts even if it is still open. A live one (still in a call, or a chat with activity in the last 30 minutes) is skipped and analysed automatically when it ends; live ids inconversation_idsare counted inexcluded_live.rerun_analyzed(defaultfalse) re-runs conversations the insight already analysed; a re-run can also remove labels that no longer apply.trigger_workflows(defaultfalse) fireson_labelworkflows for the labels the job adds or removes. Withfalsethe labels change silently: neitheron: appliednoron: removedworkflows fire, then or later. It costs LLM credits per conversation and isspends_money, so over/mcp/toolsit answersconfirmation_required, over/mcpit waits for a human YES, and the stateless/v1API does not offer it. The confirmation states how many ended conversations the selector reaches; if that number grows by more than 10% before the human confirms, the confirmation is refused and asked again. A request with zero or several selectors, oractivity_sincewithoutagent_id, is refused before it is parked. While a job for the same insight and agent (or campaign), or an Octo job for the same insight over exactly the same conversation ids, is queued or running, it creates nothing and returnscode: "batch_already_open"withexisting_batch: { id, short_id }. dialog_insight.analyze_batchreports what the job will actually do. The result’sbatch.statusis the job’s real status:queued, orcompletedwhen this insight had already analysed every selected conversation (nothing to run, nothing billed, no notification).batchalso carriesalready_analyzed(skipped at creation, not billed) andwill_analyze(item_count − already_analyzed, an upper bound: a conversation with no transcript is skipped too). A queued job started by a signed-in user adds anotificationline: the starter gets an in-app notification when it finishes; jobs started by an API key with no user get none. Past the workspace’s open-jobs limit it answerscode: "too_many_open_batches"withopen_batches,limitand anext_step; nothing is created, and retrying before a job finishes is refused the same way.dry_run: trueondialog_insight.analyze_batchcreates nothing and costs nothing. It returnsended,already_analyzed,will_analyzeandlivefor the selector, and refuses an unknown insight or agent and a missing or non-voice campaign like the real run. A dry run issafe, so over/mcp/toolsand/mcpit runs without a confirmation (/v1still does not offer the tool, whose static class isspends_money). Any other value ofdry_runis the real, billed run.- The confirmation is more precise. It now says “up to K will be analysed
(N ended; M already analysed by this insight are skipped)”. The drift check
still compares N. A
campaign_idthat is missing or not a voice campaign is refused before anything is parked. dialog_insight.batch_statusaddsskipped_by_reason(already_analyzed,no_transcript,not_ended). An attach-time job is labelled as the analysis of the last 48 hours or of all past conversations. When operators have paused batch analysis platform-wide, the result haspaused: trueand apaused_note: open jobs wait in the queue and resume on their own.- New MCP tool:
dialog_insight.batch_status(safe). Takes a job id ordib-…short id and returns the job (includingorigin:api,octoorattach_dialog, andactivity_since), counts by item state (skippedincludes conversations that were live again when their turn came, reasonnot_ended), and the first 50 failed conversations with theirreason_code; withoutbatch_idit lists the 10 most recent jobs with their insight’s name. There is no MCP tool to cancel a job: a started job runs to the end, and the workspace’s monthly LLM budget (llm.check_budget/llm.set_budget) stops it with statusstopped_over_budgetif spend runs out.
2026-10-03 — Analyse an agent’s past conversations with a Dialog Insight
Section titled “2026-10-03 — Analyse an agent’s past conversations with a Dialog Insight”-
A workspace can have at most 5 batch jobs queued or running at once. A request past it —
POST /api/dialog-insight-batches, orPATCH /api/agents/{id}withdialogInsightScope: "analyze_history"or"all_history"— answers429withcode: "too_many_open_batches",openBatchesandlimit, and the message “This workspace already has N analysis jobs queued or running. Try again when they finish.” Nothing is created, and an attach attaches nothing. Retry once a job finishes. A request whose conversations this insight has all analysed already (a job createdcompleted) is never refused. Same-insight duplicates still answer409batch_already_openfirst. ThePATCH /api/agents/{id}refusal carriesopenBatchesandlimittoo, like thePOST. -
The history attach’s
dialogInsightBatchaddsstatus:"queued", or"completed"when every conversation was already analysed (nothing to run). It is now{ id, shortId, status, itemCount, alreadyAnalyzed }. -
New failed-item reason
llm_throttled_exhausted(in a job’s failed items andGET /api/dialog-insight-batches/{id}): the AI provider was still rate limiting or overloaded on the conversation’s 30th try, about 50 minutes of continuous refusals. Run the conversation again in a later job. Until then a busy provider only slows a job down; it never fails a conversation or switches models. -
New value for
dialogInsightScopeonPATCH /api/agents/{id}:all_history. Attaches the insight (new conversations are analysed from now on, likefrom_now_on) and, in the same request, starts a background job that analyses every past conversation of the agent. It costs LLM credits. The response addsdialogInsightBatch: { id, shortId, itemCount }, ornullwhen the agent has no past conversation. Attaching and starting the job happen together: if either fails, neither happens. Only conversations that have ended (as below) are included; a live one is analysed automatically when it ends. While that job is queued or running, anotherall_historyattach of the same insight to the same agent is a409(code: "batch_already_open"). -
dialogInsightScope: "analyze_history"is a background job now, likeall_history. It attaches the insight (automatic analysis from now on) and, in the same request, starts a job over the agent’s conversations that ended with activity in the last 48 hours. The response adds the samedialogInsightBatchfield. Before, it moved the attachment’s start time back 48 hours and left the catch-up to the fallback sweep, which always fired workflows. No scope back-dates the attachment any more: every scope starts automatic analysis at the moment of the attach. While either history job is queued or running, another history attach of the same insight to the same agent is a409(batch_already_open). -
New optional field
triggerWorkflows(boolean, defaultfalse), withdialogInsightScope: "analyze_history"or"all_history".truefireson_labelworkflows for the labels the job writes; labels are written either way. Sent withfrom_now_onor with no scope it is a400withcode: "trigger_workflows_without_history_scope". -
POST /api/dialog-insight-batchesacceptsactivitySince(ISO 8601, withagentIdonly): the job takes only the agent’s conversations whose last activity is at or after that instant. WithconversationIdsorcampaignIdit is a400(activity_since_requires_agent). Jobs report it back asactivitySince(nullwhen unset). -
GET /api/agents/{id}/dialog-insight/backfill-estimatetakes an optionaldialogInsightIdquery parameter (UUID ordi-…) and returns three new fields.allHistory(whatall_historywould select: every ended conversation) andlast48h(whatanalyze_historywould select: ended, with activity in the last 48 hours) are each{ ended, alreadyAnalyzed, willAnalyze }, wherealreadyAnalyzedcounts conversations with an up-to-date result from that insight, which the job skips. Both arenullwithoutdialogInsightId.livecounts conversations that have not ended. An unknowndialogInsightIdis a404(dialog_insight_not_found). The existing fields are unchanged but deprecated: they describe what the automatic paths could reach, which no longer predicts either option. -
dialogInsightScopenow acceptsfrom_now_on,analyze_historyandall_history; any other value is a400naming the three. -
POST /api/conversations/{id}/analyzeacceptstriggerWorkflows(boolean, defaultfalse). Re-analysing a conversation no longer fireson_labelworkflows (alert emails and the like) unless you sendtrue; labels are written either way. Before, every manual analysis fired them. Theforcefield is deprecated and ignored: the endpoint always re-runs. -
Batch jobs (
POST /api/dialog-insight-batches) take only conversations that have ended, for every selector. Ended means closed, a finished call, or 30 minutes with no activity — the same moment automatic analysis treats a chat as over, so a WhatsApp or webchat conversation that went quiet is included even though it is still open. AconversationIdsentry that has not ended gets no item; the create response’s newexcludedLivefield counts them (always0forcampaignId/agentId), and when every id is live the response is the zero-items200. A conversation the customer writes to again before its turn comes is skipped withreasonCode: "not_ended". Each is analysed automatically when it ends. Ids that are not in the workspace still fail withconversations_not_found. -
A batch job skips, at creation, the conversations its insight has already analysed (an up-to-date result from that insight; nothing is skipped with
rerunAnalyzed: true). They become items withstate: "skipped"andreasonCode: "already_analyzed"straight away, so a job’s progress counts only what it will actually run, the same number the attach estimate’swillAnalyzegives.POST /api/dialog-insight-batches(202) and the attach response’sdialogInsightBatchaddalreadyAnalyzed;itemCountis still every conversation the job took. When every one was already analysed the job is created withstatus: "completed". -
GET /api/dialog-insight-batches/{id}and the list’s items addskippedByReason, skipped items by reason code, e.g.{ "already_analyzed": 3, "no_transcript": 1, "not_ended": 0 }. Those three keys are always present; any other reason appears when an item has it. -
GET /api/dialog-insight-batchestakesagentId(UUID orag-…) andstatus=open(queued or running), separately or together, to find a job still running for an agent. AnagentIdthat is not in the workspace is a404(agent_not_found). These filters are for API clients; the dashboard does not poll jobs. -
When a batch job ends, the person who started it gets an in-app notification (no email): kind
dialog_insight.batch_completedinGET /api/notificationsand on the workspace event stream (notification.new). It is sent once per job, when the job finishes (status: "completed") or stops because the workspace reached its LLM budget ("stopped_over_budget"). It is not sent for a cancelled job, for a job created alreadycompleted(everything had been analysed), or for a job started with an API key that has no user behind it. The payload is snake_case:batch_id,batch_short_id,status,dialog_insight_id,dialog_insight_name,agent_id/agent_short_id/agent_nameandcampaign_id/campaign_short_id/campaign_name(eachnullunless the job was for that agent or campaign),selector,origin,analysed,skipped_other,already_analyzed,failed,item_count, andurl, a dashboard path: the LLM budget settings for a stopped job, otherwise the conversations list (filtered to the agent for an agent job). -
A result from an insight that is not the conversation’s own is stored in the conversation’s Dialog Insight result only. A batch job, or
POST /api/conversations/{id}/analyzewith adialogInsightId, that runs a different insight from the one the conversation resolves to (campaign insight → campaign’s own extraction fields → agent default) no longer writesextractedDataon the call or the campaign record, so a campaign’s records and CSV export keep only its own fields. Running the conversation’s own insight, and correcting its result, update them as before.
2026-09-30 — WhatsApp Business App numbers, and handing a conversation back to the AI
Section titled “2026-09-30 — WhatsApp Business App numbers, and handing a conversation back to the AI”A WhatsApp number can now stay live in the owner’s WhatsApp Business app on their phone while it is connected here (Meta’s “coexistence”). If your integration creates channels, sends messages or reads conversations, these are the changes you will see.
- New MCP tool:
return_conversation_to_ai. Hands a paused (handed_off) conversation back to the AI — what the inbox’s Return to AI now button does. Nothing is sent to the customer; the AI answers their next message. It ismutates_live_deployment, so over/mcp/toolsit answersconfirmation_required, and over/mcpand/v1it waits for a human YES. It accepts only a conversation that ishanded_off, and returnsprevious_handoff_reason/previous_handoff_cause. get_conversation(MCP) tells you who wrote each message and why the AI paused. Each message carriessender(customer,ai,human_agent,business_app,system) andsource(live,business_app_reply,imported); a message the customer never received addsdelivery_statusandsent_to_customer: false. Ahanded_offconversation addshandoff_reason,handoff_cause(business_app,team,ai,workflow,unknown),handed_off_at, andauto_return_atwhen the channel returns conversations to the AI automatically.is_business_appon channel and conversation tools.whatsapp.get_status,list_configured_channels,get_channel,get_conversationandget_conversationsmark a WhatsApp Business App number withis_business_app: true.- Return to AI automatically, per channel. Set
config.autoReturnToAi = { enabled, minutes }(minutes10–60) onPOST/PATCH /api/channels, orauto_return_to_aion theupdate_channeltool: a handed-off conversation goes back to the AI after that many minutes without a reply from your team. A new WhatsApp Business App channel starts with it on at 15 minutes. PATCH /api/channels/{id}mergesconfigon a WhatsApp Business App channel: the keys you send are set and every other key is kept.- New
409codes onPOST /api/channelsandPATCH /api/channels/{id}, with a{ error, message }body:whatsapp_number_on_business_app(the number is in use on the WhatsApp Business app — connect it from the WhatsApp channel page in the dashboard instead), and on a Business App channelcoexistence_reconnect_required/coexistence_number_change(its credentials change only by reconnecting through Connect my WhatsApp Business App). - New refusal
number_disconnected(422) onPOST /api/wa-templates/{id}/sendandPOST /api/conversations/{id}/messages: the WhatsApp Business App number was disconnected from the app. Nothing was sent to Meta; reconnect it from Channels and send again. If you branch oncode, add this value.
2026-09-25 — Attaching a Dialog Insight no longer reaches into an agent’s history
Section titled “2026-09-25 — Attaching a Dialog Insight no longer reaches into an agent’s history”If you attach Dialog Insights through the API or through MCP, the first bullet changes behaviour on a call you are already making.
- An attachment now covers new conversations only. Pointing
defaultDialogInsightIdat an insight —PATCH /api/agents/{id}, or thedialog_insight.attachtool — used to leave every conversation the agent already had in scope, and the automatic paths then analysed them as each went idle. One attach produced ~920 analyses of pre-existing conversations, and the labels they applied fired workflows. An attach that names no scope now covers only conversations that start after it. Agents that already have an insight attached are untouched: they keep today’s behaviour until somebody re-attaches. - New optional field on
PATCH /api/agents/{id}:dialogInsightScope.from_now_on(the default when the field is absent) oranalyze_history, which reaches back 48 hours — the same activity window the fallback sweep uses — and no further. It must ride withdefaultDialogInsightId; on its own it is refused with400 dialog_insight_scope_without_pointer, and any other value is a400naming the two that are accepted. Detaching (null) clears the scope, so a later re-attach is a fresh “from now on”. The stored timestamp is derived, never client-writable. - New endpoint:
GET /api/agents/{id}/dialog-insight/backfill-estimate. Read-only, nothing attached and nothing billed: six integers telling you what ananalyze_historyattach would do —totalConversations,activeLast48h,backfillReachable,reachableStillOpen,reachableRecentlyClosed,reachableOlderThan48h.backfillReachable - reachableOlderThan48his the number that matters, because that scope floors at the same 48-hour window;backfillReachablealone is the age-blind upper bound and can overstate it badly (180 against 0 on a real agent). It is an upper bound in a second way too: a conversation with no transcript is skipped before any model call. Listed in the Platform API reference. - A conversation older than the attachment is skipped, not failed. The
recorded reason says so in plain language, and it is only the automatic paths
that stop:
POST /api/conversations/{id}/analyzeanddialog_insight.testboth analyse any conversation on demand. A campaign-level insight is never scoped this way — a campaign is a finite batch whose own calls are what its extraction columns are for.
2026-09-25 — /mcp now waits for a human YES before a risky action
Section titled “2026-09-25 — /mcp now waits for a human YES before a risky action”send_messageon/mcpno longer runs risky actions on the first call. These docs already said the delegate pauses for a human YES before anything that spends money. It did not: a job that needed a spend, mass-message, destructive or live-deployment action ran it immediately. Now it parks, as/v1always has. The result carriespending_action: { action_code, summary, how_to_confirm }. Nothing is done until you callsend_messageagain on the sameconversation_idwithYES <code>(orNO <code>). That second call returnsexecuted: { ok, summary }. See MCP overview.- What to change: if a script drives
/mcpunattended and relied on risky actions going through, it now gets apending_actionback and has to ask a person./mcp/toolsis unchanged: it still refuses those tools withconfirmation_required.
2026-09-23 — Campaign errors you can branch on, and two status codes that moved
Section titled “2026-09-23 — Campaign errors you can branch on, and two status codes that moved”If you integrate against /api/campaigns, four of these change behaviour. The
first two are the ones worth reading before your next release.
- A
typeorstatuswe do not recognise is a400, not a500. OnGET /api/campaignsboth went straight into a database enum comparison, so a spelling we do not have raised a cast error and surfaced as a server fault. The body now names the problem:error: "invalid_filter_value"plusparameter,receivedandaccepted[]. The same fix is onGET /api/campaigns/{id}/records, whosestatushad the identical hole.dispositionon that endpoint is still unvalidated on purpose: it is free text written by the call classifier, so an unrecognised value is an empty result rather than a mistake. An explicitly empty?type=still means “no filter”, unchanged. - Cancel, pause and resume answer
409when the campaign is in the wrong state. All three used to answer404, indistinguishable from a campaign that does not exist, so a caller firing them from a webhook or a timer could not tell a wrong id from a race it lost. Now404means fix your id, and409 campaign_not_cancellable/campaign_not_pausable/campaign_not_resumablemeans stop asking. The body carries thestatusthe campaign is actually in and the set the verb accepts. For cancel, a409is safe to treat as success: the campaign is over either way. - The refusals that block a launch carry a stable code.
launch_fields_missing,name_required,agent_disabled,too_many_recipientsandmax_concurrent_calls_too_highreplace English sentences that used to sit inerror. They are returned byPOST /api/campaigns,POST /api/seax/campaignsandPOST /api/campaigns/{id}/start. Readlaunch_fields_missing.missing[]: a voice campaign will not dial withoutagentId,config.callMissionandconfig.phoneMenuChosen, which are optional when saving a draft, required to place a call, and documented nowhere else. On the SeaX endpoint those names arrive in SeaX’s own spelling. GET /api/campaigns/{id}no longer returnsrecipientsfor a launched campaign. That field is the composer’s working state, not a recipient list. For a head-count usestats.total, which is on the list response too; for per-person outcomes useGET /api/campaigns/{id}/records. A campaign still indraftorpausedreturns it in full, and?include=recipientsasks for it on a launched one.providerCallIdon/resultsis alwaysnullfor a voice campaign. This is not new, but the field’s description implied otherwise. Key your records oncallId, which is stable and unique per attempt.
2026-09-22 — Send an approved WhatsApp template from your own system
Section titled “2026-09-22 — Send an approved WhatsApp template from your own system”- New endpoint:
POST /api/wa-templates/{id}/send. Sends one approved template to one E.164 number with a workspace API key, for “when X happens on our side, send template Y to this customer”.{id}is the template’swt-…short id or its UUID. The body istoplus Meta’s owncomponentsarray, verbatim, so a script that already speaks the Cloud API can pass its payload through. Listed in the Platform API reference. - Meta’s 24-hour window does not apply to templates. The message lands in the number’s existing conversation (the thread the customer’s reply joins), reopening it if it was closed, or starts one.
componentsare checked against the template before anything is written. A missing, extra or unknown variable, a header of the wrong kind, or a button parameter on the wrong slot is refused with422 template_components_mismatchand anerrorthat names the mismatch. Supported buttons areurl,copy_code(Meta’scoupon_codeshape, up to 15 characters) andquick_reply(with apayload), each at itsindexin the template.- Refusals carry a stable
code:402 billing_not_eligible,404 template_not_found,409 blocked_dnc,422 template_not_approved/channel_unavailable/channel_pending_verification/workspace_suspended/route_blocked,502 send_failed. When a message row was written, the body also carriesmessageId,conversationIdandconversationShortId. - Not idempotent. Every successful call sends and bills one template. After a timeout, read the thread before retrying.
POST /api/conversations/{id}/messagesacceptstemplate({ templateId, components }) instead ofcontent/mediaUrlon a WhatsApp thread, with the same checks. It cannot be combined with text or an attachment.send_whatsapp_template(MCP) goes through the same send path, so its sends now pass the same billing check and land in the same thread as the endpoint’s. A missing variable is refused before the send instead of reaching Meta as an empty string.
2026-09-12 — Send an Instagram DM from Octo, inside the 24-hour window
Section titled “2026-09-12 — Send an Instagram DM from Octo, inside the 24-hour window”- Two new MCP tools for manual Instagram replies.
can_send_instagram_message(safe) answers whether a specific DM can go to a specific person right now: it checks that the thread exists, that they have not opted out, that Instagram’s 24-hour messaging window is still open, and that the text fits, then returns how long the window has left, who will answer a reply, and a signedsend_token. It cannot send.send_instagram_message(mass_message, so it pauses for a human YES on/mcpand/v1) sends, and requires that token. - The token is the point. It binds the workspace, the user, the conversation and a hash of the message text, so the DM that goes out is the one the operator was shown. Change a word and the send refuses rather than delivering something nobody approved.
- Refusals say what is actually wrong, with a stable code you can branch on:
window_closed(with the time of their last message),opted_out,no_instagram_thread(Instagram does not let a business start a conversation),ambiguous_thread,text_too_long,manual_send_cap_reached. Meta’s own error text is never returned; the numeric codes are. comment_automation.list_leadsnow returnsconversation_idper lead, so a reachable lead can be handed straight to the two tools above.- Instagram’s 24-hour window opens on the customer’s last message and nothing you send reopens it. These tools will not pretend otherwise.
2026-09-08 — Trial phone numbers, and a catalog that describes itself
Section titled “2026-09-08 — Trial phone numbers, and a catalog that describes itself”- Three new MCP tools for the shared trial number. Omni Octo lends a pooled
phone number so a workspace can place outbound calls without buying one.
trial.get_number(safe) reports whether the number is available to this workspace, which agent is dialling from it, and the only countries it can reach.trial.bind_agentandtrial.release_number(mutates_live_deployment) point an agent at it and stop it again. - Check
trial.get_numberbefore concluding a workspace has no phone number.list_phone_numbersreturns only numbers the workspace owns, so it comes back empty for a trial workspace that nonetheless has a free number waiting. An empty list is not the same answer as “cannot call”. start_voice_callaccepts apfn-…id. That is the trial number’s identifier, returned bytrial.get_number. It only dials once an agent is bound to it, and only to that number’s allowed countries — a+1destination outside them (Jamaica, the Dominican Republic) is still refused.- The number is shared and outbound-only. The person called sees Omni Octo’s
caller ID rather than the workspace’s, and nobody can call it back and reach
the agent. Using it requires a card on file; the card is stored, not
charged. It does not raise the trial call allowance — see
trial.check_call_quota— which only a subscription lifts. - One agent per workspace, pool-wide. Binding a second agent revokes the first, so the previous agent stops dialling from the number.
- The bind and release tools are
mutates_live_deployment, so they are refused over/mcp/toolslike every other gated class, and are available through/mcpor/v1where Octo can park them for confirmation. They also require anowneroradminkey: a deployed agent must not repoint a workspace’s outbound caller ID. - Fixed: the tool catalog’s “What it does” column was empty for all 225
tools. The generator read the tool module’s own
descriptionrather than thedescriptionon its exporteddefinition, so every row rendered—. Every tool on the catalog page now carries its first sentence.
2026-08-01 — Reassigning who answers a channel
Section titled “2026-08-01 — Reassigning who answers a channel”- MCP:
deploy_agentacceptstake_over_inbound. A channel has at most one agent that answers inbound traffic, while many can share it to send or dial out. Previouslydeploy_agentcould only claim that inbound seat when it was empty: deploying to a channel another agent already answers returned outbound-only, and there was no parameter that changed the outcome. Passtake_over_inbound: trueto move the seat to the agent you are deploying. The displaced agent stays deployed and keeps its outbound capability — only who answers changes — and the response names it inreplacedAgentId. - The default is unchanged and stays
false. Deploying to an occupied channel still grants outbound only, so a routine deploy can never silently redirect live inbound traffic away from a working agent. Requireschannel_id; the legacychanneltype parameter does not identify a channel row to own and is rejected. configdoes not control routing. It is opaque per-deployment JSON.config: { inbound: true }and similar were accepted and ignored; usetake_over_inboundinstead.
2026-07-28 — Outbound compliance + AI disclosure
Section titled “2026-07-28 — Outbound compliance + AI disclosure”- Launching an outbound campaign can now return
409. Before a workspace launches its first outbound campaign of any type (voice, SMS, or WhatsApp), an owner or admin must accept the Outbound Calling Compliance acknowledgment.POST /api/campaigns/(non-draft) andPOST /api/campaigns/{id}/startreturn{ "error": "outbound_compliance_required", "version": "…" }until they do. Handle it by surfacing the message rather than retrying — acceptance is a human action at Settings → Compliance → Outbound compliance, and there is deliberately no API or tool that performs it. Drafts are never gated, sodraft: truekeeps working; only the launch is blocked. - Voice agents can speak an AI + call-recording disclosure. A per-agent notice delivered before anything else on the call, either synthesized from editable text or played from an uploaded recording (which is the only way to guarantee verbatim wording on realtime voice pipelines). Agents created from now on default to on; agents that existed before this shipped default to off and opt in, so no deployed agent changes what it says to callers without its operator doing it.
- MCP:
get_agentnow returns adisclosureblock —enabled,applies,spoken_text,mode,using_default_text, and a plain-languagenote. Read it instead of the raw columns: a nulldisclosure_textmeans “use the platform default”, not “say nothing”, andapplies: falsedistinguishes a text-only agent from a voice agent with the safeguard switched off. - MCP:
update_agentacceptsdisclosureEnabled,disclosureText, anddisclosureRecordingId. Turning the disclosure off is supported — it is opt-out by design — but you remain responsible for AI-disclosure and recording-consent law wherever you call. A recording id must belong to your workspace and be of kinddisclosure; voicemail-drop clips are a separate kind and are rejected.
2026-07-15 — Bulk operations
Section titled “2026-07-15 — Bulk operations”- Delete or modify many resources in one call. New workspace tools —
delete_conversations,delete_channels,delete_contacts,delete_campaigns,undeploy_agents, andcancel_campaigns— act on many matching rows at once (callable over the MCP servers and the OpenAI-compatible API). Each takes either afilteror an explicitidslist. This replaces the old behavior where “clean up all matching X” fanned out into one risky call — and one confirmation — per item. - One confirmation for the whole batch. A bulk delete parks a single confirmation stating the count and a sample, not N sequential asks. See Confirmations.
- Guardrails. A full-workspace
{ all: true }filter requires a matchingconfirm_count; a confirmation is drift-aborted if the matching set grows10% before you reply;
delete_campaignsskips running campaigns unless you opt in;delete_contactsis a recoverable soft delete.
2026-07-15 — Events & webhooks
Section titled “2026-07-15 — Events & webhooks”- Subscribe to workspace events. Agents can now listen for state changes
they didn’t initiate. Register signed, retried outbound webhooks with
/v1/webhooks, or hold open aGET /v1/eventsSSE stream with a resume cursor. Both carry the same snake_case event envelope. - Event catalog.
conversation.created,turn.completed,confirmation.requested,confirmation.resolved,spend.recorded,run.completed. See Events & webhooks. - Signed & replayable. Deliveries use the Standard Webhooks (svix)
signature scheme; the SSE feed replays a durable, cursor-paginated log via
?after=/Last-Event-IDso a reconnecting client never misses an event.
2026-07-15 — Phone number provisioning
Section titled “2026-07-15 — Phone number provisioning”- Search & buy phone numbers. Octo can now search available phone numbers
and purchase them directly — the tools are in the workspace catalog (so
they’re callable over the MCP servers and the
OpenAI-compatible API) and the routes are in the
Platform API reference under
/api/phone-numbers. - Spend-gated purchase. Buying a number is a spend action, so it parks for
explicit confirmation like every other risky action — the purchase only fires
after you reply
YES <code>. See Confirmations. - Credits, not dollars. Availability results and prices are quoted in workspace credits; the underlying USD cost is never exposed on any customer surface.
- Regulatory routing. Only US and CA are instant-purchase. For other countries Octo links you to the in-app regulatory-approval flow rather than an external carrier dashboard.
2026-07-14 — Agent-friendliness pass
Section titled “2026-07-14 — Agent-friendliness pass”- Rate-limit headers on every response. The OpenAI-compatible
/v1API now returnsX-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reseton success as well as on429, so an agent can pace itself proactively instead of only reacting toRetry-After. See Rate limits. - Self-correcting error envelope. Errors keep the OpenAI shape
(
message,type,param,code) and now addhintanddocs_urlon the auth, rate-limit, and server-error paths so a caller can recover without a human. See Errors. - Skills catalog. A machine-readable task catalog is published at
/.well-known/skills/index.json— each entry links to markdown instructions for a common job (authenticate, create a key, handle confirmations, respect rate limits, connect MCP, …). See Agent resources.
2026-07 — Developer portal
Section titled “2026-07 — Developer portal”- Docs site. Launched docs.omniocto.com covering
the three Octo surfaces: the OpenAI-compatible
/v1API, the MCP servers, and the platform REST API, with interactive API references. - Agent-readable docs. Added
/llms.txt,/llms-full.txt, a raw-markdown view for every page (append.mdto any URL), and OpenAPI JSON for both APIs. - Docs-search MCP. A live MCP server (
/mcp-docs) withsearch_docsandget_docso an agent can query these docs in context. - OpenAI-compatible API.
/v1/responses,/v1/chat/completions, and/v1/models, callable by pointing any OpenAI client at the Octobase_urland ansk-octo_key. SSE streaming, background mode,Idempotency-Keyreplay, andprevious_response_idcontinuation. - API keys in the dashboard. Create and revoke
sk-octo_keys from Settings → API Keys. See Get an API key.