# Email events reference

Every recipient on a send moves through a lifecycle, and Bird emits an event at each step. Events are per-recipient facts: a send to three addresses produces three independent event streams, each correlated by `email_id` + `recipient_id`. This page is the full event vocabulary; how events are delivered to your endpoint — signatures, retries, replay — is covered in the [Webhooks guide](/docs/guides/webhooks).

The delivery lifecycle, as a path through the event types:

- `email.accepted` — Bird has the send and is preparing to deliver
- → `email.processed` — the message is built and queued for delivery
- → `email.delivered` — the recipient's mail server accepted it
- → or `email.deferred` — temporary failure; Bird retries, ending in `email.delivered` or `email.bounced`
- → or `email.bounced` — permanent failure; the recipient's terminal status is `bounced`
- → or `email.rejected` — the recipient was rejected before delivery was attempted (e.g. suppressed); distinct from a bounce
- After `email.delivered`, 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 delivery status — `delivered`, `bounced`, `complained`, or `rejected` — the per-recipient `status` returned by `GET /v1/email/messages/{message_id}/recipients`. (`GET /v1/email/messages/{message_id}` itself returns the aggregate message status and per-state counts.) Engagement events (`opened`, `clicked`, unsubscribes) and `out_of_band_bounce` never change that terminal status.

## The event envelope

Events arrive at your webhook endpoint in the [Standard Webhooks](https://www.standardwebhooks.com) nested envelope described in the [Webhooks guide](/docs/guides/webhooks#event-catalog) — exactly three fields: `type`, `timestamp`, and a type-specific `data` object. The event's identity is not in the body: it rides in the `webhook-id` HTTP header, which is stable across retries of the same delivery and is your deduplication key.

| Field       | Description                                                                                                                                                                                                                      |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`      | One of the event types on this page, e.g. `email.delivered`                                                                                                                                                                      |
| `timestamp` | When the event occurred (RFC 3339) — sort by this, never by arrival order, since deliveries are not ordered. Distinct from the `webhook-timestamp` header, which is the time of this delivery attempt and changes on every retry |
| `data`      | Event-specific payload (fields below)                                                                                                                                                                                            |

For outbound email events, `data` always carries the identity base: `email_id`, `recipient_id`, `workspace_id`, the `recipient` address, and its envelope `recipient_role` (to/cc/bcc) — plus the `tags` and `metadata` you provided on the send, echoed on every event so you can route and correlate deliveries against your own records without an extra lookup (each is `null` when the send carried none). Within an event type the field set is stable — required fields, no surprises.

```json
{
  "type": "email.delivered",
  "timestamp": "2026-06-10T14:30:00Z",
  "data": {
    "email_id": "em_01krdgeqcxet5s7t44vh8rt9mg",
    "recipient_id": "er_01krdgeqcxet5s7t44vh8rt9mg",
    "workspace_id": "ws_01krdgeqcxet5s7t44vh8rt9mg",
    "recipient": "user@example.com",
    "recipient_role": "to",
    "tags": [{ "name": "category", "value": "welcome" }],
    "metadata": { "order_id": "ord_123" }
  }
}
```

Each event type's webhook `data` carries the fields listed for it below. The same per-recipient events are also queryable after the fact with `GET /v1/email/messages/{message_id}/events`, where each event carries an `id` (`ev_` prefix), `type`, `occurred_at`, `recipient_id`, and the same type-specific detail. Use the events API to backfill, replay, or reconcile against your webhook deliveries by `email_id` + `recipient_id`. A few fields are surfaced only through the events API (noted on the relevant event below), such as `is_prefetched` and `country`.

## Lifecycle events

### email.accepted

Fires once per requested recipient when Bird accepts the send and begins preparing delivery. This is the first event in every recipient's stream. Webhook payload: the identity base only.

### email.processed

Fires once per recipient when Bird has processed the message and queued it for delivery to the recipient's mail server. Webhook payload: the identity base only — to measure processing latency, compare this event's `timestamp` with `email.accepted`'s.

### email.delivered

The recipient's mail server accepted the message. Webhook payload: the identity base only; the `sending_ip` Bird sent from — useful when investigating deliverability issues that correlate with specific IPs — is on the event via the events API. Delivery means the remote server took responsibility for the message; engagement events tell you what happened after.

### email.deferred

A temporary delivery failure — the recipient's mail server asked Bird to try again later (mailbox full, greylisting, rate limiting). Bird retries automatically; a deferred recipient eventually resolves to `email.delivered` or `email.bounced`, so treat this event as informational, not terminal. Webhook payload: `bounce_type`, `bounce_class`, `defer_reason` (the reason the receiving server gave), and `sending_ip`. A recipient can defer multiple times before resolving.

## Failure events

### email.bounced

A permanent delivery failure at SMTP time — the recipient's mail server refused the message. The recipient's terminal status becomes `bounced`. Webhook payload: the bounce classification — `bounce_type` (see the [classification table](#bounce-classification)), `bounce_class`, `bounce_code` (SMTP reply code, e.g. `550`), `bounce_description` (human-readable reason), and `sending_ip`. Hard bounces automatically [suppress](#auto-suppression) the address.

### email.out_of_band_bounce

A late bounce notification that arrived asynchronously **after** the message was already delivered — the receiving server accepted the message at SMTP time, then sent a bounce report afterwards. It carries the same bounce classification as `email.bounced` (`bounce_type`, `bounce_class`, `bounce_code`, `bounce_description`, `sending_ip`), but it does **not** change the recipient's terminal status: the recipient stays `delivered`, and the out-of-band bounce is recorded as an additional fact on the timeline. Hard out-of-band bounces still trigger [auto-suppression](#auto-suppression), so future sends to the address are blocked even though this send's status is unchanged.

### email.rejected

The recipient was rejected before the message ever reached the remote mail server — no delivery attempt was made. This is distinct from `email.bounced`: a bounce is the receiving server saying no, a rejection is the message never getting that far. Webhook payload: `rejection_reason` (also on the recipient record), one of:

| `rejection_reason`     | Meaning                                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `recipient_suppressed` | The recipient is on the workspace [suppression list](/docs/guides/email/suppressions) — Bird did not attempt delivery |
| `transmission_failed`  | The message could not be transmitted for delivery                                                                     |
| `generation_failure`   | The message could not be built for delivery (template or content issue)                                               |
| `policy_rejection`     | The message was refused by sending policy                                                                             |

### email.complained

The recipient marked the message as spam, reported back to Bird through the mailbox provider's feedback loop. Complaints arrive after delivery and set the recipient's terminal status to `complained`. Webhook payload: `feedback_type` (the kind of report the provider sent, such as `abuse`). A complaint automatically [suppresses](#auto-suppression) the address — keep complaint rates low, since mailbox providers throttle senders whose mail gets reported.

## Engagement events

Engagement events never change the recipient's delivery status — a recipient who opened is still `delivered`.

### email.opened

The tracking pixel in the message body loaded. Webhook payload: `ip_address` and `user_agent` (when known). The events API additionally exposes `is_prefetched` and `country` (ISO 3166-1 alpha-2, derived from the client IP). **Check `is_prefetched` before counting opens**: it is `true` when the open was auto-fetched by an inbox privacy feature (Apple Mail Privacy Protection, Gmail's image proxy) rather than a real user action — counting prefetched opens inflates open rates.

### email.clicked

The recipient clicked a tracked link. Webhook payload: `url` (the clicked URL), `ip_address`, and `user_agent` (when known). The events API additionally exposes `country`. A click is the strongest engagement signal — privacy prefetchers load pixels, but they rarely follow links.

### email.unsubscribed

The recipient unsubscribed via a tracked unsubscribe link in the message body. Triggers [auto-suppression](#auto-suppression) for non-transactional mail.

### email.list_unsubscribed

The recipient unsubscribed via the RFC 8058 one-click List-Unsubscribe mechanism — the unsubscribe button mailbox providers render in their own UI, driven by the message's `List-Unsubscribe` headers. Webhook payload: the identity base only — the mechanism is the event type itself. Distinct from `email.unsubscribed` so you can tell provider-rendered unsubscribes apart from clicks on your own footer link. Also triggers [auto-suppression](#auto-suppression) for non-transactional mail.

## The receiving event

`email.received` is the one event about mail coming _into_ Bird rather than going out: it fires when Bird receives and parses an inbound message, and its payload carries the `inbound_message_id`, addressing, subject, and authentication verdicts. It belongs to the receiving pipeline, not the send lifecycle above — setup, payload, and the fetch-back API are covered in [Receiving email](/docs/guides/email/receiving-email).

## Bounce classification

`bounce_class` is Bird's numeric bounce classification, carried on `email.bounced`, `email.out_of_band_bounce`, and `email.deferred` events. It rolls up into the coarse `bounce_type`, but keeps the fine-grained code so you can distinguish, say, a DNS failure from a spam block when both map to the same type:

| `bounce_class`               | `bounce_type`  | Meaning                                                                                  |
| ---------------------------- | -------------- | ---------------------------------------------------------------------------------------- |
| `1`                          | `undetermined` | The receiving server's response was ambiguous                                            |
| `10`, `30`                   | `hard`         | Permanent failure — invalid address or non-existent domain                               |
| `20`–`24`, `40`, `70`, `100` | `soft`         | Transient failure — mailbox full, server temporarily unavailable, DNS or routing trouble |
| `25`                         | `admin`        | Administrative refusal — relaying denied, blocklisted domain                             |
| `50`–`54`                    | `block`        | The receiving server blocked the sending IP for reputation reasons                       |

Any class outside this list maps to `undetermined`. Only `hard` bounces trigger auto-suppression; `soft`, `block`, `admin`, and `undetermined` bounces do not, since the address may still be deliverable.

## Auto-suppression

Three event classes automatically add the recipient to the workspace [suppression list](/docs/guides/email/suppressions). The `email_suppression.created` event is reserved for these additions but is not yet delivered to webhooks -- its payload is not finalized, so a subscription to it will not fire for now. Drive suppression handling off the underlying lifecycle events below until it ships:

- **Hard bounces** (`email.bounced` or `email.out_of_band_bounce` with `bounce_type: "hard"`) — suppressed for all mail
- **Spam complaints** (`email.complained`) — suppressed for non-transactional mail
- **Unsubscribes** (`email.unsubscribed` or `email.list_unsubscribed`) — suppressed for non-transactional mail

Subsequent sends to a suppressed address are rejected up front with `email.rejected` and `rejection_reason: "recipient_suppressed"` — they never count against your deliverability.

## Next steps

- [Webhooks & events](/docs/guides/webhooks) — endpoint setup, signature verification, retries, and replay
- [Webhooks API reference](/docs/api/reference/create-webhook) — full endpoint and schema documentation
- [Suppressions](/docs/guides/email/suppressions) — how the suppression list works and how to manage it
- [Unsubscribe links](/docs/guides/email/unsubscribe-links) — wiring up the unsubscribe paths behind `email.unsubscribed` and `email.list_unsubscribed`
- [Testing & sandbox](/docs/guides/email/testing-sandbox) — sandbox sends emit real events through the normal delivery path, ideal for exercising your handler