Webhook Events

The event envelope and the payload of every Send event, from acceptance through delivery, bounce, complaint, and unsubscribe

Envelope

Every delivery has the same outer shape:

{
  "id": "evt_9c2a1d7b8e4c0f9a1b23f3",
  "type": "send.email.sent",
  "created": 1757000000000,
  "livemode": true,
  "data": { }
}
FieldTypeDescription
idstringUnique event id, prefixed evt_. The same id is sent in the X-DoDevWebhook-Id header. Use it to de-duplicate.
typestringEvent type.
creatednumberUnix time in milliseconds when the event occurred.
livemodebooleantrue for real events. false for the synthetic platform.test event.
dataobjectEvent-specific payload, described below.

Event types

EventWhenStatus
send.email.sentThe email was accepted and queued.Live
send.email.deliveredThe recipient's mail server accepted the message.Live
send.email.bouncedThe recipient's mail server rejected the message.Live
send.email.complainedThe recipient reported the message as spam.Live
send.email.unsubscribedA recipient used the hosted unsubscribe link.Live
send.email.failedThe message could not be handed to the mail provider.Planned
platform.testYou called the test endpoint.Live

sent comes from the API at acceptance. delivered, bounced, and complained are published from the mail provider's delivery notifications, so they arrive seconds to hours after sent, and not necessarily in that order. unsubscribed comes from the hosted unsubscribe endpoint and is not tied to a message.

send.email.sent

Emitted when an email is accepted and queued.

{
  "id": "evt_9c2a1d7b8e4c0f9a1b23f3",
  "type": "send.email.sent",
  "created": 1757000000000,
  "livemode": true,
  "data": {
    "messageId": "msg_3f9c2a1d7b8e4c0f9a1b2",
    "from": { "email": "receipts@mail.example.com", "name": "Example Store" },
    "to": ["customer@example.net"],
    "subject": "Your order #4821 has shipped"
  }
}
data fieldTypeDescription
messageIdstringThe id returned by the send request. Fetch the full email with Get an email.
fromobject{ email, name? } as submitted.
tostring[]Recipients. cc and bcc are not included.
subjectstringSubject as sent.

Because this event fires at acceptance, it tells you the same thing the 202 response did. It is most useful when the system that sends email is not the system that needs to know about it, for example when a queue worker sends and a CRM records.

Delivery events

send.email.delivered, send.email.bounced, and send.email.complained are published from the mail provider's notifications. One event is emitted per notification, and a notification can cover more than one recipient of the same message. All three share a common data prefix:

data fieldTypeDescription
messageIdstringThe send.dev message id (msg_…). Fetch the email with Get an email.
sesMessageIdstringThe provider's message id, useful when correlating with bounce reports.
organizationIdstringYour account's organization id. The same value on every event for your account.
fromstringThe From address as sent.
tostring[]All recipients the message was addressed to.
sentAtstringISO-8601 time the provider accepted the message.
timestampstringISO-8601 time of the delivery, bounce, or complaint itself.

The event-specific fields follow.

send.email.delivered

The recipient's mail server accepted the message. This is the same moment the email's status becomes delivered.

{
  "id": "evt_1d7b8e4c0f9a1b23f9c2a",
  "type": "send.email.delivered",
  "created": 1757000003950,
  "livemode": true,
  "data": {
    "messageId": "msg_3f9c2a1d7b8e4c0f9a1b2",
    "sesMessageId": "0100019...-000000",
    "organizationId": "org_01J8...",
    "from": "receipts@mail.example.com",
    "to": ["customer@example.net"],
    "sentAt": "2026-09-05T14:00:01.200Z",
    "timestamp": "2026-09-05T14:00:03.900Z",
    "recipients": ["customer@example.net"],
    "smtpResponse": "250 2.0.0 OK",
    "processingTimeMillis": 2700
  }
}
data fieldTypeDescription
recipientsstring[]The recipients this delivery notification covers.
smtpResponsestringThe receiving server's acceptance line.
processingTimeMillisnumberTime from provider acceptance to delivery.

Not every receiving server reports delivery back, so a message can stay at sent without ever producing this event.

send.email.bounced

The recipient's mail server rejected the message. Every recipient in the notification is listed in bouncedRecipients, and hard-bounced addresses are added to your suppression list at the same time.

{
  "id": "evt_8e4c0f9a1b23f9c2a1d7b",
  "type": "send.email.bounced",
  "created": 1757000004150,
  "livemode": true,
  "data": {
    "messageId": "msg_3f9c2a1d7b8e4c0f9a1b2",
    "sesMessageId": "0100019...-000000",
    "organizationId": "org_01J8...",
    "from": "receipts@mail.example.com",
    "to": ["gone@example.net"],
    "sentAt": "2026-09-05T14:00:01.200Z",
    "timestamp": "2026-09-05T14:00:04.100Z",
    "bounceType": "Permanent",
    "bounceSubType": "General",
    "bouncedRecipients": [
      {
        "email": "gone@example.net",
        "action": "failed",
        "status": "5.1.1",
        "diagnosticCode": "smtp; 550 5.1.1 user unknown"
      }
    ]
  }
}
data fieldTypeDescription
bounceTypestringPermanent, Transient, or Undetermined. Only Permanent bounces are suppressed.
bounceSubTypestringThe provider's classification, for example General, NoEmail, Suppressed, MailboxFull.
bouncedRecipientsobject[]One entry per rejected recipient: email, and when the receiving server supplied them, action, status (the enhanced status code), and diagnosticCode.

send.email.complained

The recipient reported the message as spam through their mail provider's feedback loop. The address is added to your suppression list with reason complaint.

{
  "id": "evt_0f9a1b23f9c2a1d7b8e4c",
  "type": "send.email.complained",
  "created": 1757069564100,
  "livemode": true,
  "data": {
    "messageId": "msg_3f9c2a1d7b8e4c0f9a1b2",
    "sesMessageId": "0100019...-000000",
    "organizationId": "org_01J8...",
    "from": "receipts@mail.example.com",
    "to": ["customer@example.net"],
    "sentAt": "2026-09-05T14:00:01.200Z",
    "timestamp": "2026-09-06T09:12:44.000Z",
    "complaintFeedbackType": "abuse",
    "complainedRecipients": ["customer@example.net"]
  }
}
data fieldTypeDescription
complaintFeedbackTypestringThe feedback-loop category when the provider supplies one, most often abuse. Absent otherwise.
complainedRecipientsstring[]The recipients who complained.

send.email.unsubscribed

Emitted when a recipient unsubscribes through the hosted link or one-click header on an email you sent with unsubscribe support. The address is added to your suppression list with reason unsubscribe. There is no messageId: the unsubscribe is a fact about the recipient, not about one message.

{
  "id": "evt_23f9c2a1d7b8e4c0f9a1b",
  "type": "send.email.unsubscribed",
  "created": 1757100000000,
  "livemode": true,
  "data": {
    "email": "customer@example.net",
    "listId": "weekly-digest",
    "source": "one-click",
    "organizationId": "org_01J8...",
    "unsubscribedAt": "2026-09-05T18:40:00.000Z"
  }
}
data fieldTypeDescription
emailstringThe address that unsubscribed, lowercased.
listIdstringThe listId you set on the email the link came from. Absent if you did not set one.
sourcestringone-click when the mail client used the List-Unsubscribe-Post header; link when the recipient clicked the footer link.
organizationIdstringYour account's organization id.
unsubscribedAtstringISO-8601 time the unsubscribe was recorded.

The event fires once per address (or per address and listId). A recipient clicking the link a second time is a no-op and does not emit again.

platform.test

Sent when you call POST /v1/webhooks/{id}/test. livemode is false.

{
  "id": "evt_0f9a1b23f9c2a1d7b8e4c",
  "type": "platform.test",
  "created": 1757000000000,
  "livemode": false,
  "data": {
    "message": "This is a test webhook event from do.dev",
    "endpoint_id": "whe_2m8x9q4wzk17c"
  }
}

Planned: send.email.failed

An event for messages the provider refused (the failed and failed_permanent statuses in Get an email) is planned but not emitted yet. Subscribing with send.email.* means you will receive it without changing your endpoint when it ships. Until then, poll the email's status for failures.

Handling unknown types

Write your handler so that an unrecognized type is acknowledged with 200 and logged, not rejected. New event types will be added, and a handler that returns 4xx or 5xx for them causes needless retries.