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/batchOne 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"
}| Field | Type | Description |
|---|---|---|
template | string | Message with merge fields: {field}, {field|fallback} or {{field}}. Required unless template_id is given or every recipient has text. |
template_id | integer | A saved template instead of template. |
recipients | array | [{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. |
messages | array | Alternative to recipients: [{to, text}] full messages, no template. |
default_country | string | ISO country for local numbers like 0701... (defaults to your workspace setting). |
sender_id | string | Your approved sender ID. Omit for each country's default. |
sender_fallback | boolean | Default true: use the country default where your sender ID isn't approved. false blocks the batch instead. |
message_type | string | transactional (default) or promotional (adds the opt-out line and respects quiet hours). |
name, tags | string, string[] | Shown in the dashboard and reports. |
schedule | object | {mode: "now" | "scheduled" | "recurring", at, timezone, recurrence}. See Campaigns. Default: send now. |
throttle_per_minute | integer | Pace the send (1-20,000 messages a minute). |
callback_url | string | Public 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
}| Field | Description |
|---|---|
status | sending, scheduled, or pending_approval when your workspace requires approval. |
accepted | Recipients 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 / currency | Estimate 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}/reportBatch 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
| Status | Code | Meaning |
|---|---|---|
| 400 | recipients_required | Neither recipients nor messages given. |
| 400 | template_required | No template and some recipients have no text. |
| 400 | text_required | An entry in messages has no text. |
| 400 | too_many_recipients | More than 10,000 entries. |
| 400 | invalid_schedule, invalid_timezone | Bad schedule. |
| 403 | insufficient_scope, role_forbidden, account_suspended | The key lacks the send scope, the team role can't send, or the account is suspended. |
| 422 | launch_blocked | A 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)