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.
email.failedThe email will not be delivered and was not rejected by a recipient server: the account could not send when it was picked up, delivery was abandoned after retries, or it never reached the queue. Payload includes reason.
email.suppressedRecipients on your suppression list were dropped and not sent to. Fires for an email whose recipients were all suppressed, and for the record of recipients removed from a partly suppressed send. Payload includes recipients.
*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, suppressed), bounce_type + diagnostic (bounced), link_id + url (clicked), and reason (failed). 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.