Skip to content

Quickstart: send a text, follow it, hear back and start a campaign

Download .md

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.

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.

Put the key and the API’s base URL in two variables. Every request below uses them:

Terminal window
export OMNI_OCTO_API_KEY=sk-octo_…
export OMNI_OCTO_API_URL=https://api.omniocto.com

The other environment’s URLs, and why a key works in one only: Environments.

Terminal window
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.

Terminal window
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.ready for a text, sms.campaigns.ready for an SMS campaign;
  • whatsapp.ready for a WhatsApp template;
  • voice.ready for 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.

From a number whose sms.ready is true, text your own phone:

Terminal window
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.

Terminal window
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.

Terminal window
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.

Rather than poll, have each change POSTed to you. Register a public https URL (with a key of an owner or admin):

Terminal window
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:

Terminal window
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:

Terminal window
# api_version=v2: the format new webhooks get
curl -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.

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.

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.

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:

Terminal window
claude mcp add --transport http omniocto https://api.omniocto.com/mcp

Start 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:

Terminal window
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.

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.