Skip to main content
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

LimitValue
Max rows per call1000
Max body per row1600 chars
Request timeout60 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.