Events
Subscribe to any combination of these event types, or use * to receive all of them.
Subscribe to any combination of these event types, or use * to receive all of
them.
| Event | Description |
|---|---|
email.sent | Email was accepted and is in transit. |
email.delivered | Delivery confirmed by the receiving mail server. |
email.bounced | Email hard-bounced (permanent failure) and the address was suppressed. The payload includes bounce_type plus the receiving server's verbatim reason — see Reading a bounce. Transient/soft failures are retried and do not fire this event. |
email.complained | Recipient marked the email as spam. |
email.opened | Recipient opened the email (requires track_opens). Fires once per email. |
email.clicked | Recipient clicked a tracked link (requires track_clicks). Payload includes link_id and url. |
* | Wildcard — subscribes to all current and future event types. |
Payload structure
{
"type": "email.delivered",
"email_id": "9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d",
"recipients": ["alice@example.com"],
"tags": { "category": "shipping_update" },
"timestamp": "2026-05-20T10:00:03.000Z"
}timestamp is when the event itself happened — the moment our MTA recorded the delivery, or
the arrival date on an async bounce/complaint report — not when we sent you the webhook. For
delivery time, read the webhook-timestamp header; the two differ when a retry or a backlog
delays the notification.
Every payload includes type, email_id, tags, and timestamp. Some events add fields:
provider_message_id (sent), recipients (delivered, bounced, complained), bounce_type +
diagnostic (bounced), and link_id + url (clicked). Deliveries from a test-mode send also
carry test_mode: true.
Reading a bounce
bounce_type is our own classifier's bucket, and it is frequently
Uncategorized — a real rejection we have no specific rule for. It is not enough
on its own to tell an invalid mailbox from a reputation block, so a bounce payload
also carries the receiving server's own words:
{
"type": "email.bounced",
"email_id": "9a8b7c6d-5e4f-4a3b-8c1d-0e9f8a7b6c5d",
"bounce_type": "Uncategorized",
"smtp_code": 554,
"diagnostic": "Your access to this mail system has been rejected due to poor reputation of a domain used in message transfer",
"recipients": ["alice@example.com"],
"tags": {},
"timestamp": "2026-05-20T10:00:03.000Z"
}| Field | Notes |
|---|---|
smtp_code | Numeric SMTP reply code (550, 554, …). Absent when the bounce arrived asynchronously as a DSN rather than on the SMTP session. |
diagnostic | The remote server's verbatim reason, trimmed to 500 characters. Absent when the remote sent no text. |
dsn_status | Dotted status (5.1.1) — present only on asynchronous DSN bounces, in place of smtp_code. |
Treat both as opaque text for humans and logs. Wording varies by provider and changes without
notice, so match on bounce_type or smtp_code when you need to branch in code — never on
diagnostic.
tags carries whatever tags the send was created with, as an
object — {} when it had none, never absent — so you can route on
payload.tags.category without checking the field exists first.