# Quickstart

> Send your first SMS with the Sendai API — confirm your sender ID, get an API key, POST a message, and poll it until the carrier confirms delivery.

This guide takes you from zero to a delivered SMS: confirm your sender ID, get an API key,
send a message, and confirm it arrived. Every request uses the base URL
`https://api.sendai.co.zw`.

## Before you send: your sender ID

The `from` on every message must be a **sender ID already approved on your account** — an
unapproved value is rejected with `400 unknown or unapproved sender id`, so this is worth 30
seconds before your first call.

You captured your sender ID (up to 3, each 3–11 letters or numbers) when you registered.
Approval runs on the network-operator side and usually takes **about 30 minutes** — check its
status in the dashboard under **Settings**, or list your identities over the API with
[Sender IDs](/api/sender-ids) and use any `address` whose `verification_status` is
`verified`.

Two more things while you wait: accounts are **prepaid**, so top up your balance in the
dashboard before the first send; and everywhere below, replace `YOUR_SENDER_ID` with your
approved sender ID.

## 1. Get an API key

Create your first key in the dashboard under **Settings → Developer**. Once you hold one, you
can mint further keys over the API — one per service is good practice:

```bash [cURL]
curl -X POST https://api.sendai.co.zw/api/v1/api-tokens \
  -H "Authorization: Bearer $SENDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "orders-service" }'
```

```js [Node.js]
const res = await fetch('https://api.sendai.co.zw/api/v1/api-tokens', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.SENDAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'orders-service' }),
})

const { data } = await res.json()
// data.token is shown only in this response — store it now
```

```python [Python]
import os
import requests

res = requests.post(
    "https://api.sendai.co.zw/api/v1/api-tokens",
    headers={"Authorization": f"Bearer {os.environ['SENDAI_API_KEY']}"},
    json={"name": "orders-service"},
)

data = res.json()["data"]
# data["token"] is shown only in this response — store it now
```

The response contains the key **once** — store it securely; it cannot be retrieved again.

```json [201 Created]
{
  "status": "success",
  "message": "api token created — save this token, it will not be shown again",
  "data": {
    "id": "9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b",
    "name": "orders-service",
    "token": "sk_live_4c8e6b21d0f34a97b5e8c1d2f6a3b904",
    "account_id": "2d6f4b8e-1a90-4e21-8b7d-3f1a5c2e9a0c",
    "created_at": "2026-07-06T09:12:03Z"
  }
}
```

Export it as `SENDAI_API_KEY` and it authenticates every call below. For rotation and safe
handling, see [Authentication](/guides/authentication).

## 2. Send an SMS

`to` is an MSISDN in international format, `from` is your approved sender ID, and `message`
is the text body.

```bash [cURL]
curl -X POST https://api.sendai.co.zw/api/v1/sms \
  -H "Authorization: Bearer $SENDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "263771000000",
    "from": "YOUR_SENDER_ID",
    "message": "Your code is 4821"
  }'
```

```js [Node.js]
const res = await fetch('https://api.sendai.co.zw/api/v1/sms', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.SENDAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: '263771000000',
    from: 'YOUR_SENDER_ID',
    message: 'Your code is 4821',
  }),
})

const sms = await res.json() // bare message object — no data wrapper
```

```python [Python]
import os
import requests

res = requests.post(
    "https://api.sendai.co.zw/api/v1/sms",
    headers={"Authorization": f"Bearer {os.environ['SENDAI_API_KEY']}"},
    json={
        "to": "263771000000",
        "from": "YOUR_SENDER_ID",
        "message": "Your code is 4821",
    },
)

sms = res.json()  # bare message object — no data wrapper
```

Sendai validates and prices the message, holds the charge against your balance, and returns
`202 Accepted`. (Responses on this page show an account whose approved sender ID is
`Sendai SMS`.)

```json [202 Accepted]
{
  "id": "0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10",
  "to": "263771000000",
  "message": "Your code is 4821",
  "sender_id": "Sendai SMS",
  "operator": "econet",
  "charge": "450",
  "charge_currency": "USD",
  "operator_response": "",
  "received_at": "2026-07-06T09:12:03Z",
  "status": "enqueued",
  "created_at": "2026-07-06T09:12:03Z",
  "delivered_at": null
}
```

> **This response is not enveloped.** `POST /api/v1/sms` returns the message object as the
> whole body — there is no `data` to unwrap. Every *other* endpoint does wrap its payload. See
> [two response conventions](/api#two-response-conventions).

Two more things to note: the sender comes back as **`sender_id`**, not `from`; and `charge` is
a **string** on this endpoint, in ten-thousandths of `charge_currency` (`"450"` == 0.045). See
[billing and segments](/api#billing-and-segments).

## 3. Confirm it arrived

Poll `GET /api/v1/sms/{id}` with the `id` from the send response. This response **is**
enveloped, so the message sits under `data`:

```bash [cURL]
curl https://api.sendai.co.zw/api/v1/sms/0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10 \
  -H "Authorization: Bearer $SENDAI_API_KEY"
```

```js [Node.js]
const id = '0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10'

const res = await fetch(`https://api.sendai.co.zw/api/v1/sms/${id}`, {
  headers: { 'Authorization': `Bearer ${process.env.SENDAI_API_KEY}` },
})

const { data: sms } = await res.json() // enveloped — the message is under data
```

```python [Python]
import os
import requests

sms_id = "0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10"

res = requests.get(
    f"https://api.sendai.co.zw/api/v1/sms/{sms_id}",
    headers={"Authorization": f"Bearer {os.environ['SENDAI_API_KEY']}"},
)

sms = res.json()["data"]  # enveloped — the message is under data
```

```json [200 OK]
{
  "status": "success",
  "message": "request successful",
  "data": {
    "id": "0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10",
    "status": "success",
    "operator_response": "DELIVRD",
    "delivered_at": "2026-07-06T09:12:44Z"
  }
}
```

**`delivered_at` is the delivery signal.** `status` moves `created` → `enqueued` →
`processing` → `success` (or `failed`) and tells you the send pipeline finished;
`delivered_at` going from `null` to a timestamp is the carrier confirming it reached the
handset. A message can sit at `status: success` with `delivered_at: null` for a while — that
is normal, not a failure.

Carrier confirmation is asynchronous and can take seconds to hours, so poll on an interval
rather than in a tight loop — or skip polling entirely and register a
[webhook](/guides/webhooks).

## Errors

Branch on the HTTP status code:

| Status | Meaning |
| --- | --- |
| `400` | Malformed JSON, an invalid id, or a `from` that is not an approved sender ID. |
| `401` | Missing, malformed, or revoked API key. |
| `404` | No such message on your account. |
| `422` | Your prepaid balance does not cover the send (bulk endpoint). |

Note that the two error bodies differ: `POST /api/v1/sms` and `GET /api/v1/sms/{id}` return
`error.type`, everything else returns `error.code`. See [errors](/api#errors).

## Next steps

- Send to many recipients at once → [Bulk SMS](/guides/bulk-sms)
- Get delivery pushed to you → [Webhooks](/guides/webhooks)
- Let an AI agent send from your account → [AI agents](/guides/mcp)
- Every endpoint, request, and response → [API reference](/api)
