Bulk SMS
POST /api/messages/send-bulk - Send to a contact list or an inline batch, with scheduling and drip.
Endpoint
POST https://sms.esmsafrica.io/api/messages/send-bulkSend one message to many recipients in a single call. Supply recipients either
inline (recipients) or by referencing saved contact lists
(contact_list_ids). The message body can carry {{variable}} placeholders that
are filled per recipient. The call returns a batch_id immediately and delivery
happens in the background - track it with Batch status.
Authentication
Authorization: Bearer esms_live_your_api_keyRequest body
{
"recipients": [
{ "to": "+254712345678", "name": "Amina", "vars": { "code": "4821" } },
{ "to": "+256772123456", "name": "Brian", "vars": { "code": "9930" } }
],
"text": "Hi {{name}}, your code is {{code}}.",
"sender_id": "MyApp",
"schedule_mode": "now"
}| Field | Type | Required | Description |
|---|---|---|---|
recipients | array | Yes* | Inline list of { to, name?, vars? }. vars fills {{key}} placeholders; {{name}} and {{phone}} are always available. |
contact_list_ids | array of int | Yes* | Saved contact list IDs to send to instead of (or as well as) recipients. |
text | string | Yes | Message body. Supports {{variable}} substitution; rendered per recipient and billed per segment. |
sender_id | string | No | Approved sender ID. Must be approved for each destination country, else it falls back to the route default. |
route | string | No | Force a public route code (e.g. ESMS_UG); otherwise auto-detected per number. |
schedule_mode | string | No | now (default), scheduled, or drip. |
scheduled_at | string | No | ISO 8601 UTC time, required when schedule_mode is scheduled (5 min - 7 days ahead). |
drip_rate | integer | No | Messages per minute, required when schedule_mode is drip. |
* Provide at least one of recipients or contact_list_ids.
Duplicate numbers are collapsed, and anyone on your opt-out (STOP) list is dropped before sending.
Response
{
"batch_id": "b6c2f1e0-1d2c-4a55-9b3f-8e6b0c2d4a11",
"total_recipients": 2,
"estimated_cost": 0.0192,
"status": "sending"
}| Field | Description |
|---|---|
batch_id | Pass to Batch status or the batch_id filter on List messages. |
total_recipients | Unique, non-opted-out recipients after deduplication. |
estimated_cost | Total charged to your wallet, in your wallet currency. |
status | sending, scheduled, or dripping depending on schedule_mode. |
Errors
| Status | Description |
|---|---|
400 | No recipients, missing text, or an unroutable country. |
402 | Insufficient balance (required, available, segments, recipients). |
403 | Sender ID not approved for a destination country. |
413 | A rendered message exceeds the segment cap (max_segments). |
Example
curl -X POST https://sms.esmsafrica.io/api/messages/send-bulk \
-H "Authorization: Bearer esms_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "to": "+254712345678", "name": "Amina", "vars": { "code": "4821" } }
],
"text": "Hi {{name}}, your code is {{code}}.",
"sender_id": "MyApp"
}'Pre-check a bulk send
POST https://sms.esmsafrica.io/api/messages/bulk-precheckRuns the same recipient gathering, deduplication and opt-out suppression as
send-bulk, but only returns a cost estimate - nothing is sent or charged. Use
it to preview spend and per-country reach before committing.
Request body
Same as send-bulk but only contact_list_ids/recipients, text and
sender_id are read:
{
"recipients": [{ "to": "+254712345678" }, { "to": "+256772123456" }],
"text": "Hi {{name}}, your code is {{code}}."
}Response
{
"total_contacts": 2,
"total_segments": 2,
"total_cost": 0.0192,
"wallet_currency": "USD",
"wallet_balance": 12.50,
"balance_after": 12.4808,
"sufficient_balance": true,
"countries": 2,
"breakdown": [
{
"country_code": "KE",
"country_name": "Kenya",
"currency": "KES",
"route_code": "ESMS_KE",
"contacts": 1,
"segments": 1,
"route_cost": 1.2,
"wallet_cost": 0.0096,
"price_per_segment": 1.2
}
],
"unroutable": [],
"unroutable_count": 0,
"suppressed_count": 0
}| Field | Description |
|---|---|
breakdown[] | Per-country reach, segments and cost (route + wallet currency). |
balance_after | Your wallet balance if you sent this batch now. |
suppressed_count | Recipients dropped because they are opted out (STOP). |
unroutable / unroutable_count | Numbers with no active route (up to 10 samples). |
Tips
- Call
bulk-precheckfirst to confirm reach and cost, thensend-bulkwith the same body. - Use a
esms_test_key to simulate a blast end to end - members are created and reach a final status, but nothing is charged or delivered.