Batch sending
POST /v1/email/batch submits up to 100 independent messages in a single
request. Each one has its own recipients, subject, and body. Reach for it
when you have many distinct messages to send, not one message going to many
recipients.
A batch is not the same as a multi-recipient send. POST /v1/email delivers
one message to a shared recipient list; see Send a single email.
A batch delivers a different message to each entry.
The request
The body is an emails array of 1 to 100 entries. Each entry takes the same
fields as a POST /v1/email body.
curl https://api.anypost.com/v1/email/batch \
-H "Authorization: Bearer $ANYPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": [
{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"subject": "Your invoice is ready",
"html": "<p>Invoice #1190 is attached.</p>"
},
{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"subject": "Welcome to Acme",
"html": "<p>Glad you are here.</p>"
}
]
}'Shared defaults
Repeating from on every entry is noise. An optional defaults object holds
fields shared across the batch; an entry inherits each one it does not set for
itself.
curl https://api.anypost.com/v1/email/batch \
-H "Authorization: Bearer $ANYPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"defaults": {
"from": "Acme <[email protected]>",
"tags": ["onboarding"]
},
"emails": [
{ "to": ["[email protected]"], "subject": "Welcome, Alex", "html": "<p>Hello.</p>" },
{ "to": ["[email protected]"], "subject": "Welcome, Sam", "html": "<p>Hello.</p>" }
]
}'Not every field merges the same way:
| Fields | How a default and an entry combine |
|---|---|
from, subject, reply_to, text, html, campaign, topic, unsubscribe | The entry's value replaces the default. |
cc, bcc, attachments | The entry's list is appended to the default list. |
headers, variables | Shallow-merged by key; the entry's value wins on a collision. |
tags | Concatenated, default tags first, then de-duplicated. |
template_id | The entry's template_id wins. An entry that inlines html or text drops the default template_id. |
Every per-message limit is checked against the merged result, not against
defaults or the entry alone. A batch cannot, for example, slip past the
50-recipient cap by splitting to between defaults and an entry.
Validation is all-or-nothing
Before any message is sent, every merged entry is validated. If one entry
fails, the whole batch is rejected with 400 and nothing is sent. Error paths
are prefixed with the entry index: a missing subject on the fourth entry
reports as emails.3.subject.
Quota is enforced the same way. If the batch as a whole would exceed your daily
limit (or, on the free plan, your monthly quota), the entire request is rejected
with 429 before any entry is sent. Split it into smaller batches and retry
after the limit resets. On a paid plan, sending past your monthly quota is
billed as overage rather than rejected. See Sending limits.
The response
Once validation passes, each entry proceeds on its own. The response reports
every entry's outcome under data, in request order, with a summary of the
totals.
{
"summary": { "total": 2, "queued": 1, "failed": 1 },
"data": [
{
"status": "queued",
"index": 0,
"id": "email_018f4f3e-7b2c-7c80-8e21-1a3a4f5b6c7d",
"created_at": "2026-04-30T12:00:00.123000Z"
},
{
"status": "failed",
"index": 1,
"error": {
"type": "permission_error",
"message": "domain_not_verified"
}
}
]
}A queued entry carries the id and created_at you would get from a single
send. A failed entry carries an error with a type and a message. The
index is the entry's zero-based position in the request emails array.
error.type | Meaning |
|---|---|
validation_error | The entry referenced a template that does not exist or is unpublished, or supplied no subject of its own. |
permission_error | The sender domain is not on the API key's allowlist, or is not verified for your team. |
internal_error | Anypost could not accept the entry. Retry that entry. |
HTTP status
The top-level status reflects the mix of outcomes:
| Status | Meaning |
|---|---|
202 | Every entry queued. |
207 | Mixed: some entries queued, some failed. Inspect data. |
400 | The batch failed validation, or every entry failed for a reason you can correct (unverified domain, missing template). |
502 | Every entry failed and at least one failure was a temporary server-side error. Retry the whole batch with backoff. |
A 207 is a success envelope, not an error: per-entry failures live inside
data[i].error, not in a top-level error object. See
API conventions for the canonical error shape a
400 uses.
The SDK resolves a 207 rather than throwing. Inspect each entry's status:
for (const entry of result.data) {
if (entry.status === 'queued') {
console.log(entry.index, entry.id);
} else {
console.error(entry.index, entry.error.type, entry.error.message);
}
}Idempotency
A batch retry is made safe the same way a single send is: include an
Idempotency-Key header. A replay returns the original data array verbatim,
so entries that queued on the first attempt are never sent twice, even when the
first response was a 207. See API conventions.
Sizing a batch
The hard cap is 100 entries, but the request body is also capped at 5 MB.
- Text-only entries reach 100 long before the body cap.
- Entries with attachments hit 5 MB well before 100. Aim for 25 entries or
fewer when attaching files. When every entry carries the same file, put it
in
defaults.attachmentsso its bytes are sent once, not once per entry.
An over-large body is rejected with 413 before any entry is processed.
Per-recipient content
A batch is how you send genuinely different content to each recipient. Set a
template as a batch default and give each entry its own variables:
{
"defaults": {
"from": "Acme <[email protected]>",
"template_id": "template_550e8400-e29b-41d4-a716-446655440000"
},
"emails": [
{
"to": ["[email protected]"],
"variables": { "first_name": "Alex", "balance": "$40" }
},
{
"to": ["[email protected]"],
"variables": { "first_name": "Sam", "balance": "$0" }
}
]
}Within a single POST /v1/email, variables is one object shared by all
recipients. A batch lifts that limit. See
Variables & personalization for placeholder syntax.
Where to go next
- Send a single email: one message to a shared recipient list.
- Sending with templates: store body content and send it by ID.
- Variables & personalization: render per-entry values into the body.