Sending batches
When to batch
Use POST /v1/emails/batch when you have many emails to enqueue at once — a digest run, onboarding bursts, order receipts. One call accepts up to 100 emails:
| Limit | Value |
|---|---|
| Emails per batch call | 100 |
The full limits table lives in Quotas and limits.
Request shape
The body is an emails array; each item has the same shape as a single send — including the optional text, headers and reply_to fields — with its own idempotency_key. With the SDK, pass an array of the same send payloads to emails.sendBatch:
const results = await notideus.emails.sendBatch([
{
from: 'Notideus <noreply@demo.dev>',
to: ['jane@example.com'],
subject: 'Your weekly digest',
html: '<p>Hi Jane, here is your digest.</p>',
idempotency_key: 'digest-2026-09-14-jane'
},
{
from: 'Notideus <noreply@demo.dev>',
to: ['john@example.com'],
subject: 'Your weekly digest',
html: '<p>Hi John, here is your digest.</p>',
idempotency_key: 'digest-2026-09-14-john'
}
]);
$results = $notideus->emails()->sendBatch([
[
'from' => 'Notideus <noreply@demo.dev>',
'to' => ['jane@example.com'],
'subject' => 'Your weekly digest',
'html' => '<p>Hi Jane, here is your digest.</p>',
'idempotency_key' => 'digest-2026-09-14-jane'
],
[
'from' => 'Notideus <noreply@demo.dev>',
'to' => ['john@example.com'],
'subject' => 'Your weekly digest',
'html' => '<p>Hi John, here is your digest.</p>',
'idempotency_key' => 'digest-2026-09-14-john'
]
]);
{
"emails": [
{
"from": "Notideus <noreply@demo.dev>",
"to": ["jane@example.com"],
"subject": "Your weekly digest",
"html": "<p>Hi Jane, here is your digest.</p>",
"idempotency_key": "digest-2026-09-14-jane"
},
{
"from": "Notideus <noreply@demo.dev>",
"to": ["john@example.com"],
"subject": "Your weekly digest",
"html": "<p>Hi John, here is your digest.</p>",
"idempotency_key": "digest-2026-09-14-john"
}
]
}
Response: per-item results
A successful call returns 200 with one result per item, in request order. This is a real three-item response:
{
"data": [
{
"index": 0,
"id": "01a0a11c-4e6e-7228-b9b3-f50fd0728a90"
},
{
"index": 1,
"id": "01a0a11c-4e75-7486-8f23-8d50dc697ebf"
},
{
"index": 2,
"error": {
"code": "from_domain_not_verified",
"message": "no verified domain matches the from address"
}
}
]
}
Decode:
indexis the item's position in the request array (0-based).- Items 0 and 1 were accepted —
idis the stored email's UUID, exactly what a single send would return. - Item 2 failed —
error.codeisfrom_domain_not_verified, anderror.messageexplains why. Nothing was stored for it.
The SDK resolves with the per-item array directly — narrow each result by
checking for the error key:
for (const item of results) {
if ('error' in item) {
console.error(item.index, item.error.code, item.error.message);
} else {
console.log(item.index, item.id);
}
}
In PHP the same array is returned — narrow each item with
isset($item['error']):
foreach ($results as $item) {
if (isset($item['error'])) {
error_log($item['index'] . ' ' . $item['error']['code'] . ' ' . $item['error']['message']);
} else {
error_log($item['index'] . ' ' . $item['id']);
}
}
Whole-call failures
Some errors reject the whole call before any item is processed, with the standard error envelope:
{
"error": {
"code": "batch_too_large",
"message": "batch is limited to 100 emails"
}
}
- 400
batch_too_large— more than 100 items; split the payload and try again. - 402
quota_exceeded— the batch is checked against your quota up front: if it would not fit, nothing is sent. Idempotent replays of already-stored emails are exempt, so retrying with the same keys never double-counts. Otherwise back off and retry later — see Quotas and limits.
Retry strategy
Only the items with an error need retrying. Build a new batch from just those items and re-send it with the same per-item idempotency_keys: items that succeeded on the first attempt will not be duplicated, and an item that was actually stored before your client timed out will replay instead of double-sending. See Idempotency.
With the SDK the same rule applies: rebuild the batch from just the failed
items and call emails.sendBatch again with the same per-item keys.
Whole-call failures (401, 402 quota_exceeded, 400 batch_too_large) throw
a NotideusError instead of returning items.