Skip to content

Platform API (/v1) overview

Download .md

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.

Terminal window
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 } }
toguidefieldsfirst success
go from a key to a text, a webhook and a campaignQuickstartabout ten requests
hear an AI agent call youCall your own phoneCallsabout 3 minutes, no card
send a textSend a textMessagesafter carrier registration, 1 to 3 weeks
send a WhatsApp templateSend a WhatsApp templateMessagesonce a number and a template are approved
reach a whole listRun a campaignCampaignsa one-person test first
keep your contacts, tags and opt-outsKeep contactsContactsright away
read and answer your inboxWork the inboxConversationsright away
hear back: deliveries, replies, callsReceive webhooksWebhooksminutes, with a test event
try it all without spendingTest without spendingright away
drive it from Claude Code or another MCP clientAPI tools (MCP), call campaigns from Claude Codeone command

What every code means: Errors. Every field of every endpoint: Reference.

environmentAPI (/v1)dashboard (keys, numbers, billing)
productionhttps://api.omniocto.comhttps://agent.omniocto.com
developmenthttps://api.agent-dev.omniocto.comhttps://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.
  • /v1 answers only on the API host. The same path on a dashboard host is 404.
  • The examples use production. To run them elsewhere, put the base URL in a variable and write $OMNI_OCTO_API_URL where they write https://api.omniocto.com, as the quickstart does.
resourcewhat it doesguide
/v1/messagesSend one SMS/MMS or one approved WhatsApp template, read its status, list what you sent.Send a text, Send a WhatsApp template
/v1/callsAn 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/campaignsSend texts, WhatsApp templates or AI calls to many people: create, preview, start, pause, watch.Run a campaign
/v1/sendersThe numbers you can send and call from, and whether each one is ready on each channel.Send a text
/v1/templatesYour WhatsApp templates: the wt-… id to send, the number each sends from, and what each needs filled in.Send a WhatsApp template
/v1/agentsYour AI agents and their ag-… ids, for calls and voice campaigns.Place a call
/v1/listsContact lists (cl-…) for a campaign’s audience.list: create one with its contacts, add more, take one out, delete it.Run a campaign
/v1/contactsThe 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-fieldsThe custom contact fields, the keys of a contact’s attributes.Keep contacts
/v1/conversationsYour 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/segmentsSaved 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/balanceThe credit balance: what can be spent now, and whether the workspace is suspended.Credits and balance
/v1/usageWhat the credits went on: totals by category, day or label, one row per charge, a CSV export.Usage and spend
/v1/complianceThe 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.

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.com in 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 is 401 invalid_api_key, whose docs_url is 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’s webhook-timestamp header 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, param names it, and hint suggests the field you most likely meant. A query parameter a route does not take is 400 unknown_parameter, the same way, and a parameter sent twice (?limit=1&limit=5) is 400 invalid_request (“pass limit once”). No request-body field and no query parameter is silently dropped, so a misspelled media never turns into a text without its picture, and a ?limit a route does not page by is never ignored. One body field is passed on as it is: a WhatsApp message’s template.components is 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}) is 405 method_not_allowed, with an Allow header listing the methods it does take. A path that does not exist is 404 not_found.
  • channel picks the body. POST /v1/messages and POST /v1/campaigns take channel (sms or whatsapp, plus voice for campaigns), and the rest of the body follows from it. New channels may be added later: ignore values you do not know.
  • One status field on every message, call and campaign. Each guide lists its values.
  • One error envelope: { "error": { "type", "code", "message", "param", … } }. Branch on code. See Errors.
  • Idempotency-Key on 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 with limit and after: 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.

listlimitdefault
most lists1 to 20050
a contact’s numbers and emails1 to 200100
campaign recipients1 to 1,000100
message search1 to 10020

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.

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.

SMSWhatsAppvoice
one sendPOST /v1/messages, "channel": "sms"POST /v1/messages, "channel": "whatsapp", an approved template onlyPOST /v1/calls: an AI agent works on your mission
sender in GET /v1/senderssms.ready; Twilio numbers only for now, texts and campaigns alike (why)whatsapp.readyvoice.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 themselvesthe 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 wayskipped the same way (a variable the mission uses)
previewwaits up to 5 s, then may answer building: pollanswers at onceanswers at once
a start without a previewrefused, 409 preview_requiredallowed: checked as a preview would beallowed, as on WhatsApp
the audiencefrozen by the preview: the start sends to exactly those peopleread again at the startread again at the start
a preview stays usable30 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_staleuntil an edit or a change to what it checkedas on WhatsApp
what a preview says it costscost_credits, the built snapshot’s cost; balance_credits shows a shortfallcost_credits: 0.1 credits a recipientcost_credits is the high end of cost_estimate (low, high, per call, talk minutes)
priceper segment (SMS prices)0.1 credits a message (pricing)by the minute (Settings → Billing)
statusqueued, 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 filtersGET /v1/messages?channel=sms: to, from, status, campaign, created_after, created_before?channel=whatsapp: the same sixGET /v1/calls: the same six
eventsmessage.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 eventscampaign.*, plus campaign.waiting and campaign.window_opened around sending hourscampaign.*campaign.*, and each call’s call.*

The preview in detail: preview rules. Placeholders in detail: Variables.

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.

resourceidalsoevery {id} route takes
campaignthe UUIDshort_id: ca-…either
messagethe UUIDthe UUID
callthe UUID(its conversation’s cv-…)either
agent, contact list, segment, WhatsApp templateag-…, cl-…, sg-…, wt-…uuid (agents, lists, segments)either, where a body names one
senderpn-… (a number), ch-… (a WhatsApp-only number), pfn-… (the shared trial number)e164, channel_uuidssee 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 (with short_id beside it) because events carry that UUID. A message has only its UUID, so a short id in /v1/messages/{id} is 400.
  • The wrong kind is 400 wherever an id goes, naming the prefix the field takes and the route that lists them: in a body field (sg-… where audience.list wants a cl-…), a path (/v1/campaigns/cl-…) or a query parameter (?campaign=cl-…) it is invalid_request, with param naming the field, id or the parameter. As from (cl-… where a number goes) it keeps from’s own code, invalid_from.
  • A right-kind id that does not exist in your workspace is 404.

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.

There is no sandbox or test mode yet. What is free, what costs a little, and how to test on yourself: Test without spending.

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.

GET /v1/usage, /v1/usage/events and /v1/usage/export: Usage and spend.

whatlimit
request body256 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 text1,600 characters
call mission4,000 characters, for a single call and a campaign
campaign audience1 to 10,000 inline contacts, or a CSV of up to 5 MB
a campaign variableplain text, a field name or a fallback: 200 characters each; a whole value: 420
metadatastring values only; at most 20 keys, each key 1 to 40 characters, each value at most 500 characters (messages, calls, campaigns)
Idempotency-Key1 to 255 printable ASCII characters, kept 24 hours

Request and sending rates (per key, per workspace, per number) are in Rate limits.

/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.

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.