Webhooks
Get told when something happens, instead of polling.
Add webhook endpoints per environment in the dashboard (Webhooks) or through the API. Each
endpoint subscribes to event types (all of them by default) and has its own whsec_… signing
secret.
const { : , } = await ..({
: "https://example.com/webhooks/coss",
: ["email.delivered", "email.bounced"],
});
// Store the secret now: it is only returned when the endpoint is created (or its secret rolled).
const = ?.;Events
| Type | Sent when |
|---|---|
email.sent | The provider accepted the email |
email.delivered | A recipient's mail server accepted it |
email.delivery_delayed | Delivery to a recipient is being retried |
email.bounced | A recipient's mail server rejected it |
email.complained | A recipient marked it as spam |
email.failed | The email could not be sent at all |
email.suppressed | A recipient was skipped because it is suppressed |
Organization events
These concern the whole organization, so every live environment's endpoints receive them
(sandbox endpoints don't). data.organization_id names the organization.
| Type | Sent when |
|---|---|
account.reputation_warning | A reputation check (bounces, complaints, content) crossed its warning threshold |
account.reputation_recovered | The warning cleared |
account.suspended | Live sending was suspended |
account.reinstated | The suspension was lifted |
billing.invoice_finalized | An invoice was issued (data.invoice, without lines) |
billing.invoice_paid | An invoice was paid in full |
billing.invoice_payment_failed | Charging an invoice failed (data.failure.message); it is retried |
billing.invoice_voided | An open invoice was voided |
billing.plan_changed | The plan changed or a change was scheduled or undone (change, plan, effective_at) |
billing.usage_threshold_reached | Usage reached an alert threshold of the month's credit or Free's allowance (percent) |
billing.hold_placed | Live sending was paused for billing (reason: delinquent, spend_limit or admin) |
billing.hold_lifted | That pause ended |
billing.credit_granted | A credit was added (data.credit) |
See Plans and billing for what they mean and how to read the rest of an organization's billing.
The request body is a JSON event. Payloads describe the email (email_id, from, to, subject,
tags, category) but never include its content, headers, cc or bcc. Events are rendered in the
endpoint's API version, named in api_version.
{
"id": "evt_01k7c8m4q2f3e9v6t5r1n0p8yz",
"object": "event",
"type": "email.delivered",
"api_version": "2026-10-10",
"created_at": "2026-10-10T12:00:01.000Z",
"data": {
"email_id": "email_01k7c8m4q2f3e9v6t5r1n0p8yz",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome",
"tags": [],
"category": null,
"sandbox": false,
"recipient": "user@example.com",
"occurred_at": "2026-10-10T12:00:00.000Z",
"delivery": { "smtp_response": "250 2.0.0 OK" }
}
}Verifying requests
Requests are signed with Standard Webhooks headers:
webhook-id, webhook-timestamp and webhook-signature. Always verify them before trusting the
body. With the SDK, use verifyWebhook.
Rolling the secret
Roll an endpoint's secret in the dashboard (Roll secret on the endpoint) or through the API to
replace it, for example on a schedule or after it may have leaked. For a grace period (a day by
default, up to a week) every request carries two signatures, one per secret, in
webhook-signature, so a receiver verifying with either one keeps accepting requests while you
deploy the new secret. Choose 0 to stop the old secret at once.
const { : } = await ..(, {
: 60 * 60, // the old secret keeps signing for an hour
});
const = ?.; // deploy it before rolled?.previous_secret_expires_atRolling again during the grace period stops the older secret immediately. The SDK never retries a roll: if the response is lost, roll again and deploy that secret.
Delivery and retries
- Respond with any
2xxstatus within 10 seconds to acknowledge an event. - Failed deliveries are retried 7 times over about 28 hours (5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h).
- Events can arrive out of order: sort by
data.occurred_at(every event has it). - An event can arrive more than once: deduplicate by
id(equal to thewebhook-idheader).
Delivery logs
Every event sent to an endpoint is logged as a delivery, with each HTTP attempt (status code,
duration, error and the first 1 KB of your response) and the payload as sent. Find them on the
endpoint's page in the dashboard, or list them through the API — newest first, filtered by
status or event_id:
const { : } = await ...(, {
: "failed",
});
// After fixing your receiver, send a failed event again.
for (const of ?. ?? []) {
await ...(, .);
}A retry makes one more attempt within seconds, signed with the endpoint's current secret and sent to its current URL. Deleting an endpoint deletes its delivery log, and deliveries are deleted 30 days after the event (see data retention).