Mailfully logo

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:

  1. Verified domain: a live batch with no verified sending domain is rejected with 403 unverified_domain_required.
  2. Spend cap, then monthly ceiling: the monthly check projects current + N for a batch of N recipients. Crossing either limit returns 429 spend_cap_exceeded or 429 monthly_quota_exceeded (live only).
  3. Daily limit: a batch that does not fit returns 429 daily_quota_exceeded with a Retry-After header.

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.