API reference
SMS
The message resource — every attribute of the message object, then the three operations on it, send, retrieve, and bulk send.
A message is one SMS to one recipient. Sending creates it, the carrier's confirmation
completes it, and its id reads it back at any point in between — a bulk send is simply many
messages created in one call, each with its own id and its own delivery signal.
One convention to hold onto throughout: POST /api/v1/sms returns the message object
bare, while every other operation wraps its payload in the { status, message, data }
envelope — see two response conventions.
The message object
The same object comes back from a send and from a retrieve; only the envelope around it
differs. Fresh from a send it reads status: enqueued, delivered_at: null; the carrier's
confirmation later fills delivered_at.
Attributes
idstring (uuid)Message id. Pass it toGET /api/v1/sms/{id}.tostringRecipient MSISDN in international format.messagestringThe body as sent.sender_idstringDisplay name of the approved sender the recipient saw. Note the name: the field you send isfrom; the field that comes back issender_id.operatorstringCarrier derived from the recipient MSISDN:econet,netone, ortelecel.chargestring | integerAmount billed, in ten-thousandths ofcharge_currency(10000== 1.00, so450== 0.045). Serialised as a string on the single send and retrieve, as an integer on bulk items — same unit, different JSON type. See billing and segments.charge_currencystringCurrency ofcharge.operator_responsestringRaw response from the carrier. Empty until dispatched;DELIVRDis a typical confirmed value.received_atstring (date-time)When Sendai accepted the message.statusstringThe send pipeline:created→enqueued→processing→success, orfailed.successmeans the carrier accepted or confirmed it — it is not the delivery signal; there is no separate delivery-status field.created_atstring (date-time)When the message row was created.delivered_atstring (date-time) | nullnulluntil the carrier confirms handset delivery, then an RFC 3339 timestamp. This is the only delivery signal. A message can sit atstatus: successwithdelivered_at: nullfor a while — normal, not a failure.
{
"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": "DELIVRD",
"received_at": "2026-07-06T09:12:03Z",
"status": "success",
"created_at": "2026-07-06T09:12:03Z",
"delivered_at": "2026-07-06T09:12:44Z"
}
Send an SMS
/api/v1/smsSends one message. Sendai validates the sender ID, prices the message per segment, places a
hold on your prepaid balance, and answers 202 Accepted — accepted, priced, and queued,
not delivered. Retrieve the message or register a
webhook for the delivery signal.
Request body
tostringrequiredRecipient MSISDN in international format, e.g.263771000000.fromstringrequiredAn approved sender ID on your account — list yours with Sender IDs. An unapproved value is rejected with400 unknown or unapproved sender id.messagestringrequiredMessage body. Billed per GSM-7/UCS-2 segment — see billing and segments.
Response
202 returns the message object bare — this is the one endpoint
with no { status, message, data } envelope, so there is no data to unwrap. Two fields to
note:
- The sender comes back as
sender_id, notfrom. chargeis a string here ("450"== 0.045charge_currency); the bulk operation returns the same value as an integer.
status: enqueued and delivered_at: null are the normal state of a fresh send — delivery
confirmation arrives asynchronously.
Errors
| Status | When |
|---|---|
400 | Malformed JSON, or a from that is not an approved sender ID. Body carries error.type. |
401 | Missing, malformed, or revoked API key. Body carries error.code. |
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": "Sendai SMS",
"message": "Your code is 4821"
}'
{
"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
}
Retrieve an SMS
/api/v1/sms/{id}Reads back one message by the id from the send response. Unlike the send operation, this
response is enveloped — the message sits under data.
Path parameters
idstring (uuid)required
Response
Two fields track the journey, and they are two separate facts:
data.status— the send pipeline:created→enqueued→processing→success, orfailed.data.delivered_at—nulluntil the carrier confirms delivery, then a timestamp. This is the only delivery signal.
A message can sit at status: success with delivered_at: null for a while — the pipeline
finished but the carrier has not confirmed yet. That is normal, not a failure. Carrier
confirmation takes seconds to hours, so poll on an interval, or let a
webhook call you instead.
operator_response is the raw carrier response — empty until dispatched.
Errors
| Status | When |
|---|---|
400 | id is not a valid UUID. Body carries error.type. |
401 | Missing, malformed, or revoked API key. Body carries error.code. |
404 | No such message on your account — unknown id, another account's message, or not an SMS. |
curl https://api.sendai.co.zw/api/v1/sms/0f9c1b3a-6d2e-4a1b-9c3d-7e5f2a8b4c10 \
-H "Authorization: Bearer $SENDAI_API_KEY"
{
"status": "success",
"message": "request successful",
"data": {
"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": "DELIVRD",
"received_at": "2026-07-06T09:12:03Z",
"status": "success",
"created_at": "2026-07-06T09:12:03Z",
"delivered_at": "2026-07-06T09:12:44Z"
}
}
Send bulk SMS
/api/v1/sms/bulkSends the same body to every recipient in to, priced as one send. The whole send is
accepted or rejected together — there is no partial success. Returns one entry per recipient,
in request order; each items[].id behaves exactly like a single-send id, so you track
each recipient with Retrieve an SMS.
Request body
toarray of stringsrequiredRecipient MSISDNs in international format. At least one. A repeated MSISDN is not collapsed — the recipient gets two messages and you are billed for both.fromstringrequiredAn approved sender ID on your account — list yours with Sender IDs.messagestringrequiredBody sent to every recipient. Billed per segment, per recipient.webhook_urlstring (url)Optional callback applied to every message in the send. Must be a valid URL. See Webhooks for the account-level alternative.
Response
This response is enveloped. data.charge is the total held for the send and
data.items[].charge what each recipient cost — operators price independently, so entries
within one send can differ.
charge is an integer here, unlike the string the
single send returns. Both are in ten-thousandths of the currency
(450 == 0.045); only the JSON type differs. Note the total's currency arrives as
currency, not charge_currency.
Errors
| Status | When |
|---|---|
400 | Validation failed — to empty, message or from blank, or webhook_url not a URL. Offending fields are listed in error.validation_errors. |
401 | Missing, malformed, or revoked API key. |
422 | insufficient balance — your prepaid balance does not cover the send. Nothing is sent. Check Balances first. |
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": "Sendai SMS",
"message": "Polls close at 18:00. Thanks for taking part!",
"webhook_url": "https://example.com/webhooks/dlr"
}'
{
"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"
}
]
}
}