Batch sending
Send up to 100 emails in one request with all-or-nothing acceptance.
POST /v1/emails/batch accepts up to 100 messages in a single request. The batch is atomic: either every message is accepted and you get every id, or nothing is saved and you get one error.
Send a batch
The request body is a top-level JSON array of 1-100 email objects. Each object takes exactly the same fields as a single POST /v1/emails: templates, tags, and scheduled_at all work per item.
# scheduled_at: any time in the next 90 days; a past time sends at once
curl -X POST https://api.mailfully.com/v1/emails/batch \
-H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '[
{
"from": "orders@mail.acme.com",
"to": "you@yourcompany.com",
"subject": "Your order shipped",
"html": "<p>Your order is on its way.</p>",
"tags": [{ "name": "event", "value": "order-shipped" }]
},
{
"from": "orders@mail.acme.com",
"to": "you@yourcompany.com",
"subject": "Your order shipped",
"text": "Order 4212 shipped.",
"scheduled_at": "2026-07-10T09:00:00Z"
}
]'Replace you@yourcompany.com with an address you own before running this.
Success is a 202 with one accepted email per input object, in request order:
{
"data": [
{
"object": "email",
"id": "01JZFYK2N8Q4T7W9E1R3M5X0AB",
"status": "queued",
"type": "transactional",
"to": ["you@yourcompany.com"],
"cc": [],
"bcc": [],
"from": "orders@mail.acme.com",
"subject": "Your order shipped",
"scheduled_at": null,
"created_at": "2026-07-08T14:15:00.000Z"
},
{
"object": "email",
"id": "01JZFYK2NAB3C5D7E9F1G3H5J7",
"status": "queued",
"type": "transactional",
"to": ["you@yourcompany.com"],
"cc": [],
"bcc": [],
"from": "orders@mail.acme.com",
"subject": "Your order shipped",
"scheduled_at": "2026-07-10T09:00:00.000Z",
"created_at": "2026-07-08T14:15:00.000Z"
}
]
}
Correlate ids by position: data[0] is the first object you sent, data[1] the second.
All-or-nothing acceptance
There are no per-item results. Every object is validated and prepared before anything is written, and the first invalid item aborts the entire batch with a single 422 validation_error whose param prefixes the offending item's index ([i] or [i].<field>, for example [37].to). Nothing is saved or queued.
A body that isn't a JSON array fails with 422 validation_error: Batch body must be a JSON array of email objects. An array outside 1-100 items fails the same way.
One caveat to the [i] prefix: an unknown template.id in any item fails the batch with a plain 404 not_found (Template not found.) that does not name the offending index. Nothing is saved. If a batch 404s, check each item's template.id.
Gates run once per batch
The org-level accept gates evaluate the batch as a unit, in this order:
- Verified domain: a live batch with no verified sending domain is rejected with
403 unverified_domain_required. - Spend cap, then monthly ceiling: the monthly check projects
current + Nfor a batch of N recipients. Crossing either limit returns429 spend_cap_exceededor429 monthly_quota_exceeded(live only). - Daily limit: a batch that does not fit returns
429 daily_quota_exceededwith aRetry-Afterheader.
Every recipient in the batch counts toward the daily and monthly limits, and a denied batch uses none of them. One denial rejects the whole batch. A batch with more recipients than your daily limit is never accepted, so split it. See Rate limits.
Idempotency
One Idempotency-Key header covers the whole batch. There are no per-item keys.
const { data, error } = await mailfully.emails.batch(inputs, {
idempotencyKey: "order-1234-shipped",
});
The rules match a single send. See Idempotency.
Batch or loop?
Use the batch endpoint when:
- The messages are generated together (an import, a digest run, a fan-out of the same notification) and partial acceptance would leave you reconciling which half went out.
- You want atomic accept semantics: all ids or one error.
- You want fewer requests through rate limiting and the accept gates.
Loop single sends instead when:
- You need per-message error isolation. In a batch, one invalid address aborts all 100; looped sends fail independently.
- You need per-message idempotency keys, for example one key per order id. A batch key covers the whole array.
- Messages are created at different moments rather than as one unit of work.
Caps to keep in mind: 100 items per batch, and each item allows up to 50 combined to, cc, and bcc recipients.
See Choosing a sending feature for how batch fits alongside scheduling, templates, and marketing sends.
Related
POST /v1/emails/batch: full request and response reference- Idempotency
- Rate limits
