Documentation
Sign inGet started

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.
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 nested envelope described in the Webhooks guide — 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.
FieldDescription
typeOne of the event types on this page, e.g. email.delivered
timestampWhen 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
dataEvent-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.
Przykład kodu
{
  "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_class, bounce_code (SMTP reply code, e.g. 550), bounce_description (human-readable reason), and sending_ip. Hard bounces automatically suppress 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, 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_reasonMeaning
recipient_suppressedThe recipient is on the workspace suppression list — Bird did not attempt delivery
transmission_failedThe message could not be transmitted for delivery
generation_failureThe message could not be built for delivery (template or content issue)
policy_rejectionThe 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 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 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 for non-transactional mail.

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_classbounce_typeMeaning
1undeterminedThe receiving server's response was ambiguous
10, 30hardPermanent failure — invalid address or non-existent domain
2024, 40, 70, 100softTransient failure — mailbox full, server temporarily unavailable, DNS or routing trouble
25adminAdministrative refusal — relaying denied, blocklisted domain
5054blockThe 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, and each addition emits an email_suppression.created event:
  • 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.