Skip to content

Changelog

Download .md

Notable, developer-facing changes to the Omni Octo API, MCP servers, and these docs. Newest first. Dates are UTC.

  • tool_choice: "none" on POST /v1/responses runs 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.effort on POST /v1/responses: low, medium or high for one turn, or none to 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_expired and 409 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 the content.mission it started with, so a new mission goes in the next round (a clone). Edit a campaign says the same: call_settings edits reach a resumed voice campaign’s calls, a new content.mission does 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 402 refusals and the usage report. Retries, idempotency and rate limits brings together Idempotency-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_url for 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 %2B rule. 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 by status and by call outcome (both sum to recipients), how many were reached, how many a next round would call back (unreached), calls by the agent prompt_version they ran on, and per Dialog Insight field what the calls extracted (counts per value for yes/no and pick-one fields; min, max, mean and 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, and unreached never anyone a round reached; the campaign must have finished (a draft may be copied as same), 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 field PATCH takes is an override. start: "draft" or "auto". Send an Idempotency-Key: a retry answers the campaign the first request made.
  • cloned_from ({ id, audience, round }, or null) on every campaign read.
  • MCP (/mcp/api): get_campaign_results, and clone_campaign (needs an idempotency_key; with start: "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 the mission and variables a voice campaign would give it. The first answer carries the agent’s opening; send its session_id with each next message. 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 the prompt_version it started on (409 agent_changed after an edit) and is forgotten an hour after its last request. Idempotency-Key makes a retried turn free. not_rehearsed names 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 English label), its calls or messages, and an AI-call campaign’s spending limit (spend_cap). MCP tool get_campaign_usage. See Reading a campaign.
  • billed_credits on every call (GET /v1/calls, GET /v1/calls/{id}, get_call): what the call cost so far. null for an outbound call that ended before 2026-10-30.
  • limits.spend_cap_credits on 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 with paused_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_person for a key, which can still lower it), and a resume that would pause again at once is 409 spend_cap_reached. Both carry dashboard_url. A voice preview adds spend_cap: { cap, calls_covered }. See Limits and retries.
  • A WhatsApp or voice campaign the platform pauses itself (out of credits, or its spend cap) now sends campaign.paused with that reason, as SMS campaigns did.
  • field_not_for_channel no longer applies to limits.spend_cap_credits on 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, enum with enum_values). Answers appear in a call’s extracted under each field’s key. Writes take owner or admin keys; PATCH takes expected_revision (409 revision_conflict). Nothing is re-run over past calls. See Choose what calls find out.
  • insight on voice campaigns (POST /v1/campaigns, PATCH /v1/campaigns/{id}, read back on GET /v1/campaigns/{id}): the insight that campaign’s calls run. A change of insight alone is also taken while the campaign is queued or running.
  • insight on PATCH /v1/agents/{id} (read back on GET /v1/agents/{id}): the agent’s insight, for its inbound calls and for campaign calls whose campaign names none. From now on; not part of prompt_version.
  • MCP tools list_insights, get_insight, create_insight (needs an idempotency_key) and update_insight (confirm first, with expected_revision); insight on update_agent, create_campaign_draft and update_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} and DELETE /v1/segments/{id}: save a filter over your contacts (tag, name, company, channel, opted_out.<channel>, birth_month, attributes.<key>, with all / 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 is 400 invalid_filter, with every problem in issues. 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.
  • filter on GET /v1/segments/{id}, in the same names.
  • MCP tools create_segment, update_segment, delete_segment (asks for confirm: true first) and list_segment_contacts. See API tools.
  • A segment with an is_set or is_not_set condition 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 by category (phone, recording, speech, ai, messaging, whatsapp, email, numbers, other), UTC day and/or label. 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_id and campaign_id; and GET /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_large and export_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/messages without channel lists your SMS and WhatsApp template sends in one list, newest first by created_at (to the microsecond, ties by id), each item with its channel. to, from, campaign, status and the created_* times work as they do with channel; status=read lists WhatsApp only and status=undelivered SMS only. Inbound messages and inbox replies are not in it (GET /v1/conversations/{id}/messages). See List texts.
  • MCP list_messages lists both channels when channel is left out. See API tools.
  • channel on GET /v1/messages is optional. channel=sms and channel=whatsapp answer as before and their cursors keep working; a cursor of one of them is refused without channel, and a cursor of the list of both is refused with one (400 invalid_cursor, the hint names channel). A blank channel= is 400 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, completed and canceled come from SMS, WhatsApp and voice campaigns; campaign.waiting and campaign.window_opened from SMS campaigns only; campaign.completed’s billed_credits is null outside 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_at on GET /v1/conversations: the conversation with the newest message first, as the dashboard’s inbox shows it (a call by when it started). The default stays created_at. See Newest activity first.
  • updated_after on GET /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 largest updated_at, ask again from 2 minutes before it, and page to the end. It takes no sort. See Poll for changes.
  • MCP list_conversations takes sort and updated_after.
  • A conversation’s last_message_at now 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 /v1 sent). It is null for a call.
  • A conversation’s updated_at now 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 an updated_after poll 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/contacts takes tag (one tag, exact and case-sensitive), attribute_key (has that attribute) with an optional attribute_value (its value as text), and updated_after (an ISO 8601 time: contacts changed at or after it, oldest change first, by updated_at then id). They combine with each other and with query, not with phone or email. The MCP tool list_contacts takes them too. See Sync changes.
  • A contact’s updated_at now 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/contacts and the cursors it already answered are unchanged.
  • Model deepseek-flash (DeepSeek V4.1 Flash) on /v1/responses, /v1/chat/completions and GET /v1/models.
  • deepseek-v4-flash stays in the list and keeps working. DeepSeek now serves it with V4.1 Flash, and it is billed at the same rate as deepseek-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 has speaker (agent, or caller for the other end of the line), text and at_ms (milliseconds after started_at, when the call was answered), with the call’s status, summary and prompt_version. Any API key. See Voice → Read the transcript.
  • summary on 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= (an ag-… id or UUID) on GET /v1/calls and GET /v1/conversations: one AI agent’s calls and conversations. An unknown or deleted agent is 404 agent_not_found.
  • GET /v1/campaigns/{id}/recipients/{recipient_id}: one campaign recipient by the list’s id or its cr-… id, with campaign_id, contact_id, created_at and completed_at; a voice recipient adds duration_sec, its call’s transcript, extracted and summary. See Voice → One campaign recipient.
  • short_id (cr-…) on every campaign recipient in GET /v1/campaigns/{id}/recipients.
  • MCP API tools: get_call_transcript and get_campaign_recipient; list_calls and list_conversations take agent. See API tools.
  • The docs said a call’s transcript is only in the dashboard. It is on GET /v1/calls/{id}/transcript now, 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’s prompt, greeting, model (chat, voice), voice and language (owner and admin keys). Send expected_version (the prompt_version you read): when someone changed the agent meanwhile, nothing changes and the answer is 409 version_conflict with current_version. A request that changes nothing answers the agent as it is. The answer carries warnings (model_replaced, voice_language_mismatch). voicemail_message is 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-versions lists them (newest first, with source and what changed); 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; null for calls before today). GET /v1/calls takes ?prompt_version= (combine it with campaign).
  • MCP (/mcp/api): update_agent (asks to confirm first, then needs confirm: true and expected_version), list_agent_prompt_versions, get_agent_prompt_version; get_agent answers the new fields.
  • GET /v1/agents/{id} now answers the agent’s prompt, greeting, model, voice, language, prompt_version and updated_at; before, it answered only the placeholders its texts use. GET /v1/agents items add prompt_version and updated_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_settings on voice POST /v1/campaigns, PATCH /v1/campaigns/{id} and POST /v1/calls, read back (every field effective) on GET /v1/campaigns/{id} and the list (null on SMS and WhatsApp): phone_menu (auto, always, never), record_calls, max_hold_seconds, voicemail (action hang_up or play_recording, recording, detect_by_agent) and use_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_seconds is fixed once a campaign has started. See Call settings.
  • /v1/voicemail-recordings: upload (multipart file, MP3, M4A or OGG up to 5 MB), list, read with a link to listen, and delete (refused with 409 voicemail_recording_in_use while an unfinished campaign plays it). A campaign or call plays one with call_settings.voicemail: { "action": "play_recording", "recording": "vm-…" }. See Voicemail recordings.
  • MCP tool list_voicemail_recordings; create_campaign_draft, update_campaign_draft and place_call take call_settings. See API tools.
  • A new voice campaign, and a POST /v1/calls, with no call_settings.phone_menu now runs auto: the agent listens first, talks when a person answers and presses keys when a phone menu answers. Before, every /v1 call 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 /v1 before this change keep always, as they ran; GET /v1/campaigns/{id} now shows it as call_settings.phone_menu: "always". To change one, pause it, PATCH call_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 said content.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/api tools, wait for its calls with wait_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/api server and its claude mcp add command.
  • Connect an MCP client said /mcp/api had 17 tools; the list is on API tools.
  • Two route names missing their {id}: PATCH /v1/webhooks/{id} (choosing a webhook’s api_version) and PATCH /v1/campaigns/{id} (content.media after POST /v1/media).
  • The docs build now checks every /v1 request 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 after wait seconds with none. Without after it starts from now and returns your first next_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> on GET /v1/events, stream and poll: only that campaign’s events. Without types, 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) and resume_campaign (confirm and idempotency_key, like start_campaign). See API tools.
  • A WhatsApp or voice campaign’s campaign.completed and campaign.canceled are 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 *_at is then the time it was written. See the event rules.
  • POST /v1/campaigns/{id}/resume honours Idempotency-Key on 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 is 422 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”
  • 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_exceeded on a campaign preview or start carries metered (minutes that day already billed), held and held_by (queued or scheduled campaigns holding minutes that day, by ca-… id), day, time_zone, resets_at and per_call_minutes. used keeps its value: it already counted queued campaigns’ calls along with billed minutes, and is now documented as metered + held; read metered for 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-mini or gpt-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 /v1 API does not expose an agent’s pipeline; it shows up in a voice campaign preview’s cost_estimate.per_call_components, which for such an agent prices realtime and tts and no stt. 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”
  • message.received now also fires for WhatsApp messages people send you. data.channel is sms or whatsapp. On v2 the field was always sms; on 2026-10-01 it is a new optional field, and absent means sms. If your handler assumes SMS, check channel. See WhatsApp → Hear replies.
  • message.received carries quick_reply: { payload } for a WhatsApp button tap, otherwise null. It is optional on 2026-10-01.
  • POST /v1/media uploads 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 media as media on POST /v1/messages, as content.media on campaigns, or as media on a conversation reply.
    • An upload that is never used is deleted after 7 days.
    • The SMS guide now lists MOV and WebM too.
  • New single-object reads:
    • GET /v1/templates/{id}
    • GET /v1/agents/{id}, with variables
    • GET /v1/segments/{id}, with contacts
    • GET /v1/sms-templates and GET /v1/sms-templates/{id}
  • New fields: a WhatsApp template has uuid, created_at and all_buttons (every button, with a URL button’s url and dynamic). An agent has description, language and created_at.
  • An SMS preview waits up to 5 s before answering. A building preview carries poll_after_ms, progress and Retry-After. See Polling an SMS preview.
  • PATCH /v1/lists/{id} renames a contact list.
  • POST /v1/lists accepts contact_ids (up to 1,000), the same as POST /v1/lists/{id}/contacts does. A request that names people but would add none of them is 400 empty_audience_input, and no list is created.
  • GET /v1/events answers an Octo-Event-Version header naming the payload version of its frames. The stream still defaults to 2026-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 for confirm: true first);
    • 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.
  • PATCH /v1/campaigns/{id} can change an SMS draft’s from (E.164, pn-… or ch-…).
    • 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.
  • GET /v1/campaigns takes status=preparing: a one-request SMS create whose preview is still being built.
    • status=draft no longer matches those campaigns.
    • List items now read preparing, as GET /v1/campaigns/{id} already did.
    • If you listed drafts to find campaigns still preparing, filter by preparing.
  • A 401 invalid_api_key now has param: "authorization" and a message naming the cause: no key, the key in X-API-Key, an Authorization header without Bearer , or a key that is not valid. type, code and 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 /v1 or the MCP tools.
  • Sending back a null that GET /v1/campaigns/{id} returned for another channel’s field ("retry": null on an SMS campaign) is accepted on create and PATCH, and changes nothing.
  • A body that leaves out channel answers "`channel` is required: `sms` or `whatsapp`." with param: "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 404 for a malformed channel, thread, draft or attachment id (was 500). The thread routes accept thr-… ids and answer 404 for a thread that is not on the URL’s channel.
  • POST /api/conversations/click-to-call answers a disabled agent with 409 { "error": "agent_disabled", "message": "…" }.
  • GET /api/billing/usage cursors keep microseconds, so no row is skipped. A malformed cursor is 400 invalid_cursor. GET /api/billing/usage/export refuses more than 250,000 events with 400 export_too_large instead of truncating.
  • GET /api/segments/{id}/members pages by after / nextCursor (offset is 400), and the total is returned only with includeTotal=true. DELETE /api/segments/{id} answers 409 segment_in_use while 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}/deliveries and GET /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 from GET /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_out until 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.
  • 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 from is refused (400 from_not_sms_capable on a text, 422 sender_provider_unsupported on 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.received is 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 with GET /v1/conversations. If you built a WhatsApp inbound handler on message.received, it will never fire.
  • The shared trial number calls any phone number on your profile or a teammate’s (GET /v1/me user_phone), not only your own; the Overview, Testing, Errors and Place a call pages, and the API’s fix/hint text, now say so.
  • billed_credits is the cost of an SMS; WhatsApp sends answer null (see WhatsApp pricing).
  • opted_out on 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 refused 409 preview_stale.
  • An Idempotency-Key collides only with a request of the same kind (the same route). POST /v1/contacts, POST /v1/lists (and adds to a list) and POST /v1/conversations/{id}/messages are keyed too, now listed in the Idempotency table.
  • Authentication names the whole messaging API and links its own rate limits.
  • MCP start_campaign needs a preview_id on 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 a ready preview with samples; the send examples show text and media.

Changes to the dashboard API (/api):

  • POST /api/lk/token now requires a signed-in session and an agent of your workspace. It answers 401 without one, 403 session_required for an API key, 404 agent_not_found for an agent id that is not a live agent of your workspace, and 409 agent_disabled for 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”
  • A call with source: "agent-test" now keeps the ask-a-human tool on unless you pass skipInputGathering: 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”
  • POST /v1/calls from the shared trial number (pfn-…) accepts any agent of your workspace as agent. The agent chosen for the trial number in the dashboard is now the default when agent is left out; naming a different agent of your workspace used to be 400 invalid_from. The route guard and the trial caps are unchanged.
  • MCP list_phone_numbers adds deployed_agents, can_dial and inbound_agent per voice and SMS channel, and only_active now keeps every number that can dial, whether or not an agent answers it. outbound_agents and outbound_enabled still return the same values for one more release; move to deployed_agents and can_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 by status, clicked and replied (for example everyone who got the text but did not click), with previewOnly to count first. People who opted out are always left out. Listed in the tool catalog. No /v1 change.

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_template names its template by templateName (plus language when that name is approved in more than one language), as POST /v1/messages already does with template.name. templateId still 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 the rev_… 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”
  • On POST /v1/responses and POST /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) and POST /v1/campaigns / PATCH /v1/campaigns/{id} (content.agent) answer 404 agent_not_found, the same body as for an id that names nothing. MCP place_call, create_campaign_draft, preview_campaign and start_campaign follow.
  • A voice campaign whose agent was deleted after it was saved: a PATCH of its content answers 404 agent_not_found, a preview lists agent_not_found, and a start refuses it. A campaign whose agent is deleted while it runs skips its remaining calls as agent_disabled.
  • POST /v1/calls without agent, from a number whose own agent was deleted, answers 400 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-call answers 404 { error: "agent_not_found" } for an agentId that is not a live agent of this workspace (was: the call was placed, or 400 / 409), and accepts an ag-… id as well as a UUID.
  • POST /api/email-channel/{channelId}/threads/{threadId}/mode and POST /api/simulation/start answer 404 for 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) and start_campaign answered 404 not_found for 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, its GET /v1/messages/{id} id; a search match adds api_message_id. A message adds text and media.
  • GET /v1/conversations/search pages: newest first, with after.
  • GET /v1/contacts/{id}/channels pages with limit and after.
  • GET /v1/senders adds sms.channel_id and voice.channel_id.
  • Preview samples add contact_short_id (the ct-… id; contact_id stays the UUID); the error envelope declares channels, cap, used, remaining, resets_at, allowed_country and closes_at.
  • 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. primary is what the contact’s phone or email shows.
  • A contact’s updated_at moves when a number or email is added or removed, and on an opt-out or opt-in.
  • Search results come newest first; score is the search index’s relevance when it scored a match, else 0.
  • {{contact.id}} on a WhatsApp or AI-call campaign is 400 invalid_variable on that variable, with a hint.
  • A conversation opened by POST /v1/messages answers its phone, and every conversation fills name, phone and email from its contact.
  • A preview with no audience lists sender_not_cleared too.
  • An SMS campaign’s variables keep the text’s own |fallback.
  • invalid_rows counts the contact_ids an 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/messages with channel: "whatsapp" now keeps a refusal that came after the message was stored, and a retry with the same Idempotency-Key replays it (Idempotency-Replayed: true). That is 409 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.
Section titled “2026-10-19 — The inbox: conversations, replies, search”
  • Work the inbox, a new guide to /v1/conversations.
  • GET /v1/conversations lists your inbox on every channel, and GET /v1/conversations/{id} reads one, with its summary and labels.
  • GET /v1/conversations/{id}/messages pages a conversation’s messages, with who wrote each (author).
  • POST /v1/conversations/{id}/messages replies in the thread, through the same checks as a reply from the inbox. On SMS it sends message.sent with author: "api".
  • PATCH /v1/conversations/{id} hands a conversation to a person, returns it to the AI agent, or closes it.
  • GET /v1/conversations/search finds messages by their words.
  • POST /v1/calls/{id}/end ends a call in progress, and GET /v1/calls/{id}/voicemail downloads its voicemail.
  • Seven new MCP API tools for the inbox, including reply_to_conversation and end_call behind confirm: 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/campaigns with start: "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 still 422 no_recipients with skipped_by_reason, and now names the draft in campaign_id (with short_id and state: "refused"); before, no campaign was created. Fix the audience and start the draft, or delete it. A retry with the same Idempotency-Key answers 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 in refusal.
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/contacts takes them, and POST /v1/contacts/{id}/tags and DELETE /v1/contacts/{id}/tags/{tag} change them. Tag workflows start for them.
  • POST /v1/contacts/{id}/dnc opts 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}/channels lists, adds and removes a contact’s phone numbers and emails.
  • GET /v1/contact-fields lists the custom contact fields.
  • POST /v1/lists/{id}/contacts takes contact_ids, contacts you already have.
  • Ten new MCP API tools for contacts and lists, including delete_contact_list behind confirm: 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/campaigns with start: "auto" and a WhatsApp or voice audience nobody can be sent to is 422 no_recipients (with skipped_by_reason), and no campaign is created. A voice campaign used to complete empty; a WhatsApp one answered 400 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: start is required on POST /v1/campaigns ("draft" or "auto"); without it the answer is 400 start_required and nothing is created.
  • Breaking: an id of the wrong kind is 400 invalid_request, not 404, on every campaign {id} route, GET /v1/calls/{id}, GET /v1/messages/{id}, the campaign filters, /v1/media and /v1/recordings. See Ids.
  • Breaking: /v1/events, /v1/media and /v1/recordings answer the /v1 error envelope, refuse unknown or repeated query parameters, and refuse an after that is not a whole number (400 invalid_cursor; it used to replay seven days).
  • Breaking: webhook endpoint created_at and updated_at are ISO 8601 strings, not Unix seconds. See the webhooks reference.
  • Breaking: a WhatsApp template id with another template’s name or language is 400; a misspelled audience.contacts[] key is 400 unknown_field; WhatsApp previews’ no_whatsapp is no_phone_number; a WhatsApp or voice start that reaches nobody is 422 no_recipients; GET /v1/senders has the new statuses provider_unsupported (SMS) and trial_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 add cost_estimate; WhatsApp samples add buttons.
  • 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/calls filters by to and status; WhatsApp GET /v1/messages takes from and campaign.
  • 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/calls without agent, from a number with no agent of its own, is 400 agent_required and dials nothing. It used to call the real person with a built-in test agent. Name an agent from GET /v1/agents.
  • The event stream sends v2 with ?api_version=v2, the same body a v2 webhook endpoint receives. The default is still 2026-10-01.
  • Outbound test calls now send call.ringing before call.answered.
  • MCP API tools: waits end within wait_seconds, and every method other than POST is 405.
  • schedule.end_at must be at least 1 ms after start_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, and paused_reason values.

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”
  • Payload versions per endpoint. Every webhook endpoint has an api_version. Endpoints that existed before this release (v1.84.0.0) keep 2026-10-01 and receive exactly the same bytes as before. A new endpoint (API or dashboard) gets v2 unless you ask for "api_version": "2026-10-01"; PATCH switches it. The GET /v1/events stream still sends 2026-10-01. See Payload versions, with an upgrade recipe.
  • v2 events use the /v1 words and ids: object, api_version and livemode on the envelope; channel everywhere; canceled; message events keyed by the /v1 message id (message.sent adds message_ids for a split reply); one message.status_updated shape for SMS and WhatsApp with error.message and read_at; campaign.completed counts in the GET /v1/campaigns/{id} buckets, as of completion; short ids and UUIDs side by side, sender_id; and your own metadata on message, call and campaign events. The OpenAPI document lists every v2 event under webhooks.
  • /v1/webhooks answers like the rest of /v1 (breaking): the /v1 error envelope with code and request_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), 405 with Allow, 400 for 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 /v1 request you would make, so errors, rate limits and idempotency are the API’s own.
  • send_message, place_call and start_campaign need confirm: true and an idempotency_key; without them they answer confirmation_required and send nothing. start_campaign needs a preview_id. create_campaign_draft needs an idempotency_key too.
  • There is no test mode yet: every message and call is live.
  • /mcp and /mcp/tools are 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 429 is now possible): the messaging routes take 300 reads (GET) and 100 writes a second per key. Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds); past the limit the answer is 429 rate_limited with Retry-After. SMS sends keep their own, stricter send limit (10 a second), whose 429 carries its own Retry-After. See Rate limits.
  • schedule.end_at is enforced (breaking): at end_at a campaign starts no new text or call, a call or text under way finishes, everyone not reached reads skipped, and the campaign turns completed with the new completed_reason: "schedule_ended". A date is the end of that day in tz. end_at must be after start_at and now, and a start after it has passed is 400 invalid_schedule on schedule.end_at. A voice recipient the campaign never called (the end, DNC) reads skipped.
  • Schedules are checked (breaking: some schedules accepted before are now 400): hours start is 00:00–23:59 and end 00:00–24:00 (25:99 was taken); a window that starts and ends at the same time is invalid_schedule; a later start than end is an overnight window. A start_at with an offset is taken (it was refused). start_at and end_at read 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, …) is 400 invalid_cursor with a hint. The same filter written another way keeps the cursor.
  • field_not_for_channel: a field of another channel’s campaigns (content.mission on SMS, limits.spend_cap_credits on voice, retry on WhatsApp, …) is one 400 on create and PATCH, with param and fields (it was unknown_field, field_not_editable or invalid_request).
  • Templates list their words: GET /v1/templates answers body and header_text.
  • A call’s mission takes 4,000 characters (it was 500), as a campaign’s.
  • unmapped_variable puts its example in hint, 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 a hint saying so, with the %2B form (to, from).
  • PATCH variables merges 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/messages lists everything you sent on a channel: to and campaign are optional filters now, on SMS and WhatsApp. Both channels come newest first by created_at, ties by id. Breaking for SMS: the SMS list was ordered by hand-over time (texts not handed over yet first); it is now created_at, newest first, and a cursor from the old order is refused once with 400 invalid_cursor (start again without after). WhatsApp and call cursors keep working.
  • metadata on single sends and calls: POST /v1/messages (both channels) and POST /v1/calls take metadata, 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. Campaign metadata now has the same limits (it had none but the value length); past one is 400 invalid_request with param metadata or metadata.<key>. {} is the same as no metadata, including for an Idempotency-Key retry.
  • Ids that join: every call object carries short_id (cv-…, the id the dashboard shows; id is unchanged). Campaign recipients carry message_id (SMS and WhatsApp: the id GET /v1/messages answers) and, on voice, call_short_id. message_id is null for 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 of call.completed (status completed, voicemail, no_answer or busy) or call.failed (a system or carrier failure). Each carries call_id (the call’s id), conversation_id, direction, from, to, agent_id, campaign_id, status and occurred_at; call.answered adds answered_at; the terminal two add answered_at, outcome, duration_sec, ended_at and disconnect_reason. A campaign’s calls reach only endpoints that name the types. See Call events.
  • Every ended call gets an outcome within 5 minutes, unknown when there was nothing to classify (a new outcome word; inbound calls, which are not classified, read it too). A call that stops reporting is closed as failed / 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’s status and outcome never change.
  • A call nobody answered (SIP no-answer) or that was declined now reads no_answer / busy instead of failed.

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} merges variables: a name you send is set, "name": null removes 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 is 400 unknown_variable with every such name in fields; remove them with null in 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 to audience.import. refusal is now { status, error }, the /v1 error a start would answer.
  • Every problem at once: a preview lists every unfilled placeholder on every channel, and unknown_variable / unmapped_variable carry fields. A ready voice preview has samples (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 from of a text, a WhatsApp message or a campaign is 422 trial_sender_not_allowed (was 400 invalid_from).
  • An unknown key inside audience is 400 unknown_field with a did-you-mean; an id of the wrong kind (sg-… as audience.list) is 400 invalid_request naming the prefix. A template name in more than one language without language is 400 invalid_request (was 404). A voice draft may leave out content. Call outcome values are listed in Place calls. 413 carries request_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/lists and GET /v1/segments list the cl-… and sg-… ids a campaign’s audience takes. GET /v1/agents and GET /v1/templates now page with limit (default 50) and after, like every other list; agents carry uuid and senders channel_uuids, the ids events use. Templates carry variable_keys, the keys a campaign’s variables takes.
  • 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 (or campaign_canceled), before any preview check; previewing a campaign that is not a draft or paused is 409 campaign_not_previewable.
  • A preview reports every problem it finds before counting (error.also).
  • retry.on no longer takes busy (it was dropped silently: a busy line counts as no_answer). content.voicemail_drop is 400 voicemail_drop_unavailable (it was saved but could not play).
  • Campaigns answer started_at and completed_at; variables is {} when empty; list items have no channel block. A replayed create answers the first answer word for word; an SMS message’s created_at is the same on create, replay and read. A write with a query parameter is 400 unknown_parameter. GET /v1/me adds user_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/messages sends a text ("channel": "sms") or an approved WhatsApp template ("channel": "whatsapp"), POST /v1/calls has an AI agent call a number with a mission, and POST /v1/campaigns sends texts, templates or calls to a whole audience. GET /v1/senders lists your numbers with their readiness on each channel, and GET /v1/me names the workspace and key. Bodies are snake_case and strict (400 unknown_field), every object has one status, 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. content is 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 is 409 preview_stale. variables are template strings such as "{{contact.shoe_size|unknown}}". See Campaigns.
  • One rule for placeholders on every channel. A variables name the content has no placeholder for is 400 unknown_variable with a did-you-mean. A placeholder nothing fills makes the preview status: "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. PATCH refuses another number’s template (422 template_not_on_sender), an unknown agent or template, and can set a draft’s audience.list or audience.segment. A start missing something answers 422 launch_fields_missing with missing in /v1 field names. Campaigns return limits and retry as written, and take a WhatsApp template by name and language as a single send does.
  • Strict query strings and clearer refusals. An unknown query parameter is 400 unknown_parameter; GET /v1/templates and GET /v1/agents take limit. Did-you-mean hints use edit distance at the field’s own path, validation messages are plain sentences, a campaign contact’s bad phone is 400 invalid_number with its E.164 form, schedule.tz must be an IANA zone, and every /v1 response (an unknown path included) carries X-Request-Id and, for errors, the JSON envelope.
  • WhatsApp statuses are events. message.status_updated now fires for WhatsApp messages too (sent, delivered, read, failed), and every payload carries channel and the public status.
  • Campaign events for every channel. WhatsApp and voice campaigns now send campaign.started, campaign.paused, campaign.resumed, campaign.canceled and campaign.completed. campaign_type can be whatsapp_template, voice_ai or voicemail_drop, and billed_credits is null for those. See Delivery & webhooks.
  • New guides: Send SMS, Send WhatsApp, Place calls, Campaigns, Delivery & webhooks and Errors. The dashboard’s own /api routes are still there for the dashboard, in the Dashboard API reference.

Changes to the dashboard API (/api):

  • POST /api/wa-templates/{id}/send answers 402 (was 422) for workspace_suspended and route_blocked.
  • POST /api/campaigns answers 409 create_in_progress (was idempotency_conflict) while a create with the same Idempotency-Key is still running. POST /api/seax/campaigns is unchanged.
  • POST /api/conversations/click-to-call called with an API key defaults source to manual.
  • message.status_updated payloads gain channel and status, and a WhatsApp variant (channel: "whatsapp").
  • campaign.* events also fire for WhatsApp and voice campaigns, with campaign_type whatsapp_template, voice_ai or voicemail_drop, and billed_credits null for 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/sends refuses a to that is not E.164. A number such as 415-555-0100 or +1 415 555 0100 used to be read in the sender’s country, sent and billed. It is now 400 invalid_number with param: "to", and nothing is sent or charged. When the number is clear, the hint says which E.164 we think you meant (Did you mean +14155550100?). Campaign contacts[].phone was already strict; a contactsCsv file is still read loosely.
  • /api/workspaces/{id}/outbound-compliance refusals carry code. A stale version is 409 with code: "version_mismatch" (error is unchanged); the other refusals there carry invalid_request, insufficient_role, forbidden or not_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"), workspaceId for the compliance call, one rule for naming the sender, the exact Idempotency-Key characters, 400 empty_audience_input, the full webhook endpoint object, listing, changing (event_types replaces the list) and deleting endpoints, the replay_truncated frame 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, or POST /api/campaigns with a launch) answers 402 insufficient_credits with shortCredits when the balance is below 0.1 × recipients. A create that launches also returns the campaignId of 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-previous now 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 replaces 409 call_setup_in_progress here), 503 release_not_ready (a passing problem; retry), and 503 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 answers 409 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" }; show message to 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/events types: meta_response.completed (a background POST /v1/responses finished or failed) and meta_chat.updated (a chat with Octo got a new reply). "*" endpoints receive them: answer 2xx to any type you 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 its error.
  • /api/users/me/integrations is retired. GET, PUT and PATCH answer 410 with { "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 with POST /v1/webhooks instead. Its two call-session events are not carried over: nothing sends them any more.
  • If you received meta_response.completed on the old webhook: add a webhook endpoint subscribed to it (Settings → API Keys & Webhooks, or POST /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 of x-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/webhooks now refuses a key whose owner has the Agent role in the workspace with 403 insufficient_role, on every route. A webhook receives every event of the workspace, so the same rule applies in the dashboard.
  • GET /v1/events is owners and admins only too, for the same reason: the feed carries every event of the workspace, message text included. An Agent-role key gets 403 insufficient_role.
  • POST /v1/webhooks/{id}/rotate-secret returns 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}/test sends one sample webhook.test event 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 url is 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 *_at time is when it was written, not when the campaign changed, and a late campaign.resumed has previous_reason: null. campaign.waiting and campaign.window_opened are 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/events types: campaign.waiting (an SMS campaign reached the end of its sending hours, or started outside them, and waits; resumes_at says when its hours next start and tz the time zone they are set in) and campaign.window_opened (its hours started again and it goes on sending). Waiting is not a pause: the campaign’s status stays queued or running. A pause or cancel ends the wait: the campaign sends its own event and that wait never gets a campaign.window_opened; resumed while its hours are still closed, it sends a new campaign.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 other campaign.* events. Answer 2xx to any type you 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/events types: campaign.started, campaign.paused, campaign.resumed, campaign.completed and campaign.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.completed carries 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) any type you 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/byo can answer 409 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 suggests defaultCallRouting: "texts_only". Send callRouting: "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.outcome in 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.recorded fields are now in credits. amount_usd is now amount_credits and balance_usd is now balance_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: true marks it).
  • GET /api/campaigns/{id}/records (SMS). New sort, dir and sealed after paging (400 invalid_cursor; de-duplicate by id), plus each row’s latest reply (replyText, lastReplyAt) and clicks.
  • Clicks. Scanner and preview fetches do not count, and repeat visits within 30 minutes are one click.
  • not_receiving numbers (incoming texts diverted to a Twilio Messaging Service) cannot send: 422 sender_not_cleared from the API (senderStatus on a single send, status on a campaign start). Octo’s single-text tool answers sender_not_receiving; a workflow sms.send step is skipped with that reason.
  • Scheduling. schedule.startAt as a day or an instant with offset; Octo and MCP tools take schedule.startLocal plus an IANA tz and 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. Pass dialog_insight_id and exactly one of conversation_ids (up to 1,000), campaign_id (voice campaigns only) or agent_id (optionally narrowed with activity_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 in conversation_ids are counted in excluded_live. rerun_analyzed (default false) re-runs conversations the insight already analysed; a re-run can also remove labels that no longer apply. trigger_workflows (default false) fires on_label workflows for the labels the job adds or removes. With false the labels change silently: neither on: applied nor on: removed workflows fire, then or later. It costs LLM credits per conversation and is spends_money, so over /mcp/tools it answers confirmation_required, over /mcp it waits for a human YES, and the stateless /v1 API 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, or activity_since without agent_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 returns code: "batch_already_open" with existing_batch: { id, short_id }.
  • dialog_insight.analyze_batch reports what the job will actually do. The result’s batch.status is the job’s real status: queued, or completed when this insight had already analysed every selected conversation (nothing to run, nothing billed, no notification). batch also carries already_analyzed (skipped at creation, not billed) and will_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 a notification line: 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 answers code: "too_many_open_batches" with open_batches, limit and a next_step; nothing is created, and retrying before a job finishes is refused the same way.
  • dry_run: true on dialog_insight.analyze_batch creates nothing and costs nothing. It returns ended, already_analyzed, will_analyze and live for the selector, and refuses an unknown insight or agent and a missing or non-voice campaign like the real run. A dry run is safe, so over /mcp/tools and /mcp it runs without a confirmation (/v1 still does not offer the tool, whose static class is spends_money). Any other value of dry_run is 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_id that is missing or not a voice campaign is refused before anything is parked.
  • dialog_insight.batch_status adds skipped_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 has paused: true and a paused_note: open jobs wait in the queue and resume on their own.
  • New MCP tool: dialog_insight.batch_status (safe). Takes a job id or dib-… short id and returns the job (including origin: api, octo or attach_dialog, and activity_since), counts by item state (skipped includes conversations that were live again when their turn came, reason not_ended), and the first 50 failed conversations with their reason_code; without batch_id it 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 status stopped_over_budget if 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, or PATCH /api/agents/{id} with dialogInsightScope: "analyze_history" or "all_history" — answers 429 with code: "too_many_open_batches", openBatches and limit, 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 created completed) is never refused. Same-insight duplicates still answer 409 batch_already_open first. The PATCH /api/agents/{id} refusal carries openBatches and limit too, like the POST.

  • The history attach’s dialogInsightBatch adds status: "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 and GET /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 dialogInsightScope on PATCH /api/agents/{id}: all_history. Attaches the insight (new conversations are analysed from now on, like from_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 adds dialogInsightBatch: { id, shortId, itemCount }, or null when 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, another all_history attach of the same insight to the same agent is a 409 (code: "batch_already_open").

  • dialogInsightScope: "analyze_history" is a background job now, like all_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 same dialogInsightBatch field. 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 a 409 (batch_already_open).

  • New optional field triggerWorkflows (boolean, default false), with dialogInsightScope: "analyze_history" or "all_history". true fires on_label workflows for the labels the job writes; labels are written either way. Sent with from_now_on or with no scope it is a 400 with code: "trigger_workflows_without_history_scope".

  • POST /api/dialog-insight-batches accepts activitySince (ISO 8601, with agentId only): the job takes only the agent’s conversations whose last activity is at or after that instant. With conversationIds or campaignId it is a 400 (activity_since_requires_agent). Jobs report it back as activitySince (null when unset).

  • GET /api/agents/{id}/dialog-insight/backfill-estimate takes an optional dialogInsightId query parameter (UUID or di-…) and returns three new fields. allHistory (what all_history would select: every ended conversation) and last48h (what analyze_history would select: ended, with activity in the last 48 hours) are each { ended, alreadyAnalyzed, willAnalyze }, where alreadyAnalyzed counts conversations with an up-to-date result from that insight, which the job skips. Both are null without dialogInsightId. live counts conversations that have not ended. An unknown dialogInsightId is a 404 (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.

  • dialogInsightScope now accepts from_now_on, analyze_history and all_history; any other value is a 400 naming the three.

  • POST /api/conversations/{id}/analyze accepts triggerWorkflows (boolean, default false). Re-analysing a conversation no longer fires on_label workflows (alert emails and the like) unless you send true; labels are written either way. Before, every manual analysis fired them. The force field 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. A conversationIds entry that has not ended gets no item; the create response’s new excludedLive field counts them (always 0 for campaignId / agentId), and when every id is live the response is the zero-items 200. A conversation the customer writes to again before its turn comes is skipped with reasonCode: "not_ended". Each is analysed automatically when it ends. Ids that are not in the workspace still fail with conversations_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 with state: "skipped" and reasonCode: "already_analyzed" straight away, so a job’s progress counts only what it will actually run, the same number the attach estimate’s willAnalyze gives. POST /api/dialog-insight-batches (202) and the attach response’s dialogInsightBatch add alreadyAnalyzed; itemCount is still every conversation the job took. When every one was already analysed the job is created with status: "completed".

  • GET /api/dialog-insight-batches/{id} and the list’s items add skippedByReason, 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-batches takes agentId (UUID or ag-…) and status=open (queued or running), separately or together, to find a job still running for an agent. An agentId that is not in the workspace is a 404 (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_completed in GET /api/notifications and 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 already completed (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_name and campaign_id / campaign_short_id / campaign_name (each null unless the job was for that agent or campaign), selector, origin, analysed, skipped_other, already_analyzed, failed, item_count, and url, 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}/analyze with a dialogInsightId, that runs a different insight from the one the conversation resolves to (campaign insight → campaign’s own extraction fields → agent default) no longer writes extractedData on 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 is mutates_live_deployment, so over /mcp/tools it answers confirmation_required, and over /mcp and /v1 it waits for a human YES. It accepts only a conversation that is handed_off, and returns previous_handoff_reason / previous_handoff_cause.
  • get_conversation (MCP) tells you who wrote each message and why the AI paused. Each message carries sender (customer, ai, human_agent, business_app, system) and source (live, business_app_reply, imported); a message the customer never received adds delivery_status and sent_to_customer: false. A handed_off conversation adds handoff_reason, handoff_cause (business_app, team, ai, workflow, unknown), handed_off_at, and auto_return_at when the channel returns conversations to the AI automatically.
  • is_business_app on channel and conversation tools. whatsapp.get_status, list_configured_channels, get_channel, get_conversation and get_conversations mark a WhatsApp Business App number with is_business_app: true.
  • Return to AI automatically, per channel. Set config.autoReturnToAi = { enabled, minutes } (minutes 10–60) on POST / PATCH /api/channels, or auto_return_to_ai on the update_channel tool: 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} merges config on a WhatsApp Business App channel: the keys you send are set and every other key is kept.
  • New 409 codes on POST /api/channels and PATCH /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 channel coexistence_reconnect_required / coexistence_number_change (its credentials change only by reconnecting through Connect my WhatsApp Business App).
  • New refusal number_disconnected (422) on POST /api/wa-templates/{id}/send and POST /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 on code, 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 defaultDialogInsightId at an insight — PATCH /api/agents/{id}, or the dialog_insight.attach tool — 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) or analyze_history, which reaches back 48 hours — the same activity window the fallback sweep uses — and no further. It must ride with defaultDialogInsightId; on its own it is refused with 400 dialog_insight_scope_without_pointer, and any other value is a 400 naming 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 an analyze_history attach would do — totalConversations, activeLast48h, backfillReachable, reachableStillOpen, reachableRecentlyClosed, reachableOlderThan48h. backfillReachable - reachableOlderThan48h is the number that matters, because that scope floors at the same 48-hour window; backfillReachable alone 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}/analyze and dialog_insight.test both 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_message on /mcp no 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 /v1 always has. The result carries pending_action: { action_code, summary, how_to_confirm }. Nothing is done until you call send_message again on the same conversation_id with YES <code> (or NO <code>). That second call returns executed: { ok, summary }. See MCP overview.
  • What to change: if a script drives /mcp unattended and relied on risky actions going through, it now gets a pending_action back and has to ask a person. /mcp/tools is unchanged: it still refuses those tools with confirmation_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 type or status we do not recognise is a 400, not a 500. On GET /api/campaigns both 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" plus parameter, received and accepted[]. The same fix is on GET /api/campaigns/{id}/records, whose status had the identical hole. disposition on 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 409 when the campaign is in the wrong state. All three used to answer 404, 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. Now 404 means fix your id, and 409 campaign_not_cancellable / campaign_not_pausable / campaign_not_resumable means stop asking. The body carries the status the campaign is actually in and the set the verb accepts. For cancel, a 409 is 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_recipients and max_concurrent_calls_too_high replace English sentences that used to sit in error. They are returned by POST /api/campaigns, POST /api/seax/campaigns and POST /api/campaigns/{id}/start. Read launch_fields_missing.missing[]: a voice campaign will not dial without agentId, config.callMission and config.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 returns recipients for a launched campaign. That field is the composer’s working state, not a recipient list. For a head-count use stats.total, which is on the list response too; for per-person outcomes use GET /api/campaigns/{id}/records. A campaign still in draft or paused returns it in full, and ?include=recipients asks for it on a launched one.
  • providerCallId on /results is always null for a voice campaign. This is not new, but the field’s description implied otherwise. Key your records on callId, 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’s wt-… short id or its UUID. The body is to plus Meta’s own components array, 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.
  • components are 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 with 422 template_components_mismatch and an error that names the mismatch. Supported buttons are url, copy_code (Meta’s coupon_code shape, up to 15 characters) and quick_reply (with a payload), each at its index in 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 carries messageId, conversationId and conversationShortId.
  • Not idempotent. Every successful call sends and bills one template. After a timeout, read the thread before retrying.
  • POST /api/conversations/{id}/messages accepts template ({ templateId, components }) instead of content / mediaUrl on 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 signed send_token. It cannot send. send_instagram_message (mass_message, so it pauses for a human YES on /mcp and /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_leads now returns conversation_id per 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_agent and trial.release_number (mutates_live_deployment) point an agent at it and stop it again.
  • Check trial.get_number before concluding a workspace has no phone number. list_phone_numbers returns 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_call accepts a pfn-… id. That is the trial number’s identifier, returned by trial.get_number. It only dials once an agent is bound to it, and only to that number’s allowed countries — a +1 destination 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/tools like every other gated class, and are available through /mcp or /v1 where Octo can park them for confirmation. They also require an owner or admin key: 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 description rather than the description on its exported definition, 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_agent accepts take_over_inbound. A channel has at most one agent that answers inbound traffic, while many can share it to send or dial out. Previously deploy_agent could 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. Pass take_over_inbound: true to 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 in replacedAgentId.
  • 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. Requires channel_id; the legacy channel type parameter does not identify a channel row to own and is rejected.
  • config does not control routing. It is opaque per-deployment JSON. config: { inbound: true } and similar were accepted and ignored; use take_over_inbound instead.

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) and POST /api/campaigns/{id}/start return { "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, so draft: true keeps 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_agent now returns a disclosure block — enabled, applies, spoken_text, mode, using_default_text, and a plain-language note. Read it instead of the raw columns: a null disclosure_text means “use the platform default”, not “say nothing”, and applies: false distinguishes a text-only agent from a voice agent with the safeguard switched off.
  • MCP: update_agent accepts disclosureEnabled, disclosureText, and disclosureRecordingId. 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 kind disclosure; voicemail-drop clips are a separate kind and are rejected.
  • Delete or modify many resources in one call. New workspace tools — delete_conversations, delete_channels, delete_contacts, delete_campaigns, undeploy_agents, and cancel_campaigns — act on many matching rows at once (callable over the MCP servers and the OpenAI-compatible API). Each takes either a filter or an explicit ids list. 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 matching confirm_count; a confirmation is drift-aborted if the matching set grows

    10% before you reply; delete_campaigns skips running campaigns unless you opt in; delete_contacts is a recoverable soft delete.

  • 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 a GET /v1/events SSE 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-ID so a reconnecting client never misses an event.
  • 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.
  • Rate-limit headers on every response. The OpenAI-compatible /v1 API now returns X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset on success as well as on 429, so an agent can pace itself proactively instead of only reacting to Retry-After. See Rate limits.
  • Self-correcting error envelope. Errors keep the OpenAI shape (message, type, param, code) and now add hint and docs_url on 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.
  • Docs site. Launched docs.omniocto.com covering the three Octo surfaces: the OpenAI-compatible /v1 API, 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 .md to any URL), and OpenAPI JSON for both APIs.
  • Docs-search MCP. A live MCP server (/mcp-docs) with search_docs and get_doc so 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 Octo base_url and an sk-octo_ key. SSE streaming, background mode, Idempotency-Key replay, and previous_response_id continuation.
  • API keys in the dashboard. Create and revoke sk-octo_ keys from Settings → API Keys. See Get an API key.