eSMS AfricaeSMS Africa
API ReferenceSMS API

Personalised batch

POST /v1/messages/batch - Send an individual message to many recipients in one call, with merge fields, scheduling and an Idempotency-Key.

Endpoint

POST https://sms.esmsafrica.io/v1/messages/batch

One call sends a personalised message to up to 10,000 recipients: a template filled with each recipient's vars, or a full text per recipient. It creates an API campaign and launches it, so it is durable (it survives restarts), billed per batch as it sends, and reported per recipient. For bigger lists, upload a file with Imports and use a campaign.

The /api/... prefix works too (/api/messages/batch). Rate limit: 10 calls a minute per key.

Authentication

Authorization: Bearer esms_live_your_api_key
Idempotency-Key: <a unique id for this batch>

Send an Idempotency-Key. A retry with the same key returns the original batch ("idempotent_replay": true) and never sends or charges again. The SDKs add a random key per call when you don't pass one. An esms_test_ key simulates the whole batch (no real SMS, no charge, sandbox magic numbers apply).

Request

{
  "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",
  "message_type": "transactional",
  "schedule": { "mode": "scheduled", "at": "2026-11-01T09:00", "timezone": "Africa/Kampala" },
  "callback_url": "https://example.com/hooks/campaigns"
}
FieldTypeDescription
templatestringMessage with merge fields: {field}, {field|fallback} or {{field}}. Required unless template_id is given or every recipient has text.
template_idintegerA saved template instead of template.
recipientsarray[{to, name?, vars?, text?}], up to 10,000. vars are this recipient's merge-field values; text is a full message that replaces the template for this recipient.
messagesarrayAlternative to recipients: [{to, text}] full messages, no template.
default_countrystringISO country for local numbers like 0701... (defaults to your workspace setting).
sender_idstringYour approved sender ID. Omit for each country's default.
sender_fallbackbooleanDefault true: use the country default where your sender ID isn't approved. false blocks the batch instead.
message_typestringtransactional (default) or promotional (adds the opt-out line and respects quiet hours).
name, tagsstring, string[]Shown in the dashboard and reports.
scheduleobject{mode: "now" | "scheduled" | "recurring", at, timezone, recurrence}. See Campaigns. Default: send now.
throttle_per_minuteintegerPace the send (1-20,000 messages a minute).
callback_urlstringPublic URL that receives the signed campaign.completed event.

Built-in fields: name, first_name, last_name (from name) and phone.

Response

202 Accepted

{
  "batch_id": "6f0f5c55-6a2e-4bb1-9c39-2a6f1b6c7d10",
  "campaign_id": 482,
  "status": "scheduled",
  "accepted": 2,
  "rejected": [],
  "rejected_count": 0,
  "estimated_cost": 0.0168,
  "currency": "USD",
  "idempotent_replay": false,
  "environment": "live",
  "livemode": true
}
FieldDescription
statussending, scheduled, or pending_approval when your workspace requires approval.
acceptedRecipients that will be messaged.
rejected[]{index, to, reason, message} for entries that are never sent or charged: duplicate, opted_out, unavailable_country, or a number problem (not_a_number, invalid_number, missing_country_code, landline, ...).
estimated_cost / currencyEstimate in your wallet currency at your prices. Charged per batch as messages go out.

Track and cancel

GET  https://sms.esmsafrica.io/v1/messages/batch/{batch_id}
POST https://sms.esmsafrica.io/v1/messages/batch/{batch_id}/cancel
GET  https://sms.esmsafrica.io/v1/campaigns/{campaign_id}/report

Batch status returns the message counts plus campaign_id, the campaign status and per-recipient progress (recipients: counts by pending, sending, submitted, failed, skipped, cancelled). Cancelling stops recipients that have not been sent yet (they are never charged). Per-message delivery reports carry the batch_id and campaign_id.

Errors

StatusCodeMeaning
400recipients_requiredNeither recipients nor messages given.
400template_requiredNo template and some recipients have no text.
400text_requiredAn entry in messages has no text.
400too_many_recipientsMore than 10,000 entries.
400invalid_schedule, invalid_timezoneBad schedule.
403insufficient_scope, role_forbidden, account_suspendedThe key lacks the send scope, the team role can't send, or the account is suspended.
422launch_blockedA blocking check failed (error.details.blocking lists them, e.g. insufficient_balance, missing_field, sender_not_approved, content_blocked). Nothing is created or charged.

Examples

curl -X POST https://sms.esmsafrica.io/v1/messages/batch \
  -H "Authorization: Bearer $ESMS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "template": "Your order {order_id} is out for delivery today. Track it: {tracking_link}",
    "recipients": [
      { "to": "+254712345678", "vars": { "order_id": "A-1001", "tracking_link": "https://example.com/t/1001" } },
      { "to": "+254712345679", "vars": { "order_id": "A-1002", "tracking_link": "https://example.com/t/1002" } }
    ]
  }'
// SDK 1.2.0 or later
const batch = await esms.messages.sendBatch({
  template: "Your order {order_id} is out for delivery today. Track it: {tracking_link}",
  recipients: [
    { to: "+254712345678", vars: { order_id: "A-1001", tracking_link: "https://example.com/t/1001" } },
    { to: "+254712345679", vars: { order_id: "A-1002", tracking_link: "https://example.com/t/1002" } },
  ],
});
const status = await esms.messages.getBatch(batch.batch_id);
# SDK 1.2.0 or later
batch = esms.messages.send_batch(
    template="Your order {order_id} is out for delivery today. Track it: {tracking_link}",
    recipients=[
        {"to": "+254712345678", "vars": {"order_id": "A-1001", "tracking_link": "https://example.com/t/1001"}},
        {"to": "+254712345679", "vars": {"order_id": "A-1002", "tracking_link": "https://example.com/t/1002"}},
    ],
)
status = esms.messages.get_batch(batch["batch_id"])
// SDK 1.2.0 or later
$batch = $esms->messages->sendBatch([
    'template' => 'Your order {order_id} is out for delivery today. Track it: {tracking_link}',
    'recipients' => [
        ['to' => '+254712345678', 'vars' => ['order_id' => 'A-1001', 'tracking_link' => 'https://example.com/t/1001']],
        ['to' => '+254712345679', 'vars' => ['order_id' => 'A-1002', 'tracking_link' => 'https://example.com/t/1002']],
    ],
]);
$status = $esms->messages->getBatch($batch['batch_id']);
// SDK 1.2.0 or later
batch, err := client.Messages.SendBatch(ctx, esms.SendBatchParams{
	Template: "Your order {order_id} is out for delivery today. Track it: {tracking_link}",
	Recipients: []esms.BatchRecipient{
		{To: "+254712345678", Vars: map[string]string{"order_id": "A-1001", "tracking_link": "https://example.com/t/1001"}},
		{To: "+254712345679", Vars: map[string]string{"order_id": "A-1002", "tracking_link": "https://example.com/t/1002"}},
	},
})
status, _ := client.Messages.GetBatch(ctx, batch["batch_id"].(string), 0, 50)

On this page