The eSMS bulk SMS API gives you two endpoints: POST /api/messages/send-bulk sends one message (with simple placeholders) to many numbers, and POST /v1/messages/batch sends up to 10,000 personalised messages in one durable call. Both return a batch_id straight away, deliver in the background, refuse the request before charging if something is wrong, and accept an Idempotency-Key so a retry never sends twice.
Last reviewed: 29 October 2026
How to send bulk SMS with the bulk SMS API
Pick the right endpoint
You want to Use Send the same text to a list, now, at a set time, or dripped out per minute POST /api/messages/send-bulkSend a different message to each person (balances, names, order numbers) POST /v1/messages/batchSend to more than 10,000 people from a file Upload the file with Imports and run a campaign (see the campaigns reference). Send one message to many numbers
Pass inline
recipients(or savedcontact_list_ids).{{name}}and{{phone}}are always available, andvarsfills any other{{key}}.curl -X POST https://sms.esmsafrica.io/api/messages/send-bulk \ -H "Authorization: Bearer $ESMS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: closure-notice-2026-10-29" \ -d '{ "recipients": [ { "to": "+254712345678", "name": "Amina" }, { "to": "+256772123456", "name": "Brian" } ], "text": "Hi {{name}}, our Westlands branch is closed on Friday for maintenance.", "schedule_mode": "now" }'The response has
batch_id,total_recipients(after duplicates and opted-out numbers are removed),estimated_costandstatus. Setschedule_modetoscheduledwithscheduled_at(5 minutes to 7 days ahead, contact lists only), or todripwithdrip_rate(1 to 5,000 messages a minute).Send personalised messages in one call
The batch endpoint renders a template per recipient. Merge fields look like
{balance};{first_name|there}uses the fallback when the value is empty. Built-in fields arename,first_name,last_nameandphone.curl -X POST https://sms.esmsafrica.io/v1/messages/batch \ -H "Authorization: Bearer $ESMS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: october-dues-2026" \ -d '{ "template": "Hi {first_name|there}, your balance of UGX {balance} is due on {due_date}.", "recipients": [ { "to": "+256772123456", "name": "Ada Obi", "vars": { "balance": "1,500", "due_date": "1 Nov" } }, { "to": "0701234567", "vars": { "balance": "200", "due_date": "1 Nov" } } ], "default_country": "UG", "sender_id": "ACMEBANK", "callback_url": "https://example.com/hooks/esms/campaigns" }'It answers HTTP 202:
{ "batch_id": "6f0f5c55-6a2e-4bb1-9c39-2a6f1b6c7d10", "campaign_id": 482, "status": "sending", "accepted": 2, "rejected": [], "rejected_count": 0, "idempotent_replay": false, "livemode": true }Bad entries are listed in
rejectedwith a reason (duplicate,opted_out,unavailable_countryor a number problem such asmissing_country_code) and are never sent or charged. The batch is billed per batch as it sends. If your sender ID is not approved in a country, that country's default sender is used unless you set"sender_fallback": false.Use an SDK if you prefer
Python example (Node.js uses
messages.sendBatch(), PHP$esms->messages->sendBatch()and Goclient.Messages.SendBatch):batch = esms.messages.send_batch( template="Hi {first_name|there}, your order {order_id} ships today.", recipients=[ {"to": "+254712345678", "name": "Amina", "vars": {"order_id": "A-1001"}}, {"to": "+254712345679", "vars": {"order_id": "A-1002"}}, ], idempotency_key="dispatch-2026-10-29", ) status = esms.messages.get_batch(batch["batch_id"])The SDKs add a random
Idempotency-Keywhen you do not pass one.Track, cancel and get notified
GET /v1/messages/batch/{batch_id}returns message counts and progress.POST /v1/messages/batch/{batch_id}/cancelstops recipients that have not been sent yet; they are never charged. Each message's delivery report carries thebatch_id, and a signedcampaign.completedevent goes to yourcallback_url(or the account's campaign events URL) when a batch finishes.
Common errors and how to fix them
| What you see | What it means | How to fix it |
|---|---|---|
400 missing_merge_field (send-bulk) | Some recipients have no value for a placeholder that has no fallback. Nothing was sent or charged. | Add a fallback such as {{name|Customer}} or fill the data. The fields list shows which field and how many recipients. |
422 launch_blocked (batch) | A blocking check failed, for example insufficient balance, a missing field or an unapproved sender with fallback off. | Read error.details.blocking, fix the first problem and resend. |
400 destinations_unavailable | Some recipients are in countries not available yet. Nothing was charged. | Remove them, or set skip_unavailable to true on send-bulk to drop them uncharged. |
402 insufficient_balance | The balance does not cover the whole send. | Top up, or split the list. |
400 too_many_recipients | More than 10,000 entries in one batch call. | Split the list or use Imports and a campaign. |
409 idempotency_in_progress | A request with the same key is still being processed. | Wait a few seconds and retry with the same key. |
413 message_too_long | A rendered message is over the 10-segment cap. | Shorten the template or the longest merge value. |
429 rate_limited | More than 10 bulk calls in a minute on this key. | Batch more recipients per call and back off before retrying. |
Frequently asked questions
Are duplicate and opted-out numbers charged?
No. Duplicates are collapsed and anyone on your STOP list is removed before sending, on both endpoints.
Can one bulk send cover several countries?
Yes. Each recipient is priced at its own country's rate, and every country must be enabled for live sends.
Which placeholder style should I use?
send-bulk documents {{name}} style; the batch endpoint accepts {field}, {field|fallback} and {{field}}.
Can I schedule a batch in the recipient's time zone?
Yes. Pass schedule with mode, at and timezone, for example Africa/Kampala.
Can I test a bulk send for free?
Yes. With a test key every recipient gets a simulated outcome and nothing is charged. Scheduled send-bulk is not available with test keys.
Related guides
Ready? Create a free eSMS account, then create a test API key and send your first simulated message in minutes. Nothing is charged until you switch to a live key.



