# Batch Delivery Modes

> How POST /emails/batch decides between sending inline (200) and queueing for background delivery (202).

`POST /emails/batch`

A [batch send](https://www.mailblastr.com/docs/api/emails-batch) is answered one of two ways, chosen by the **size of the batch**. Small batches are sent while your request is open and return `200`, exactly as they always have. Larger batches are accepted, queued, and returned `202 Accepted` — the emails are then delivered in the background.

Both modes return the same `data` array of created email ids, in request order, so the ids are available immediately either way.

## The boundary

| batch size | status | meaning |
| --- | --- | --- |
| 1–40 | `200 OK` | Every email was handed to the mail service before the response. Unchanged behaviour. |
| 41–100 | `202 Accepted` | Every email was accepted and queued. Delivery happens in the background, normally within minutes; a full 100-email batch typically finishes in about ten. |

The boundary is 40 because that is the largest batch that reliably completes inside a single request. Each email in an inline batch is a full send — a chain of database round trips plus the handoff to the mail service, about two seconds each — so 40 emails take roughly 97 seconds of the 120 available. A 100-email batch would need about 168 seconds, which is why it is queued instead of being sent until the request runs out of time.

> **Note:** One exception: a batch containing a [reserved test recipient](https://www.mailblastr.com/docs/emails/send-test) (`@mailblastr.dev`) is always sent inline, whatever its size, because those addresses are simulated rather than transmitted.

## The 202 response

```json
{
  "data": [
    { "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c" },
    { "id": "3a9f8c2b-1e5d-4f8a-9c7b-2d6e5f8a9c7b" }
  ],
  "queued": true,
  "queued_count": 100
}
```

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] | No | One `{ id }` per email, in the order you submitted them. Same field as the `200` response. |
| `queued` | boolean | No | Present and `true` only on a queued (`202`) batch. Absent on an inline `200`. |
| `queued_count` | number | No | How many emails were queued. Always equal to the length of `data`. |

If your client only reads `data`, no change is needed — every MailBlastr SDK treats `202` as success and returns the same ids. Branch on `queued` if you want to know which mode you got.

## Following a queued batch

Each queued email is a normal email object from the moment you get the `202`. There is no separate batch resource to poll — use the ids you were given:

- [GET /emails/:id](https://www.mailblastr.com/docs/api/emails-get) — the current `status` and event log for one email.
- [GET /emails](https://www.mailblastr.com/docs/api/emails-list) — page through them, optionally filtered with `status`.
- [POST /emails/:id/cancel](https://www.mailblastr.com/docs/api/emails-cancel) — cancel one that has not been sent yet.

A queued email starts at `scheduled` and moves to `sent` (then `delivered`, `bounced`, and so on as events arrive) or to `failed` with an `error` explaining why. Webhooks behave exactly as they do for any other send: you receive `email.sent`, `email.delivered`, `email.bounced`, `email.failed`, and `email.suppressed` as they happen. A queued batch does **not** emit `email.scheduled` — you did not schedule it, and the `202` is your receipt.

## What is checked when

Anything whose answer cannot change between accepting the batch and sending it is checked **before** the `202`, so you hear about it synchronously. Anything that can change is re-checked at send time, per email — which is also how [scheduled sends](https://www.mailblastr.com/docs/emails/schedule) have always worked.

| checked | when | what you see |
| --- | --- | --- |
| Payload validation of every item | before the `202` | The whole batch is rejected and nothing is queued. |
| API key domain scope | before the `202` | `restricted_api_key`, nothing queued. |
| From-domain is verified | before the `202` | `validation_error`, nothing queued. |
| `topic_id` exists | before the `202` | `validation_error`, nothing queued. |
| Plan domain allowance | before the `202` | `plan_limit_reached` (402), nothing queued. |
| Sending quota and credits | at send time, per email | Emails over your limit stay `scheduled` and retry hourly, rather than the batch being rejected. |
| Suppression list and topic opt-outs | at send time, per email | That email becomes `failed` with the reason; the rest still send. |
| Sending reputation and warm-up | at send time, per email | That email stays `scheduled` and is retried after the stated window. |

> **Warning:** Quota is consumed when each email is actually sent, not when the batch is accepted. If a batch exceeds your remaining daily allowance, the emails that fit go out and the rest wait — they retry hourly for up to three days and then fail with a quota reason. Check your [limits](https://www.mailblastr.com/docs/api/limits) before queueing a batch you know is near the cap, or split it across days.

## Retrying safely

Pass an `Idempotency-Key` and the whole answer — including a `202` and its ids — is recorded against that key. Retrying with the same key replays the recorded response and queues nothing more, so a batch can never be enqueued twice. The queueing itself is atomic: the emails, their delivery jobs and the recorded answer are written in one transaction, so a request that is cut off part-way through leaves nothing behind and can simply be retried. See [Idempotency keys](https://www.mailblastr.com/docs/emails/idempotency).

## When an inline batch runs out of time

An inline batch (40 or fewer) that has not finished when its time budget expires stops itself rather than being cut off, and reports exactly what went out:

```json
{
  "statusCode": 503,
  "name": "batch_incomplete",
  "message": "Only 34 of 40 emails were sent before this request ran out of time. …",
  "sent": [{ "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c" }],
  "sent_count": 34
}
```

The emails in `sent` **were** sent; the remainder were not. Resend only the remainder, with a new `Idempotency-Key`. The same `sent`/`sent_count` fields appear when an inline batch fails part-way through for any other reason (a quota or reputation limit reached mid-batch, for example).

> **Warning:** The status of a partial result depends on whether you supplied an `Idempotency-Key`. With a key it is `503` with `Retry-After`, because retrying that key replays the recorded answer and re-sends nothing. **Without** a key the same name and body are returned as `422` and no `Retry-After` is set — the SDKs retry `503` and `429` automatically, and an automatic retry of an unrecorded partial batch would re-send the emails that already went out.

## Client timeouts

Every MailBlastr SDK defaults to a 30-second per-request timeout, which is shorter than a large inline batch takes. A queued (`202`) batch answers in about two seconds and is unaffected. If you deliberately send inline batches near the boundary, raise your client timeout — a client that gives up mid-request cannot tell what was sent, and retrying without an `Idempotency-Key` risks duplicates.

## Errors

See the [error reference](https://www.mailblastr.com/docs/api/errors). `batch_incomplete` is specific to this endpoint; every other name a batch can return is shared with [POST /emails](https://www.mailblastr.com/docs/api/emails-send).
