Quickstart: send a text, follow it, hear back and start a campaign
Go from an API key to a text you can follow, a webhook that tells you about it, and a campaign to one person: you. Every step is one curl request.
What takes waiting: a number can text only after carrier registration, usually 1 to 3 weeks (Get a number cleared to text), so start that first. A call to your own phone from the shared trial number needs nothing of the kind: step 10.
1. Get an API key
Section titled “1. Get an API key”Create a key in the dashboard under Settings → API Keys & Webhooks:
Get an API key. It starts sk-octo_, and it
works in the workspace and the environment it was created in.
2. Pick your environment
Section titled “2. Pick your environment”Put the key and the API’s base URL in two variables. Every request below uses them:
export OMNI_OCTO_API_KEY=sk-octo_…export OMNI_OCTO_API_URL=https://api.omniocto.comThe other environment’s URLs, and why a key works in one only: Environments.
3. Check the key
Section titled “3. Check the key”curl "$OMNI_OCTO_API_URL/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 } }workspace is where everything you do lands, and user_phone is the
number on your profile: the phone you will text and call below. null
means your profile has none yet; add it under
Account. A 401 invalid_api_key is a
wrong, revoked or expired key, or a key of the other environment.
4. Find your numbers
Section titled “4. Find your numbers”curl "$OMNI_OCTO_API_URL/v1/senders" -H "Authorization: Bearer $OMNI_OCTO_API_KEY"{ "object": "list", "has_more": false, "next_cursor": null, "data": [ { "id": "pn-0000000002", "object": "sender", "e164": "+16285550100", "label": "Front desk", "trial": false, "mode": "managed", "sms": { "ready": true, "status": "cleared", "fix": null, "provider": "twilio", "campaigns": { "ready": true, "status": "cleared", "fix": null } }, "whatsapp": null, "voice": { "ready": true, "status": "ready", "fix": null }, "channel_uuids": ["4e1d…", "8a07…"] }, { "id": "pfn-0000000001", "object": "sender", "e164": "+16285550199", "label": null, "trial": true, "mode": "managed", "sms": null, "whatsapp": null, "voice": { "ready": false, "status": "trial_agent_not_chosen", "fix": "Choose which agent calls from the trial number: open Phone numbers in the dashboard (https://agent.omniocto.com/…), pick the trial number and choose an agent." }, "channel_uuids": [] } ] }Each number says, per channel, whether it can be used now:
sms.readyfor a text,sms.campaigns.readyfor an SMS campaign;whatsapp.readyfor a WhatsApp template;voice.readyfor a call.
When one is false, its fix says what to do. The second row is the
shared trial number, which only calls (the trial number).
What each sms.status means: Pick a from number.
5. Send a text
Section titled “5. Send a text”From a number whose sms.ready is true, text your own phone:
curl -X POST "$OMNI_OCTO_API_URL/v1/messages" \ -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "channel": "sms", "from": "+16285550100", "to": "+14155550123", "text": "Hello from the Omni Octo API" }'{ "id": "00000000-0000-4000-8000-000000000015", "object": "message", "channel": "sms", "from": "+16285550100", "to": "+14155550123", "status": "queued", "billed_credits": null, "created_at": "2026-10-15T17:02:11.000Z" }(Shortened; every field: Send a text.)
The answer is 202 Accepted: the text is on its way. Keep the
Idempotency-Key: if the request times out, send it again with the same
key, and the text still goes out once.
6. Follow its status
Section titled “6. Follow its status”curl "$OMNI_OCTO_API_URL/v1/messages/00000000-0000-4000-8000-000000000015" \ -H "Authorization: Bearer $OMNI_OCTO_API_KEY"status moves from queued through sending and sent to delivered,
usually within seconds, and billed_credits then says what it cost. Any
other final word (undelivered, failed) comes with error.category
saying why: Read a text’s status.
7. Find it in the list
Section titled “7. Find it in the list”curl "$OMNI_OCTO_API_URL/v1/messages?channel=sms&limit=2" -H "Authorization: Bearer $OMNI_OCTO_API_KEY"{ "object": "list", "data": [ { "id": "00000000-0000-4000-8000-000000000015", "status": "delivered" } ], "has_more": false, "next_cursor": null }Your text is first: lists are newest first. With more than limit items,
has_more is true, and you pass next_cursor back as after for the
next page, with the same filters. How every list pages:
Lists and paging.
8. Hear back
Section titled “8. Hear back”Rather than poll, have each change POSTed to you. Register a public https
URL (with a key of an owner or admin):
curl -X POST "$OMNI_OCTO_API_URL/v1/webhooks" \ -H "Authorization: Bearer $OMNI_OCTO_API_KEY" -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/octo", "event_types": ["message.status_updated", "message.received"] }'Store the id (whe-…) and the secret from the answer: the secret is
shown only once. Then have one signed sample event sent right away:
curl -X POST "$OMNI_OCTO_API_URL/v1/webhooks/whe-0000000001/test" -H "Authorization: Bearer $OMNI_OCTO_API_KEY"It answers with what your receiver said (ok, status). With no public
URL yet, read the same events as a stream instead:
# api_version=v2: the format new webhooks getcurl -N "$OMNI_OCTO_API_URL/v1/events?api_version=v2&types=message.status_updated" \ -H "Authorization: Bearer $OMNI_OCTO_API_KEY"Checking the signature, retries and the stream’s cursor: Receive webhooks.
9. A campaign: draft, preview, start
Section titled “9. A campaign: draft, preview, start”A campaign sends one message to a whole audience: you make a list, create a
draft with "start": "draft", preview it (which sends nothing), accept the
compliance acknowledgment once, and start exactly what you previewed. Try it
on a list of just your own number first:
Run a campaign.
10. Optional: call your own phone
Section titled “10. Optional: call your own phone”An AI agent can call you from the shared trial number, with no card and no registration, once you choose its agent in the dashboard (the trial number): Call your own phone.
Prefer an agent? Use MCP
Section titled “Prefer an agent? Use MCP”Claude Code, Codex or any MCP client can make the requests above for you at
https://api.omniocto.com/mcp: each API tool it finds and runs makes one of them. Add the
server and sign in with your Omni Octo account; no key needed:
claude mcp add --transport http omniocto https://api.omniocto.com/mcpStart Claude Code; your browser opens (or type /mcp and choose
Authenticate). Choose the workspace and click Allow. To use the key
from step 1 instead, for a script or CI:
claude mcp add --transport http omniocto https://api.omniocto.com/mcp \ --header "Authorization: Bearer $OMNI_OCTO_API_KEY"Then ask Claude Code “which numbers can I send from?”. Codex and other clients: API tools. A whole campaign loop: Call campaigns from Claude Code or Codex.
If something is refused
Section titled “If something is refused”Every refusal is one envelope with a stable error.code, a message to
show and usually a hint. Look the code up in Errors.
The ones a first run meets most: 401 invalid_api_key (step 3),
422 sender_not_cleared (the number is not registered yet),
409 outbound_compliance_required (step 9) and 402 insufficient_credits.
- How the channels differ: previews, placeholders, statuses and costs, side by side.
- Send a WhatsApp template and Place a call: the other two channels.
- Test without spending: what each step costs.