# Bulk SMS

> Send one SMS body to a list of recipients in a single call with POST /api/v1/sms/bulk, and track each recipient's message individually.

`POST /api/v1/sms/bulk` sends the same body to every recipient in one call, prices the whole
send up front, and returns **one message id per recipient**. Those ids behave exactly like
single-send ids, so you track each recipient with `GET /api/v1/sms/{id}`.

## Sending to a list

`to` is an array of MSISDNs in international format; `message` and `from` apply to every
recipient.

```bash [cURL]
curl -X POST https://api.sendai.co.zw/api/v1/sms/bulk \
  -H "Authorization: Bearer $SENDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["263771000000", "263772000001"],
    "from": "YOUR_SENDER_ID",
    "message": "Polls close at 18:00. Thanks for taking part!",
    "webhook_url": "https://example.com/webhooks/dlr"
  }'
```

```js [Node.js]
const res = await fetch('https://api.sendai.co.zw/api/v1/sms/bulk', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.SENDAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: ['263771000000', '263772000001'],
    from: 'YOUR_SENDER_ID',
    message: 'Polls close at 18:00. Thanks for taking part!',
    webhook_url: 'https://example.com/webhooks/dlr',
  }),
})

const { data } = await res.json() // enveloped — items under data.items
```

```python [Python]
import os
import requests

res = requests.post(
    "https://api.sendai.co.zw/api/v1/sms/bulk",
    headers={"Authorization": f"Bearer {os.environ['SENDAI_API_KEY']}"},
    json={
        "to": ["263771000000", "263772000001"],
        "from": "YOUR_SENDER_ID",
        "message": "Polls close at 18:00. Thanks for taking part!",
        "webhook_url": "https://example.com/webhooks/dlr",
    },
)

data = res.json()["data"]  # enveloped — items under data["items"]
```

Unlike the single-send endpoint, this response **is** enveloped — the payload sits under
`data`, with one entry per recipient **in request order**:

```json [202 Accepted]
{
  "status": "success",
  "message": "bulk sms enqueued",
  "data": {
    "total": 2,
    "charge": 900,
    "currency": "USD",
    "items": [
      {
        "id": "0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10",
        "to": "263771000000",
        "status": "enqueued",
        "operator": "econet",
        "charge": 450,
        "charge_currency": "USD"
      },
      {
        "id": "7e5f2a8b-4c10-4a1b-9c3d-0f9c1b3a6d2e",
        "to": "263772000001",
        "status": "enqueued",
        "operator": "netone",
        "charge": 450,
        "charge_currency": "USD"
      }
    ]
  }
}
```

Store each `items[].id` against your own recipient record — that id is how you check delivery
later.

> **`charge` is an integer here.** The single-send endpoint returns it as a string. Both are
> in ten-thousandths of the currency (`450` == 0.045); only the JSON type differs.

Per-recipient `charge` values can differ within one send: operators are priced independently,
so an Econet and a NetOne recipient may not cost the same. `data.charge` is the total held.

The optional `webhook_url` is validated as a URL and applied to every message in the send.
For delivery callbacks on your whole account rather than one send, see
[Webhooks](/guides/webhooks).

## Tracking each recipient

There is no batch-level status endpoint on this surface — you poll the individual message ids:

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

As with a single send, `status` tracks the pipeline (`created` → `enqueued` → `processing` →
`success` / `failed`) and **`delivered_at` is the delivery confirmation** — `null` until the
carrier confirms, then a timestamp.

## Errors

The whole send is accepted or rejected together; there is no partial success.

| Status | Meaning |
| --- | --- |
| `400` | Validation failed — `to` empty, `message` or `from` blank, or `webhook_url` not a URL. The body lists the offending fields in `error.validation_errors`. |
| `401` | Missing, malformed, or revoked API key. |
| `422` | `insufficient balance` — your prepaid balance does not cover the send. |

```json [400 Bad Request]
{
  "status": "error",
  "message": "Validation failed",
  "error": {
    "code": "VALIDATION_ERROR",
    "description": "One or more fields failed validation",
    "validation_errors": [
      { "field": "from", "description": "cannot be blank" }
    ]
  }
}
```

## Getting bulk sends right

- **Watch the encoding.** One non-GSM-7 character switches the whole body to UCS-2 and roughly
  halves the characters per segment. Across 50,000 recipients that is a large and avoidable
  bill — see [billing and segments](/api#billing-and-segments).
- **Deduplicate before you send.** A repeated MSISDN is not collapsed; the recipient gets two
  messages and you are billed for both.
- **Normalise to international format** (`263771000000`). A number that does not resolve to an
  operator is a message you paid to have rejected.
- **Make retries safe.** If a request times out, do not blindly resend — the send may well have
  been accepted. Reconcile against the returned ids first.
- **Poll on an interval, not in a tight loop.** Delivery settles over minutes to hours, bounded
  by how fast carriers confirm.

## Next steps

- Send a single message → [Quickstart](/guides/quickstart)
- Get delivery pushed instead of polling → [Webhooks](/guides/webhooks)
- Check your balance before a big send → [Balances](/api/balances)
- Every field and response → [Send bulk SMS](/api/sms#send-bulk-sms)
