Send a Batch
Send up to 100 emails in one call with POST /v1/emails/batch
POST /v1/emails/batch sends up to 100 emails in one request. Each email
is independent: its own sender, recipients, subject and content, with the same
fields as POST /v1/emails.
Requires API 0.22.0 or later. The SDKs have a batch method too: see With the SDKs.
Request
The body is an object with one field, emails: an array of 1 to 100 emails.
curl -X POST https://api.tratto.email/v1/emails/batch \
-H "Authorization: Bearer tratto_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2e0a-batch-2026-10-20" \
-d '{
"emails": [
{
"from": "hello@yourdomain.com",
"to": "alice@example.com",
"subject": "Your receipt",
"text": "Thanks for your order, Alice."
},
{
"from": "hello@yourdomain.com",
"to": ["bob@example.com"],
"cc": ["billing@example.com"],
"subject": "Your receipt",
"templateId": "tmpl_abc123",
"variables": { "firstName": "Bob" }
}
]
}'import { randomUUID } from 'node:crypto';
const response = await fetch('https://api.tratto.email/v1/emails/batch', {
method: 'POST',
headers: {
'Authorization': 'Bearer tratto_live_...',
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify({
emails: [
{ from: 'hello@yourdomain.com', to: 'alice@example.com', subject: 'Your receipt', text: 'Thanks, Alice.' },
{ from: 'hello@yourdomain.com', to: 'bob@example.com', subject: 'Your receipt', text: 'Thanks, Bob.' },
],
}),
});
const { data } = await response.json();
console.log(`${data.accepted} queued, ${data.failed} failed`);Rules for each email of the batch:
- It accepts every field of
POST /v1/emailsexceptscheduledAt. To schedule, send the email on its own withPOST /v1/emails. attachmentsis not accepted: it fails with 422.- An unknown field fails with 422: both the body and each email are checked
strictly.
POST /v1/emailsdoes the same with API 0.24.0 or later, see Unknown fields. - At most 50 distinct recipients across
to,ccandbcc, as on a single send. See Recipients.
The request body can be at most 5 MiB. Once rendered, each email can be at most 900 KB and the whole batch at most 9 MiB.
Validation is all or nothing. If one email is invalid, the whole batch is
rejected and nothing is written or charged. details[].path names the email
by its position in the array, starting from 0:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed.",
"docs": "https://docs.tratto.email/en/docs/error-codes",
"details": [{ "path": "/emails/3/to", "message": "An email can have at most 50 recipients across to, cc and bcc (got 51). Split the send, or use a campaign for a larger audience." }]
}
}An unverified sender domain answers 403 FORBIDDEN, and the message starts
with the position of the email, as in emails[3]: Domain 'mail.acme.com' is not verified for this tenant.
Response
A batch that passes validation answers 202 Accepted, always with this
shape:
{
"data": {
"batchId": "batch_V1StGXR8Z5jdHi6BmyT2a",
"livemode": true,
"accepted": 2,
"failed": 1,
"emails": [
{ "id": "email_abc123xyz", "status": "queued", "to": ["alice@example.com"] },
{
"status": "failed",
"to": ["bob@example.com"],
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Not sent and not charged: it could not be handed to the delivery queue. Send it again in a new batch, with a new Idempotency-Key."
}
},
{ "id": "email_def456uvw", "status": "queued", "to": ["carol@example.com"] }
]
}
}emailshas one entry per email of the request, in the same order.acceptedcounts the emails queued for delivery;failedcounts the ones not sent and is always present,0on a full success.- A
queuedemail has anid: read its status withGET /v1/emails/{id}, as for any other email. Afailedemail has noidand will not be sent. - A failed email's
error.messagesays whether it was charged. When the quota could not be given back, the message says itmay still count against the monthly quota. Nothing was sent in either case. - The 202 has no
Retry-Afterheader, even when some emails failed.
Retrying the failed emails
Send only the failed emails again, in a new batch with a new
Idempotency-Key. Repeating the same request with the same key returns the
original response, failures included, and sends nothing (see
Idempotency).
If no email of the batch could be queued, the API answers 503 SERVICE_UNAVAILABLE instead of a 202: see Errors.
Idempotency
One Idempotency-Key covers the whole batch, for 24 hours. A replay with the
same key and the same body returns the same response, a partial one included,
and sends nothing. The same key with a different body answers 409 IDEMPOTENCY_CONFLICT; a request with a key whose first request is still
running answers 409 IDEMPOTENCY_IN_PROGRESS. See
Idempotency.
Quota
Each email counts against the monthly quota
as one email per distinct recipient in its to, cc and bcc, ignoring case, exactly as
on a single send. Recipients are deduplicated within one email, never across
emails: the same address in two emails of the batch counts as two emails.
The batch reserves the sum of its emails at once, all or nothing. If the
account has fewer emails left than the batch has distinct recipients in total, the API answers
429 QUOTA_EXCEEDED and nothing is written or sent:
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "This batch has 120 recipients, more than the emails left on the free plan (3000 per month). Each distinct address in to, cc and bcc counts as one email. The quota is shared by every workspace of the account that owns this one.",
"docs": "https://docs.tratto.email/en/docs/error-codes",
"suggestion": "Send to fewer recipients, or have the workspace owner upgrade the plan at https://app.tratto.email/settings/billing."
}
}In test mode the daily cap of 100 test emails counts the sum of the batch in the same way.
Rate limits
A batch counts as one request on the limit of 100 requests per second per
API key. On top of that, each key can make at most 10 batch requests per
second; over it the API answers 429 RATE_LIMITED with Retry-After: 1 and
the message Rate limit exceeded: 10 batch requests/second per API key. See
Rate Limits.
Errors
| Status | Code | When | Written or charged |
|---|---|---|---|
| 422 | VALIDATION_ERROR | 0 or more than 100 emails; an invalid email; more than 50 recipients in one email; scheduledAt, attachments or an unknown field; a reserved header; no sender; an email over 900 KB or a batch over 9 MiB once rendered | Nothing |
| 403 | FORBIDDEN | The sender domain of an email is not verified (emails[i]: …). Checked only with a live key | Nothing |
| 413 | PAYLOAD_TOO_LARGE | Request body over 5 MiB | Nothing |
| 429 | QUOTA_EXCEEDED | Fewer emails left than the batch has recipients in total | Nothing |
| 429 | RATE_LIMITED | More than 10 batches per second per key, or another rate limit. Retry after Retry-After when present, otherwise after a short backoff | Nothing |
| 409 | IDEMPOTENCY_CONFLICT | Same Idempotency-Key, different body | Nothing |
| 409 | IDEMPOTENCY_IN_PROGRESS | Same Idempotency-Key while the first request is still running | Nothing |
| 503 | SERVICE_UNAVAILABLE | Too many batches in progress, or no email could be queued. Comes with Retry-After: 1 | Nothing sent; nothing charged when too many batches are in progress, otherwise the message says whether it was charged |
| 500 | INTERNAL_ERROR | The outcome of storing the emails is unknown | Nothing sent; may be charged |
After a 503, retry the same request: the same Idempotency-Key can be
reused, because no email of the batch was sent.
After a 500, no email of the batch was sent, but the batch may count against
the monthly quota. Send it again: the same Idempotency-Key can be reused.
With the SDKs
Each SDK sends the batch with one call. The call returns the 202 response,
also when some emails failed: check failed and the status of each
email. Errors (422, 429, 503, 500 and the others in Errors) are
raised as exceptions, and on a 422 the exception's details names the email
by its position, as in /emails/3/to. The SDKs do not retry: follow the rules
above, the same key after a 503 or 500, a new key for the failed emails.
Node.js
emails.sendBatch(emails, idempotencyKey?) requires @tratto/email 1.6.0 or
later. See the Node.js SDK.
import { randomUUID } from 'node:crypto';
import { Tratto } from '@tratto/email';
const tratto = new Tratto(process.env.TRATTO_API_KEY!);
const batch = await tratto.emails.sendBatch(
[
{ from: 'hello@yourdomain.com', to: 'alice@example.com', subject: 'Your receipt', text: 'Thanks, Alice.' },
{ from: 'hello@yourdomain.com', to: 'bob@example.com', subject: 'Your receipt', text: 'Thanks, Bob.' },
],
randomUUID(),
);
console.log(`${batch.accepted} queued, ${batch.failed} failed`);
for (const e of batch.emails) {
if (e.status === 'failed') console.error(e.to, e.error.code, e.error.message);
}Python
emails.send_batch(emails, *, idempotency_key=None) requires tratto-email
2.1.0 or later. The key goes on the call: an item with its own
idempotency_key raises ValueError. See the
Python SDK.
import uuid
from tratto import SendEmailOptions, Tratto
client = Tratto('tratto_live_...')
result = client.emails.send_batch(
[
SendEmailOptions(from_='hello@yourdomain.com', to='alice@example.com', subject='Your receipt', text='Thanks, Alice.'),
SendEmailOptions(from_='hello@yourdomain.com', to='bob@example.com', subject='Your receipt', text='Thanks, Bob.'),
],
idempotency_key=str(uuid.uuid4()),
)
print(result['accepted'], result['failed'])
for item in result['emails']: # same order as the request
if item['status'] == 'failed':
print(item['to'], item['error']['code'])PHP
emails->sendBatch(array $emails, ?string $idempotencyKey = null) requires
tratto/email 1.1.0 or later. See the PHP SDK.
$emails = [
['from' => 'hello@yourdomain.com', 'to' => ['alice@example.com'], 'subject' => 'Your receipt', 'text' => 'Thanks, Alice.'],
['from' => 'hello@yourdomain.com', 'to' => ['bob@example.com'], 'subject' => 'Your receipt', 'text' => 'Thanks, Bob.'],
];
$batch = $tratto->emails->sendBatch($emails, 'receipts-2026-10-21');
// Resend only the failed emails, in a new batch with a new key.
$failed = [];
foreach ($batch['emails'] as $i => $result) {
if ($result['status'] === 'failed') {
$failed[] = $emails[$i];
}
}
if ($failed !== []) {
$tratto->emails->sendBatch($failed, 'receipts-2026-10-21-retry-1');
}Next Steps
- One email at a time? See Send Email
- Track what happens next? Set up Webhooks
- Plan your throughput? Read Rate Limits & Quotas
Edit this page on GitHub
Last updated on