Gareth Wilson Gareth Wilson

Guide to Resend Webhooks: Features and Best Practices

Published


Resend webhooks notify your application about the lifecycle of the emails you send: delivered, bounced, complained, opened, clicked, and inbound email received. If you're building on Resend, webhooks are how you react to email events without polling.

This guide covers how Resend webhooks work, the events you'll handle, how to verify the Svix signature with the official SDK, and the best practices for production.

What are Resend webhooks?

Resend webhooks are JSON POSTs delivered through Svix, so they follow the Svix signature format: three headers, svix-id, svix-timestamp, and svix-signature. The signing secret starts with whsec_. The signature is an HMAC-SHA256 (base64) over <svix-id>.<svix-timestamp>.<raw_body>, and the svix-signature header can carry multiple space-separated values (each prefixed v1,); any one matching is valid. Deliveries older than 5 minutes are rejected. The official resend SDK verifies all of this.

Resend webhook features

FeatureDetails
ConfigurationResend dashboard > Webhooks
Signature headerssvix-id, svix-timestamp, svix-signature
Signature schemeSvix: base64 HMAC-SHA256 over id.timestamp.body, whsec_ secret
Replay window5 minutes; multiple v1, signatures, any match is valid
SDKresend webhooks.verify (Node); manual Svix verification for Python
DeliveryAt-least-once (duplicates possible); dedupe on data.email_id

Common events

Resend covers the outbound email lifecycle plus inbound:

EventFires when
email.sentThe message is accepted for sending
email.deliveredThe message is delivered
email.delivery_delayedDelivery is delayed
email.bouncedThe message bounces
email.complainedThe recipient marks it as spam
email.opened / email.clickedThe recipient opens or clicks
email.receivedAn inbound email arrives (metadata only; fetch the body via the Receiving API)

Setting up Resend webhooks

In the Resend dashboard, go to Webhooks, add your endpoint, select events, and copy the signing secret (whsec_...) into RESEND_WEBHOOK_SECRET. For inbound email.received, configure a receiving address or an MX record. Note that the inbound payload is metadata only, you fetch the body and attachments via the Receiving API.

Securing Resend webhooks

Use the official SDK's webhooks.verify, passing the raw body and the three Svix headers (the SDK expects short key names id / timestamp / signature). It throws on an invalid signature and returns the parsed event otherwise. Capture the raw body before parsing.

const { Resend } = require("resend");

const resend = new Resend(process.env.RESEND_API_KEY);
const SECRET = process.env.RESEND_WEBHOOK_SECRET;

app.post("/webhooks/resend", express.raw({ type: "application/json" }), (req, res) => {
  try {
    const event = resend.webhooks.verify({
      payload: req.body.toString("utf8"), // raw body
      headers: {
        id: req.headers["svix-id"],
        timestamp: req.headers["svix-timestamp"],
        signature: req.headers["svix-signature"],
      },
      webhookSecret: SECRET, // your whsec_... secret
    });
    res.sendStatus(200); // acknowledge fast
    processQueue.add(event); // branch on event.type, dedupe on data.email_id, async
  } catch {
    res.sendStatus(401); // invalid signature or stale timestamp
  }
});

The same verification in Python, done manually (the Svix construction):

import base64
import hashlib
import hmac
import os

SECRET = os.environ["RESEND_WEBHOOK_SECRET"]

def verify(raw_body: bytes, svix_id: str, svix_timestamp: str, svix_signature: str) -> bool:
    key = base64.b64decode(SECRET.split("_", 1)[1])  # strip whsec_ then base64-decode
    signed = f"{svix_id}.{svix_timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for part in svix_signature.split():  # space-separated, each "v1,<sig>"
        if part.startswith("v1,") and hmac.compare_digest(part[3:], expected):
            return True
    return False

Resend webhook limitations and pain points

The SDK wants short header keys

The Problem: webhooks.verify takes { id, timestamp, signature }, but the actual HTTP headers are svix-id / svix-timestamp / svix-signature. Passing the full header names fails.

Why It Happens: The SDK maps the Svix headers to short keys.

Workarounds:

  • Map svix-* headers to the short keys when calling verify.

How Hookdeck Can Help: Hookdeck verifies the Svix signature at the edge, so your app doesn't juggle header naming.

The secret is base64 after the prefix

The Problem: When verifying manually, the whsec_ secret must have its prefix stripped and the remainder base64-decoded before use as the HMAC key. Using the whole string, or skipping the decode, never matches.

Why It Happens: Svix secrets are whsec_ + base64 key material.

Workarounds:

  • Strip whsec_, base64-decode, then HMAC; or use the SDK.

How Hookdeck Can Help: Hookdeck handles the secret format at the edge.

Inbound payload is metadata only

The Problem: email.received carries sender, recipient, and subject, but not the body or attachment content, so acting on the message needs a second call.

Why It Happens: Resend keeps the inbound webhook lean.

Workarounds:

  • Fetch the full message and attachments via the Receiving API.

How Hookdeck Can Help: Hookdeck durably queues events so your worker can enrich them via the API at its own pace.

At-least-once delivery

The Problem: Duplicates are possible, and the signature proves authenticity, not uniqueness.

Why It Happens: Resend delivers at-least-once with retries.

Workarounds:

  • Dedupe on data.email_id and make handlers idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge. See our guide to webhook idempotency.

Best practices

Verify with the official SDK over the raw body

Use resend.webhooks.verify in Node with the mapped Svix headers; verify the Svix construction manually in Python.

Respect the 5-minute window and multiple signatures

Reject stale deliveries, and accept if any v1, signature matches.

Dedupe on data.email_id

Delivery is at-least-once, so make handlers idempotent.

Acknowledge fast, process asynchronously

Return 200 quickly and defer work to a queue. See why to process webhooks asynchronously.

Make Resend webhooks production-ready

Hookdeck verifies the Svix signature, deduplicates, and durably queues every email event

Conclusion

Resend webhooks use the Svix format: svix-id / svix-timestamp / svix-signature headers, a whsec_ secret, base64 HMAC-SHA256 over id.timestamp.body, a 5-minute window, and possibly multiple signatures. Verify with the official resend SDK (map the headers to short keys), or the Svix construction manually in Python, dedupe on data.email_id, and fetch inbound bodies via the Receiving API.

Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

Get started with Hookdeck for free and handle Resend webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.