Developer Guides
A batch returned 200. Three of the messages failed.
You post a hundred messages in one request and get 200 back. Ninety-seven were accepted. Three were not, and nothing in the status code says so.
This is not a quirk. The request succeeded — it was received, parsed and processed. The outcome of each message inside it is a different question, answered in the body.
Why not fail the whole request
Because one bad address should not stop ninety-nine good messages. The alternative — all-or-nothing — means a single typo blocks an entire batch, and the retry re-sends everything including what already worked.
Per-message results are the right design. They just require reading them.
What the body contains
An array, in request order, each entry carrying either a message ID or an error. The order matters: it is how you map a result back to the input that produced it.
Do not assume failures are at the end, and do not assume the array is shorter when some fail. Walk it by index.
Handling the failures
Separate them by cause, because they want different responses:
- Invalid address — permanent. Do not retry. Fix the data.
- Suppressed recipient — permanent and correct. The address is on your suppression list for a reason. Not an error to fix.
- Rate limited — temporary. Retry that message later, with its own idempotency key.
- Template or validation error — a bug. Surface it rather than retrying.
Retrying correctly
Resend the failed messages individually, not the batch. The batch's idempotency key covers the batch: retrying it returns the stored result, including the same failures, and sends nothing.
Each resent message needs its own key. See idempotency keys for how to choose one.
Batch size
Large batches amortise request overhead and make partial failure harder to reason about. Somewhere between fifty and a few hundred is usually the sweet spot.
The real constraint is often the request size limit rather than the message count — a hundred messages with substantial HTML bodies is a large request.
The minimum you should write
Check the status. Then walk the results array, count failures, log each with its index and reason, and act on the ones that are actionable.
Code that checks the status and moves on will drop mail quietly for as long as it runs, and the first you hear of it is a customer saying they never got the message.
DKIM signing, SPF alignment and DMARC are set up the moment you verify a domain.