Batch sends are for campaigns, bulk notifications, and scheduled blasts. One API call processes up to 1000 messages — each with its own recipient, optional per-row idempotency key, and optional variables.
Minimal batch
curl -X POST https://api.robase.dev/v1/sms/batch \
-H "Authorization: Bearer rb_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"from": "SHUTTLERS",
"messages": [
{ "to": "+2348012345678", "body": "Hi Chidi, your ride is at 10:00am." },
{ "to": "+2348087654321", "body": "Hi Amaka, your ride is at 10:15am." }
]
}'
Response shape
Batch always returns HTTP 200 — even when some rows fail. The response tells you exactly which succeeded and which didn’t:
{
"accepted": 2,
"failed": 1,
"success": false,
"data": [
{ "index": 0, "sms": { "id": "...", "status": "queued", ... } },
{ "index": 1, "error": { "type": "phone_invalid", "message": "...", "field": "to" } },
{ "index": 2, "sms": { "id": "...", "status": "queued", ... } }
]
}
success is a convenience boolean — true only when every row succeeded. Your code should branch on failed > 0 to handle partial success gracefully.
Per-row idempotency
Each message can carry its own idempotency_key, scoped per row:
await pm.sms.batch({
from: 'SHUTTLERS',
messages: trips.map(trip => ({
to: trip.passenger_phone,
body: `Hi ${trip.name}, ride at ${trip.time}.`,
idempotency_key: `trip-${trip.id}-sms`, // retries are safe
})),
});
Batch with templates
Use a shared template at the batch level, override variables per row:
await pm.sms.batch({
from: 'SHUTTLERS',
template: 'ride-reminder',
messages: trips.map(trip => ({
to: trip.passenger_phone,
variables: { name: trip.name, time: trip.time },
idempotency_key: `trip-${trip.id}-reminder`,
})),
});
A row can also override the template:
{
"messages": [
{ "to": "+234...", "template": "ride-reminder", "variables": { ... } },
{ "to": "+234...", "template": "vip-reminder", "variables": { ... } }
]
}
Scheduled batch
Schedule every message in the batch for the same time:
await pm.sms.batch({
from: 'SHUTTLERS',
messages: recipients.map(r => ({ to: r.phone, body: r.msg })),
send_at: new Date('2026-05-01T09:00:00+01:00').toISOString(),
});
Limits
| Limit | Value |
|---|
| Max rows per call | 1000 |
| Max body per row | 1600 chars |
| Request timeout | 60 s |
If you need more than 1000 in one go, chunk into multiple calls with a small jitter between them:
for (const chunk of chunks(recipients, 1000)) {
await pm.sms.batch({ from, messages: chunk });
await new Promise(r => setTimeout(r, 250)); // 4 batches/s
}
Rate limits count each batch call as 1 request — 1000 messages in one batch is still 1/s against the rate limiter.
Partial-success handling
The canonical pattern:
const result = await pm.sms.batch({ from, messages });
for (const row of result.data) {
if (row.error) {
await db.updateStatus(row.index, 'failed', row.error.message);
} else {
await db.updateStatus(row.index, 'queued', row.sms!.id);
}
}
if (result.failed > 0) {
await alertOps(`Batch had ${result.failed} failures`);
}
Fail-loud: if you have an SLO on delivery, log the failed rows so you can follow up manually or push them into a retry queue.