The event envelope and the payload of every Send event, from acceptance through delivery, bounce, complaint, and unsubscribe
Every delivery has the same outer shape:
{
"id": "evt_9c2a1d7b8e4c0f9a1b23f3",
"type": "send.email.sent",
"created": 1757000000000,
"livemode": true,
"data": { }
}| Field | Type | Description |
|---|---|---|
id | string | Unique event id, prefixed evt_. The same id is sent in the X-DoDevWebhook-Id header. Use it to de-duplicate. |
type | string | Event type. |
created | number | Unix time in milliseconds when the event occurred. |
livemode | boolean | true for real events. false for the synthetic platform.test event. |
data | object | Event-specific payload, described below. |
| Event | When | Status |
|---|---|---|
send.email.sent | The email was accepted and queued. | Live |
send.email.delivered | The recipient's mail server accepted the message. | Live |
send.email.bounced | The recipient's mail server rejected the message. | Live |
send.email.complained | The recipient reported the message as spam. | Live |
send.email.unsubscribed | A recipient used the hosted unsubscribe link. | Live |
send.email.failed | The message could not be handed to the mail provider. | Planned |
platform.test | You 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.sentEmitted 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 field | Type | Description |
|---|---|---|
messageId | string | The id returned by the send request. Fetch the full email with Get an email. |
from | object | { email, name? } as submitted. |
to | string[] | Recipients. cc and bcc are not included. |
subject | string | Subject 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.
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 field | Type | Description |
|---|---|---|
messageId | string | The send.dev message id (msg_…). Fetch the email with Get an email. |
sesMessageId | string | The provider's message id, useful when correlating with bounce reports. |
organizationId | string | Your account's organization id. The same value on every event for your account. |
from | string | The From address as sent. |
to | string[] | All recipients the message was addressed to. |
sentAt | string | ISO-8601 time the provider accepted the message. |
timestamp | string | ISO-8601 time of the delivery, bounce, or complaint itself. |
The event-specific fields follow.
send.email.deliveredThe 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 field | Type | Description |
|---|---|---|
recipients | string[] | The recipients this delivery notification covers. |
smtpResponse | string | The receiving server's acceptance line. |
processingTimeMillis | number | Time 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.bouncedThe 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 field | Type | Description |
|---|---|---|
bounceType | string | Permanent, Transient, or Undetermined. Only Permanent bounces are suppressed. |
bounceSubType | string | The provider's classification, for example General, NoEmail, Suppressed, MailboxFull. |
bouncedRecipients | object[] | One entry per rejected recipient: email, and when the receiving server supplied them, action, status (the enhanced status code), and diagnosticCode. |
send.email.complainedThe 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 field | Type | Description |
|---|---|---|
complaintFeedbackType | string | The feedback-loop category when the provider supplies one, most often abuse. Absent otherwise. |
complainedRecipients | string[] | The recipients who complained. |
send.email.unsubscribedEmitted 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 field | Type | Description |
|---|---|---|
email | string | The address that unsubscribed, lowercased. |
listId | string | The listId you set on the email the link came from. Absent if you did not set one. |
source | string | one-click when the mail client used the List-Unsubscribe-Post header; link when the recipient clicked the footer link. |
organizationId | string | Your account's organization id. |
unsubscribedAt | string | ISO-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.testSent 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"
}
}send.email.failedAn 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.
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.