Platform API (/v1) overview
The Platform API (/v1) lets your own app reach your customers through Omni Octo. You
send a text or a WhatsApp template, have an AI agent call someone, or run a
campaign to a whole list. Every channel uses the same field names, the same
status field, the same errors and the same idempotency rules.
curl https://api.omniocto.com/v1/me -H "Authorization: Bearer $OMNI_OCTO_API_KEY"{ "object": "me", "workspace": { "id": "00000000-0000-4000-8000-00000000000f", "short_id": "ws-0000000001", "name": "Main Street Cafe" }, "user_id": "00000000-0000-4000-8000-000000000010", "user_phone": "+14155550123", "api_key": { "id": "00000000-0000-4000-8000-000000000011", "role": "owner", "expires_at": null } }Start here
Section titled “Start here”| to | guide | fields | first success |
|---|---|---|---|
| go from a key to a text, a webhook and a campaign | Quickstart | about ten requests | |
| hear an AI agent call you | Call your own phone | Calls | about 3 minutes, no card |
| send a text | Send a text | Messages | after carrier registration, 1 to 3 weeks |
| send a WhatsApp template | Send a WhatsApp template | Messages | once a number and a template are approved |
| reach a whole list | Run a campaign | Campaigns | a one-person test first |
| keep your contacts, tags and opt-outs | Keep contacts | Contacts | right away |
| read and answer your inbox | Work the inbox | Conversations | right away |
| hear back: deliveries, replies, calls | Receive webhooks | Webhooks | minutes, with a test event |
| try it all without spending | Test without spending | right away | |
| drive it from Claude Code or another MCP client | API tools (MCP), call campaigns from Claude Code | one command |
What every code means: Errors. Every field of every endpoint: Reference.
Environments
Section titled “Environments”| environment | API (/v1) | dashboard (keys, numbers, billing) |
|---|---|---|
| production | https://api.omniocto.com | https://agent.omniocto.com |
| development | https://api.agent-dev.omniocto.com | https://agent-dev.omniocto.com |
- A key belongs to one environment and one workspace. Create it in that
environment’s dashboard. A production key sent to development is
401 invalid_api_key, and so is the other way round. /v1answers only on the API host. The same path on a dashboard host is404.- The examples use production. To run them elsewhere, put the base URL
in a variable and write
$OMNI_OCTO_API_URLwhere they writehttps://api.omniocto.com, as the quickstart does.
The resources
Section titled “The resources”| resource | what it does | guide |
|---|---|---|
/v1/messages | Send one SMS/MMS or one approved WhatsApp template, read its status, list what you sent. | Send a text, Send a WhatsApp template |
/v1/calls | An AI agent calls one number and works on a mission you write; read or list calls, end one, download its voicemail. | Place a call |
/v1/campaigns | Send texts, WhatsApp templates or AI calls to many people: create, preview, start, pause, watch. | Run a campaign |
/v1/senders | The numbers you can send and call from, and whether each one is ready on each channel. | Send a text |
/v1/templates | Your WhatsApp templates: the wt-… id to send, the number each sends from, and what each needs filled in. | Send a WhatsApp template |
/v1/agents | Your AI agents and their ag-… ids, for calls and voice campaigns. | Place a call |
/v1/lists | Contact lists (cl-…) for a campaign’s audience.list: create one with its contacts, add more, take one out, delete it. | Run a campaign |
/v1/contacts | The people you text and call (ct-…): create, find, search, change and delete them; their numbers and emails, tags and opt-outs. | Keep contacts |
/v1/contact-fields | The custom contact fields, the keys of a contact’s attributes. | Keep contacts |
/v1/conversations | Your inbox on every channel (cv-…): list and read conversations and their messages, reply in one, hand it off or close it, search what was said. | Work the inbox |
/v1/segments | Saved segments (sg-…): a filter over your contacts, for a campaign’s audience.segment. Create, change, delete, and see who is in one. | Build a segment |
/v1/balance | The credit balance: what can be spent now, and whether the workspace is suspended. | Credits and balance |
/v1/usage | What the credits went on: totals by category, day or label, one row per charge, a CSV export. | Usage and spend |
/v1/compliance | The outbound compliance acknowledgment a workspace’s first campaign needs: read it, accept it. | Run a campaign |
GET /v1/me names the workspace and the key a request runs as, when the key
expires (api_key.expires_at, null when it never does), and the phone
number on the key owner’s profile (user_phone), the number the shared trial
number can call. Delivery
receipts and replies reach you as webhooks or on an event stream: see
Receive webhooks.
One shape for every channel
Section titled “One shape for every channel”These rules hold on the messaging endpoints: /v1/messages, /v1/calls,
/v1/campaigns, /v1/senders, /v1/me, /v1/templates, /v1/agents,
/v1/lists, /v1/contacts, /v1/contact-fields, /v1/conversations,
/v1/segments, /v1/balance, /v1/usage, /v1/compliance,
/v1/webhooks, /v1/events, /v1/media and /v1/recordings. The
OpenAI-compatible routes keep the OpenAI format: Errors.
- Base URL:
https://api.omniocto.comin production; the other environment is in Environments. - Authentication: send a workspace API key as
Authorization: Bearer sk-octo_…. Create one in the dashboard under Settings → API Keys & Webhooks; see Get an API key. A key works in the workspace it was created in. A missing or bad key is401 invalid_api_key, whosedocs_urlis Authentication. - JSON in, JSON out. Send
Content-Type: application/json. Field names are snake_case and every time is an ISO 8601 string. The one exception is not in a body: a webhook delivery’swebhook-timestampheader is Unix seconds, as Standard Webhooks defines it. - Strict bodies and query strings. A field a route does not take is
refused with
400 unknown_field,paramnames it, andhintsuggests the field you most likely meant. A query parameter a route does not take is400 unknown_parameter, the same way, and a parameter sent twice (?limit=1&limit=5) is400 invalid_request(“passlimitonce”). No request-body field and no query parameter is silently dropped, so a misspelledmedianever turns into a text without its picture, and a?limita route does not page by is never ignored. One body field is passed on as it is: a WhatsApp message’stemplate.componentsis Meta’s own array, and its entries are checked against the approved template (422 template_components_mismatch), not against a schema of ours. - Methods. A messaging path called with a method it does not take
(
PUT /v1/campaigns/{id}) is405 method_not_allowed, with anAllowheader listing the methods it does take. A path that does not exist is404 not_found. channelpicks the body.POST /v1/messagesandPOST /v1/campaignstakechannel(smsorwhatsapp, plusvoicefor campaigns), and the rest of the body follows from it. New channels may be added later: ignore values you do not know.- One
statusfield on every message, call and campaign. Each guide lists its values. - One error envelope:
{ "error": { "type", "code", "message", "param", … } }. Branch oncode. See Errors. Idempotency-Keyon the requests that send, call or start something. A retry with the same key does not act twice. See Idempotency.- Lists answer
{ "object": "list", "data", "has_more", "next_cursor" }and page withlimitandafter: Lists and paging.
Lists and paging
Section titled “Lists and paging”Every list answers { "object": "list", "data", "has_more", "next_cursor" },
newest first unless the reference says otherwise (a contact’s numbers and
emails come primary first; custom contact fields in the dashboard’s order).
Pass next_cursor as after for the next page; it is null on the last
page. GET /v1/senders is the one exception: it answers every number at once,
has_more is always false, and it takes neither limit nor after.
| list | limit | default |
|---|---|---|
| most lists | 1 to 200 | 50 |
| a contact’s numbers and emails | 1 to 200 | 100 |
| campaign recipients | 1 to 1,000 | 100 |
| message search | 1 to 100 | 20 |
A limit out of range is 400 invalid_request.
A cursor is opaque and belongs to the list that answered it. Pass it back
as it is, to the same list (the same campaign’s recipients, for a recipients
cursor), with the same filters (channel, to, from, status,
campaign, created_after, created_before, whichever you sent). A filter
written another way that means the same thing (2026-10-16T11:00:00+02:00 for
2026-10-16T09:00:00Z, a pn-… id for its number) keeps the cursor. Anything
else, a cursor of another list or the same list with other filters, is
400 invalid_cursor, with a hint saying which.
How the channels differ
Section titled “How the channels differ”The request shapes are the same; these are the things that are not. Each cell links to, or is, the rule; the guides do not repeat it.
| SMS | voice | ||
|---|---|---|---|
| one send | POST /v1/messages, "channel": "sms" | POST /v1/messages, "channel": "whatsapp", an approved template only | POST /v1/calls: an AI agent works on your mission |
sender in GET /v1/senders | sms.ready; Twilio numbers only for now, texts and campaigns alike (why) | whatsapp.ready | voice.ready. The shared trial number (pfn-…) places single calls only to a phone number on your profile or a teammate’s (GET /v1/me user_phone), never a campaign |
| placeholders | {{name}} in content.text; first_name, last_name, full_name, email and phone fill by themselves | the template’s variable_keys; nothing fills by itself | {{name}} in content.mission; nothing in a mission fills by itself |
| a contact with no value | {{contact.<field>|fallback}} fills it; with no fallback the person is skipped as missing_variable:<name> | skipped the same way | skipped the same way (a variable the mission uses) |
| preview | waits up to 5 s, then may answer building: poll | answers at once | answers at once |
| a start without a preview | refused, 409 preview_required | allowed: checked as a preview would be | allowed, as on WhatsApp |
| the audience | frozen by the preview: the start sends to exactly those people | read again at the start | read again at the start |
| a preview stays usable | 30 minutes, until a newer preview, an edit or a change to what it checked; after that it reads failed with preview_expired, and a start from it is 409 preview_stale | until an edit or a change to what it checked | as on WhatsApp |
| what a preview says it costs | cost_credits, the built snapshot’s cost; balance_credits shows a shortfall | cost_credits: 0.1 credits a recipient | cost_credits is the high end of cost_estimate (low, high, per call, talk minutes) |
| price | per segment (SMS prices) | 0.1 credits a message (pricing) | by the minute (Settings → Billing) |
status | queued, sending, sent, delivered, undelivered, failed, skipped, unconfirmed (meanings) | the same words, plus read, without undelivered (what differs) | queued, ringing, in_progress, completed, voicemail, no_answer, busy, failed (meanings) |
| list filters | GET /v1/messages?channel=sms: to, from, status, campaign, created_after, created_before | ?channel=whatsapp: the same six | GET /v1/calls: the same six |
| events | message.status_updated; message.received (replies) and message.sent (what your agent or team texted) | message.status_updated (with read); message.received (replies, what fires) | call.ringing, call.answered, call.completed, call.failed |
| campaign events | campaign.*, plus campaign.waiting and campaign.window_opened around sending hours | campaign.* | campaign.*, and each call’s call.* |
The preview in detail: preview rules. Placeholders in detail: Variables.
Phone numbers
Section titled “Phone numbers”Every phone number, in a body or an answer, is E.164: +, the country
code and the number, with no spaces or punctuation (+14155550100).
415-555-0100 and +1 415 555 0100 are 400 invalid_number, and nothing is
sent or charged. When the intended number is clear, the hint says
Did you mean +14155550100?; we never send to the suggestion, so send again
with it. In a query string, write the + as %2B
(GET /v1/contacts?phone=%2B14155550100): a bare + reads as a space.
| resource | id | also | every {id} route takes |
|---|---|---|---|
| campaign | the UUID | short_id: ca-… | either |
| message | the UUID | the UUID | |
| call | the UUID | (its conversation’s cv-…) | either |
| agent, contact list, segment, WhatsApp template | ag-…, cl-…, sg-…, wt-… | uuid (agents, lists, segments) | either, where a body names one |
| sender | pn-… (a number), ch-… (a WhatsApp-only number), pfn-… (the shared trial number) | e164, channel_uuids | see from below |
from takes the same forms on every channel: the number’s E.164
(+16285550100), its pn-… id, the ch-… id of one of its channels (the
SMS, voice or WhatsApp channel; whatsapp.channel_id in GET /v1/senders),
or the UUID of the number or the channel. It must be able to send on the
channel you use, or it is 400 invalid_from. A single call also takes the
shared trial number’s pfn-… id.
- Either form works. Wherever a resource has both a short id and a UUID,
every route and body field that names it takes either. A campaign keeps
its UUID as
id(withshort_idbeside it) because events carry that UUID. A message has only its UUID, so a short id in/v1/messages/{id}is400. - The wrong kind is
400wherever an id goes, naming the prefix the field takes and the route that lists them: in a body field (sg-…whereaudience.listwants acl-…), a path (/v1/campaigns/cl-…) or a query parameter (?campaign=cl-…) it isinvalid_request, withparamnaming the field,idor the parameter. Asfrom(cl-…where a number goes) it keepsfrom’s own code,invalid_from. - A right-kind id that does not exist in your workspace is
404.
The shared trial number
Section titled “The shared trial number”The shared trial number (trial: true in GET /v1/senders) places test calls to
a phone number on your profile or a teammate’s, with no card. Its rules:
The trial number.
Testing without spending
Section titled “Testing without spending”There is no sandbox or test mode yet. What is free, what costs a little, and how to test on yourself: Test without spending.
Credits and balance
Section titled “Credits and balance”Sends and calls are paid in credits from the workspace’s balance. The balance,
what each channel costs, the 402 refusals and the usage report:
Billing and credits.
Usage and spend
Section titled “Usage and spend”GET /v1/usage, /v1/usage/events and /v1/usage/export:
Usage and spend.
Limits
Section titled “Limits”| what | limit |
|---|---|
| request body | 256 KB for /v1/messages and /v1/calls; 5 MB of audience plus a small envelope for /v1/campaigns. Over it: 413 payload_too_large |
list page size (limit) | 1 to 200, default 50; campaign recipients 1 to 1,000, default 100 |
| SMS text | 1,600 characters |
call mission | 4,000 characters, for a single call and a campaign |
| campaign audience | 1 to 10,000 inline contacts, or a CSV of up to 5 MB |
| a campaign variable | plain text, a field name or a fallback: 200 characters each; a whole value: 420 |
metadata | string values only; at most 20 keys, each key 1 to 40 characters, each value at most 500 characters (messages, calls, campaigns) |
Idempotency-Key | 1 to 255 printable ASCII characters, kept 24 hours |
Request and sending rates (per key, per workspace, per number) are in Rate limits.
Versioning
Section titled “Versioning”/v1 is stable. Within it we only add: new routes, new optional fields in a
request, new fields in an answer, new status and code values and new
event types. Ignore what you do not know, and branch on code, not on
message. A change that could break working code (removing or renaming a
field, changing what a field means, refusing a request we accepted) comes
with a new version or, for a fix, a notice in the
changelog before it ships. Every change is in the changelog.
Reference
Section titled “Reference”Every endpoint in one page of tables: Reference. Every operation, with its request and response schema and a try-it console: API reference.
The dashboard runs on its own REST API under /api/*. It accepts the same API
key and reaches what the Platform API does not cover, such as uploading an
MMS picture and buying phone numbers:
Dashboard API reference.