Email events
We emit an event at each step of a recipient's delivery. Events are per-recipient facts, so a send to three addresses produces three independent streams, each correlated by email_id plus recipient_id. This page is the event vocabulary. How events reach your endpoint (signatures, retries, ordering, replay) is covered in the Webhooks guide.
Every recipient starts at email.accepted, then email.processed. From there the send either lands (email.delivered), gets deferred and retried (email.deferred, which resolves to delivered or bounced), is refused by the receiving server (email.bounced), or never gets a delivery attempt at all (email.rejected). After a delivery the stream can continue with email.out_of_band_bounce, email.complained, email.opened, email.clicked, email.unsubscribed, and email.list_unsubscribed.
Each recipient ends in exactly one terminal status, delivered, bounced, complained, or rejected, returned as the per-recipient status from GET /v1/email/messages/{message_id}/recipients. Engagement events never change it: a recipient who opened is still delivered. A late bounce report does change it, because the receiving server is retracting an acceptance it already gave, so the recipient moves from delivered to bounced. The message as a whole has its own roll-up status and per-state counts on GET /v1/email/messages/{message_id}.
The event envelope
Events arrive as the three-field envelope every webhook uses: type, timestamp (when the event occurred, RFC 3339), and a type-specific data object.
कोड उदाहरण
{
"type": "email.delivered",
"timestamp": "2026-07-23T14:51:47.107Z",
"data": {
"email_id": "em_01ky7qc398fmxraqtxn604zeq9",
"recipient_id": "er_01ky7qc398fmwsc96nt6anrbqs",
"workspace_id": "ws_01ky7m21hjffh9s8kq76gyb1xf",
"recipient": "delivered@messagebird.dev",
"recipient_role": "to",
"tags": [{ "name": "category", "value": "welcome" }],
"metadata": { "order_id": "ord_123" }
}
}Every outbound email event's data object has that identity base: email_id, recipient_id, workspace_id, the recipient address, and its envelope recipient_role (to, cc, or bcc), plus the tags and metadata from the send request, echoed on every event so you can route and correlate against your own records without a second lookup. Each is null when the send had none. Event types with more to say add the fields listed below.
The same events are queryable after the fact from GET /v1/email/messages/{message_id}/events, where each one also has an id (ev_ prefix) and an occurred_at. Use it to backfill, replay, or reconcile against what your endpoint received. A few fields reach you only through that API rather than the webhook, and each is noted below.
Lifecycle events
email.accepted
We have accepted the send and started preparing delivery. Fires once per requested recipient and is the first event in every stream. Payload: the identity base only.
email.processed
The message is built and queued for delivery to the recipient's mail server. Payload: the identity base only. Comparing this event's timestamp with email.accepted's gives you our own processing time.
email.delivered
The receiving mail server accepted the message and took responsibility for it. What happened afterwards is what the engagement events tell you. Payload: the identity base only over the webhook; the events API adds sending_ip, the address the message was sent from, which matters when a deliverability problem tracks one IP.
email.deferred
A temporary failure: the receiving server asked us to try again later (full mailbox, greylisting, rate limiting). We retry automatically, and the recipient eventually resolves to email.delivered or email.bounced, so this event is informational rather than terminal, and a recipient can defer several times first. Payload: bounce_type, bounce_class, defer_reason (the reason the server gave), and sending_ip.
Failure events
email.bounced
A permanent failure at SMTP time: the receiving server refused the message, and the recipient's terminal status becomes bounced. Payload: bounce_type (see the classification table), bounce_class, bounce_code (the SMTP reply code, for example 550), bounce_description (the reason the server gave), and sending_ip. A hard bounce suppresses the address.
email.out_of_band_bounce
A late bounce: the receiving server accepted the message at SMTP time and then sent a bounce report afterwards. It has the same classification as email.bounced (bounce_type, bounce_class, bounce_code, bounce_description, sending_ip). When the report classifies as a bounce (any class in the table), the server has retracted its earlier acceptance, so the recipient moves from delivered to bounced. Reports whose class is not in the table, such as auto-replies, are recorded on the timeline and leave the status alone. A hard out-of-band bounce also suppresses the address.
email.rejected
The recipient never reached the remote mail server, so no delivery was attempted. That is what separates a rejection from a bounce, where the receiving server is the one saying no. Payload: rejection_reason, also on the recipient record, one of:
| rejection_reason | Meaning |
|---|---|
| recipient_suppressed | The recipient is on the workspace suppression list, so delivery was never attempted |
| transmission_failed | The message could not be transmitted for delivery |
| generation_failure | The message could not be built for delivery, a template or content problem |
| policy_rejection | Sending policy refused the message |
| domain_unverified | The sending domain was not verified |
| quota_exceeded | The organization's send quota was reached |
| recipient_not_allowed | The recipient was not permitted for this send; sends on a shared onboarding domain reach only verified members of your workspace |
email.complained
The recipient marked the message as spam and the mailbox provider reported it back through its feedback loop. Complaints arrive after delivery and set the terminal status to complained. Payload: feedback_type, the kind of report the provider sent, such as abuse or fraud, and null when the provider did not say. A complaint suppresses the address for marketing mail. Keep your complaint rate low: providers throttle senders who collect reports.
Engagement events
email.opened
The tracking pixel in the message body loaded. Payload: ip_address and user_agent when known; the events API adds is_prefetched and country (ISO 3166-1 alpha-2, derived from the client IP). Check is_prefetched before you count an open. It is true when an inbox privacy feature fetched the pixel automatically rather than a person opening the message, and counting those inflates your open rate. Open and click tracking covers the instrumentation.
email.clicked
The recipient clicked a tracked link. Payload: url (the link clicked), ip_address, and user_agent when known; the events API adds country. Clicks are the engagement signal to trust: privacy proxies load pixels, and they rarely follow links.
email.unsubscribed
The recipient used the unsubscribe link in the message body. Payload: the identity base only. Suppresses the address for marketing mail. Unsubscribe links covers how the link gets into your mail.
email.list_unsubscribed
The recipient used the one-click unsubscribe button the mailbox provider renders in its own UI, driven by the message's List-Unsubscribe headers. Payload: the identity base only; the mechanism is the event type itself, which is why it is separate from email.unsubscribed. Also suppresses the address for marketing mail.
Message-level events
Two events describe the message as a whole rather than one recipient, so their data has email_id, workspace_id, tags, and metadata but no recipient identity. Both belong to scheduled sending.
email.scheduled
We accepted a send with a scheduled_at in the future. Payload: the message-level base plus scheduled_at. When that time arrives, the per-recipient lifecycle starts at email.accepted.
email.canceled
A scheduled message was canceled before it went out, so it produces no recipient lifecycle events at all. Payload: the message-level base only.
Inbound and mailbox events
email.received is the one event about mail coming in on the receiving side: it fires when we receive and parse an inbound message, and its payload has the inbound_message_id, the addressing, the subject, and the authentication verdicts. Setup, payload, and the fetch-back API are in Receiving email. A mailbox has its own email_mailbox.* family on top of that, covered in the mailboxes guide.
Bounce classification
bounce_class is the numeric bounce classification included on email.bounced, email.out_of_band_bounce, and email.deferred. It rolls up into the coarse bounce_type and keeps the fine-grained code, so you can still tell a full mailbox from a routing failure even though both report as soft:
| bounce_class | bounce_type | Meaning |
|---|---|---|
| 1 | undetermined | The receiving server's response was ambiguous |
| 10, 30 | hard | Permanent failure: invalid address, or a domain that does not exist |
| 20 to 24, 40, 70, 100 | soft | Transient failure: full mailbox, server temporarily unavailable, DNS or routing trouble |
| 25 | admin | Administrative refusal: relaying denied, blocklisted domain |
| 50 to 54 | block | The receiving server refused the sending IP |
Any class outside this list maps to undetermined. Only hard bounces suppress the address; soft, block, admin, and undetermined do not, because the address may still be deliverable.
Auto-suppression
Three things add a recipient to the workspace suppression list automatically, and they block different mail:
| Event | Suppression reason | What it blocks |
|---|---|---|
| email.bounced or email.out_of_band_bounce with bounce_type: "hard" | hard_bounce | All mail, transactional included |
| email.complained | complaint | Marketing mail; transactional still sends |
| email.unsubscribed or email.list_unsubscribed | unsubscribe | Marketing mail; transactional still sends |
A hard bounce blocks everything because the address itself is gone. A complaint or an unsubscribe blocks marketing only, because someone who opted out of your newsletter still needs their password reset.
Each addition fires an email_suppression.created event that has the suppression_id, the suppressed email, the reason, and the workspace_id. The full record schema and how to manage entries by hand are in the Suppressions guide.
Later sends to a suppressed address are rejected up front as email.rejected with rejection_reason: "recipient_suppressed", and never count against your deliverability.
Next steps
- Webhooks & events: endpoint setup, signature verification, retries, and replay
- Suppressions: how the suppression list works and how to manage it
- Unsubscribe links: wiring up the paths behind email.unsubscribed and email.list_unsubscribed
- Testing & sandbox: sandbox sends emit real events through the normal path, which is the cheapest way to exercise your handler