SendaiDocs

Guides

View as Markdown

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.

RequestPOST /api/v1/sms/bulk
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"
  }'

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

Response
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.

Tracking each recipient

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

RequestGET /api/v1/sms/{id}
curl https://api.sendai.co.zw/api/v1/sms/0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10 \
  -H "Authorization: Bearer $SENDAI_API_KEY"

As with a single send, status tracks the pipeline (createdenqueuedprocessingsuccess / failed) and delivered_at is the delivery confirmationnull until the carrier confirms, then a timestamp.

Errors

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

StatusMeaning
400Validation failed — to empty, message or from blank, or webhook_url not a URL. The body lists the offending fields in error.validation_errors.
401Missing, malformed, or revoked API key.
422insufficient balance — your prepaid balance does not cover the send.
Response
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.
  • 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