Webhooks
Event types
Every webhook event Sooner sends, what triggers it, and the fields in its data object.
Subscribe a webhook to the events you need with the events field, or use "*" for all of them. Every event uses the same envelope with type, created_at and data. See Webhooks overview.
Email events
Email events share these data fields:
| Field | Type | Description |
|---|---|---|
email_id | string | The ID of the email. |
from | string | The sender. |
to | string[] | The recipients. |
subject | string | The subject. |
created_at | string | When the email was created, as ISO 8601. |
tags | object | The email's tags as { "name": "value" }. Left out when the email has none. |
broadcast_id | string | The broadcast the email belongs to. Only on broadcast emails. |
template_id | string | The template the email was sent with. Only when one was used. |
| Event | When it fires | Extra data fields |
|---|---|---|
email.sent | Sooner handed the email to the mail network. | None. |
email.delivered | The receiving server accepted the email. | None. |
email.delivery_delayed | The receiving server has not accepted it yet. Delivery is still being attempted. | delay.type |
email.bounced | The receiving server rejected the email. | bounce.type, bounce.subType, bounce.message |
email.complained | The recipient marked the email as spam. | complaint.type |
email.opened | The recipient opened the email. Needs open tracking. | open.ipAddress, open.userAgent |
email.clicked | The recipient clicked a link. Needs click tracking. | click.link, click.ipAddress, click.userAgent |
email.failed | Sooner could not send the email. | failed.reason |
email.scheduled | A scheduled email was accepted. It fires once, when you create it. | None. |
email.canceled | A scheduled email was canceled. | None. |
email.suppressed | The email was accepted but not sent, because every recipient is suppressed or opted out. | suppressed.type, suppressed.message |
Suppressed emails
{
"type": "email.suppressed",
"created_at": "2026-11-14T10:30:58.000Z",
"data": {
"email_id": "01jcz8w3f2x9k4m7n6p5q2r1s0",
"from": "Acme <orders@mail.yourcompany.com>",
"to": ["nobody@example.com"],
"subject": "Your order is confirmed",
"created_at": "2026-11-14T10:30:58.000Z",
"tags": { "category": "order_confirmation" },
"suppressed": {
"type": "OnAccountSuppressionList",
"message": "Sooner has suppressed sending to this address because it is on the account-level suppression list."
}
}
}suppressed.type | Meaning |
|---|---|
OnAccountSuppressionList | Every recipient is on your suppression list. |
UnsubscribedFromTopic | The recipient opted out of the email's topic. |
The event fires when the API accepts the request. If only some recipients are suppressed, the email is sent to the rest and there is no email.suppressed event. If every recipient becomes suppressed between the request and the send, you get email.failed instead.
email.canceled and the UnsubscribedFromTopic type are Sooner additions. Resend does not send them.
Bounce details
{
"type": "email.bounced",
"created_at": "2026-11-14T10:31:09.000Z",
"data": {
"email_id": "01jcz8w3f2x9k4m7n6p5q2r1s0",
"from": "Acme <orders@mail.yourcompany.com>",
"to": ["nobody@example.com"],
"subject": "Your order is confirmed",
"created_at": "2026-11-14T10:30:58.000Z",
"bounce": {
"type": "Permanent",
"subType": "General",
"message": "smtp; 550 5.1.1 user unknown"
}
}
}A Permanent bounce is a hard bounce. Sooner adds the address to your suppression list automatically. A complaint does the same. A temporary problem arrives as email.delivery_delayed first.
Opens and clicks
{
"type": "email.clicked",
"created_at": "2026-11-14T10:35:44.000Z",
"data": {
"email_id": "01jcz8w3f2x9k4m7n6p5q2r1s0",
"from": "Acme <orders@mail.yourcompany.com>",
"to": ["ada@example.com"],
"subject": "Your order is confirmed",
"created_at": "2026-11-14T10:30:58.000Z",
"click": {
"link": "https://yourcompany.com/orders/1042",
"ipAddress": "203.0.113.10",
"userAgent": "Mozilla/5.0"
}
}
}Opens and clicks are recorded as events but do not change the email's status. Mail clients that preload images can produce opens that no person triggered, so treat opens as a signal and not as a count of readers.
Contact events
| Event | When it fires |
|---|---|
contact.created | A contact was created in the dashboard or with the API. |
contact.updated | A contact's name, unsubscribed flag or properties were updated through the API. |
contact.deleted | A contact was deleted. |
The data object is the contact:
{
"type": "contact.created",
"created_at": "2026-11-14T10:30:00.000Z",
"data": {
"id": "jx7r4k9m2c5v8n1b3q6w0e2t",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"unsubscribed": false,
"created_at": "2026-11-14T10:30:00.000Z"
}
}Changes a contact makes themselves on the hosted preferences page do not fire contact.updated.
Domain events
| Event | When it fires |
|---|---|
domain.verified | A domain's DKIM records validated and the domain can send. |
domain.failed | A domain's verification failed, or a verified domain stopped validating. |
{
"type": "domain.verified",
"created_at": "2026-11-14T10:42:00.000Z",
"data": {
"id": "jh7b2k4d9x1m5q8r3t6v0w2y",
"name": "mail.yourcompany.com",
"status": "verified"
}
}See Add a domain for what moves a domain between statuses.
Email statuses
The webhook events map onto the email status like this. The status only moves forward, so a late delivery_delayed never undoes delivered:
| Event | Resulting status |
|---|---|
email.sent | sent |
email.delivery_delayed | delivery_delayed |
email.delivered | delivered |
email.bounced | bounced |
email.complained | complained |
email.failed | failed |
email.opened, email.clicked | No change |
See Send an email for the full list of statuses.