Resend
Verify and handle Resend webhooks — email delivery, engagement, contact, domain, and suppression events.
Create the endpoint under Webhooks in the Resend dashboard, pass its signing secret, and handle events by name:
import { createWebhookHandler } from 'webhooks-sdk'
import { resend } from 'webhooks-sdk/resend'
const handler = createWebhookHandler({
provider: resend({ secret: process.env.RESEND_WEBHOOK_SECRET! }),
on: {
'email.delivered': async (event) => {
await markDelivered(event.payload.data.email_id)
},
'email.bounced': async (event) => {
await suppress(event.payload.data.email_id)
},
},
})
export const POST = handler.fetch
The secret is the whsec_… value on the endpoint’s page in the dashboard.
Resend signs with Standard Webhooks
over the legacy svix-* headers — the id and timestamp are inside the
signed material, so a replayed request is rejected on its own.
Options
| Option | Type | Default | |
|---|---|---|---|
secret |
string | string[] |
— | The whsec_… signing secret(s). Pass an array during rotation. |
tolerance |
number |
300 |
Replay window in seconds. |
Events
email.sent · email.scheduled · email.delivered ·
email.delivery_delayed · email.bounced · email.complained ·
email.failed · email.opened · email.clicked · email.received ·
email.suppressed · contact.created · contact.updated ·
contact.deleted · domain.created · domain.updated ·
domain.deleted · suppression.added · suppression.removed
These autocomplete in the on map; any other string still routes, so an
event type Resend adds later is handled without an SDK update.
Two of them are easy to conflate: email.suppressed fires when a specific
send is suppressed, suppression.added when an address lands on your
suppression list — after a hard bounce or spam complaint, or manually.
The envelope
event.id— thesvix-idheader, the canonical idempotency key. The body has no top-level id of its own.event.type— the body’stype.event.timestamp— the signedsvix-timestampheader. The body’screated_atis an ISO string of when the event occurred, which can be earlier than the delivery.event.payload—{ type, created_at, data }. Email events name the send indata.email_id; contact, domain, and suppression events name the resource indata.id.
Standalone & testing
The wrapper is the Standard Webhooks
provider with id: 'resend', so the
standalone triple lives there:
import {
verifyStandardWebhook, // (raw, { secret }) — throws on failure
parseStandardWebhook, // (raw) — the envelope
signStandardWebhook, // (body, secret, { headerPrefix: 'svix' }) — for tests
} from 'webhooks-sdk/standard-webhooks'
Pass headerPrefix: 'svix' to signStandardWebhook to produce the header
names Resend actually sends. See Testing.