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

  1. Pick the right endpoint

    You want toUse
    Send the same text to a list, now, at a set time, or dripped out per minutePOST /api/messages/send-bulk
    Send a different message to each person (balances, names, order numbers)POST /v1/messages/batch
    Send to more than 10,000 people from a fileUpload the file with Imports and run a campaign (see the campaigns reference).
  2. Send one message to many numbers

    Pass inline recipients (or saved contact_list_ids). {{name}} and {{phone}} are always available, and vars fills 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_cost and status. Set schedule_mode to scheduled with scheduled_at (5 minutes to 7 days ahead, contact lists only), or to drip with drip_rate (1 to 5,000 messages a minute).

  3. 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 are name, first_name, last_name and phone.

    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 rejected with a reason (duplicate, opted_out, unavailable_country or a number problem such as missing_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.

  4. Use an SDK if you prefer

    Python example (Node.js uses messages.sendBatch(), PHP $esms->messages->sendBatch() and Go client.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-Key when you do not pass one.

  5. Track, cancel and get notified

    GET /v1/messages/batch/{batch_id} returns message counts and progress. POST /v1/messages/batch/{batch_id}/cancel stops recipients that have not been sent yet; they are never charged. Each message's delivery report carries the batch_id, and a signed campaign.completed event goes to your callback_url (or the account's campaign events URL) when a batch finishes.

Common errors and how to fix them

What you seeWhat it meansHow 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_unavailableSome 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_balanceThe balance does not cover the whole send.Top up, or split the list.
400 too_many_recipientsMore than 10,000 entries in one batch call.Split the list or use Imports and a campaign.
409 idempotency_in_progressA request with the same key is still being processed.Wait a few seconds and retry with the same key.
413 message_too_longA rendered message is over the 10-segment cap.Shorten the template or the longest merge value.
429 rate_limitedMore 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.

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.