> ## Documentation Index
> Fetch the complete documentation index at: https://docs.robase.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Batch sends

> Up to 1000 messages in one API call with partial-success handling.

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

<CodeGroup>
  ```bash cURL theme={null}
  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." }
      ]
    }'
  ```

  ```typescript Node.js theme={null}
  const result = await pm.sms.batch({
    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.' },
    ],
  });

  console.log(result.accepted, result.failed, result.success);
  ```
</CodeGroup>

## Response shape

Batch always returns **HTTP 200** — even when some rows fail. The response tells you exactly which succeeded and which didn't:

```json theme={null}
{
  "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", ... } }
  ]
}
```

<Note>
  `success` is a convenience boolean — `true` only when every row succeeded. Your code should branch on `failed > 0` to handle partial success gracefully.
</Note>

## Per-row idempotency

Each message can carry its own `idempotency_key`, scoped per row:

```typescript theme={null}
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:

```typescript theme={null}
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:

```json theme={null}
{
  "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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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.
