eusend
Webhooks

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.

EventDescription
email.sentEmail was accepted and is in transit.
email.deliveredDelivery confirmed by the receiving mail server.
email.bouncedEmail 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.complainedRecipient marked the email as spam.
email.openedRecipient opened the email (requires track_opens). Fires once per email.
email.clickedRecipient clicked a tracked link (requires track_clicks). Payload includes link_id and url.
*Wildcard — subscribes to all current and future event types.

Payload structure

example payload (email.delivered)
{
  "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:

example payload (email.bounced)
{
  "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"
}
FieldNotes
smtp_codeNumeric SMTP reply code (550, 554, …). Absent when the bounce arrived asynchronously as a DSN rather than on the SMTP session.
diagnosticThe remote server's verbatim reason, trimmed to 500 characters. Absent when the remote sent no text.
dsn_statusDotted 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.