Message lifecycle
A message is one send. A delivery is one recipient’s copy of it: every address
across to, cc and bcc becomes its own delivery, tracked and counted separately.
The message carries an aggregate status; each delivery carries its own.
The path a delivery takes
Once a delivery reaches delivered, bounced or failed it stays there: a late or
out-of-order report cannot move it back. deferred is the one non-terminal outcome, so a
delivery sitting in it will still change.
Reading the three failure states
The distinction is what you should do, not how bad it looks.
bounced— do not send to this address again. The provider refused it permanently: the mailbox does not exist, the domain does not accept mail, or the message was rejected on policy. Retrying costs you sender reputation.deferred— do nothing. The provider asked us to come back later: a full mailbox, a greylisting delay, a rate limit at their end. Retries are ours to run; a second send from you just duplicates the message.failed— investigate, then decide. We retried until the queue expired and never got through. The address may be fine and the destination may have been down for hours. This is the one state where a fresh send can be the right answer.
The aggregate status
A message’s status is folded from its deliveries. With one recipient it is simply that
recipient’s status. With several it is their shared status, or mixed when they disagree.
mixed appears only on the message, never on a delivery. When you see it, read
deliveries — the aggregate deliberately does not pick a winner.
Filtering GET /v1/messages?status=... matches the aggregate status, so
status=mixed is a valid filter and status=bounced will not return the message above.
The event timeline
GET /v1/messages/{id} returns an events array: every status change across every
delivery of the message, oldest first. Each entry names the address it belongs to, so
a multi-recipient message interleaves its deliveries in one timeline.
A deferred entry can appear several times — one per refused attempt — and the timeline
then continues to whatever the retries reach. Treat a type you do not recognise as “no
status change” rather than an error: the set can grow, and the field is a string in the
schema for exactly that reason.
Following a send
Send, then poll the message. There is nothing to subscribe to yet.
A send that returns 202 is accepted, not delivered — the status at that moment is
accepted. Most deliveries reach a terminal status within seconds, but a deferred one
can take hours, so poll with backoff rather than in a tight loop, and stop once every
delivery is terminal.
For aggregate numbers over time, use GET /v1/stats instead of walking the
message list: it returns gap-filled per-day, per-week or per-month counters for each
status.
What is retained
Messages, deliveries and the event timeline stay readable for the full stored history.
The rendered body (GET /v1/messages/{id}/body) is the exception: it is kept for 30 days
after send, then removed, and the endpoint answers 404 message.body_unavailable
afterwards. See Limits & facts.