Batch Delivery Modes
How POST /emails/batch decides between sending inline (200) and queueing for background delivery (202).
/emails/batchA batch send 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.
@mailblastr.dev) is always sent inline, whatever its size, because those addresses are simulated rather than transmitted.The 202 response
{
"data": [
{ "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c" },
{ "id": "3a9f8c2b-1e5d-4f8a-9c7b-2d6e5f8a9c7b" }
],
"queued": true,
"queued_count": 100
}dataobject[]optionalOne { id } per email, in the order you submitted them. Same field as the 200 response.
queuedbooleanoptionalPresent and true only on a queued (202) batch. Absent on an inline 200.
queued_countnumberoptionalHow 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 — the current
statusand event log for one email. - GET /emails — page through them, optionally filtered with
status. - POST /emails/:id/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 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. |
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.
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:
{
"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).
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. batch_incomplete is specific to this endpoint; every other name a batch can return is shared with POST /emails.