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

> From an API key to a text, its delivery status, a webhook and a one-person campaign, with curl, in about ten requests. A call to your own phone needs no card and no waiting.

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](/platform-api/sms/#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](#call-your-own-phone-optional).

## 1. Get an API key

Create a key in the dashboard under Settings → API Keys & Webhooks:
[Get an API key](/getting-started/api-keys/). It starts `sk-octo_`, and it
works in the workspace and the environment it was created in.

## 2. Pick your environment

<a id="environments"></a>

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

```bash
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](/platform-api/overview/#environments).

## 3. Check the key

<a id="check-the-key"></a><a id="2-check-the-key"></a>

```bash
curl "$OMNI_OCTO_API_URL/v1/me" -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
```

```json
{ "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](https://agent.omniocto.com/account). A `401 invalid_api_key` is a
wrong, revoked or expired key, or a key of the other environment.

## 4. Find your numbers

<a id="find-your-numbers"></a><a id="3-find-the-trial-number"></a>

```bash
curl "$OMNI_OCTO_API_URL/v1/senders" -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
```

```json
{ "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](/platform-api/voice/#the-trial-number)).
What each `sms.status` means: [Pick a `from` number](/platform-api/sms/#pick-a-from-number).

## 5. Send a text

<a id="send-a-text"></a><a id="next-send-a-text"></a>

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

```bash
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" }'
```

```json
{ "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](/platform-api/sms/#send-one-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

<a id="follow-status"></a>

```bash
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](/platform-api/sms/#read-a-texts-status).

## 7. Find it in the list

<a id="list"></a>

```bash
curl "$OMNI_OCTO_API_URL/v1/messages?channel=sms&limit=2" -H "Authorization: Bearer $OMNI_OCTO_API_KEY"
```

```json
{ "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](/platform-api/overview/#lists-and-paging).

## 8. Hear back

<a id="webhooks"></a>

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

```bash
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:

```bash
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:

```bash
# 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](/platform-api/events/).

## 9. A campaign: draft, preview, start

<a id="campaign"></a>

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](/platform-api/campaigns/#draft-preview-start).

## 10. Optional: call your own phone

<a id="call-your-own-phone-optional"></a><a id="4-place-the-call"></a><a id="5-see-how-it-went"></a>

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](/platform-api/voice/#the-trial-number)):
[Call your own phone](/platform-api/voice/#call-your-own-phone).

## Prefer an agent? Use MCP

<a id="mcp"></a>

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:

```bash
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:

```bash
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](/mcp/api-tools/#connect-codex). A whole campaign loop:
[Call campaigns from Claude Code or Codex](/mcp/claude-code-campaigns/).

## If something is refused

<a id="if-the-call-is-refused"></a>

Every refusal is one envelope with a stable `error.code`, a `message` to
show and usually a `hint`. Look the code up in [Errors](/platform-api/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`.

## Next

- [How the channels differ](/platform-api/overview/#how-the-channels-differ):
  previews, placeholders, statuses and costs, side by side.
- [Send a WhatsApp template](/platform-api/whatsapp/) and
  [Place a call](/platform-api/voice/): the other two channels.
- [Test without spending](/platform-api/testing/): what each step costs.