Webhooks

Webhooks carry Resend's event names, payloads and signatures. If your handler works with Resend, it works here with a new secret.

Create an endpoint

Create one in your dashboard or with the API. The URL must use HTTPS. The response includes signing_secret, which starts with whsec_.

curl
curl -X POST https://manzo.dev/webhooks \
  -H "Authorization: Bearer $MANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://yourapp.com/hooks/email",
    "events": ["email.delivered", "email.bounced"]
  }'

Verify the signature

We sign with the Standard Webhooks scheme and send webhook-id, webhook-timestamp and webhook-signature, with svix- copies of each. The SDK's webhooks.verify() checks them. Read the raw body, because parsing it first breaks the signature.

handler.ts
const event = resend.webhooks.verify({
  payload: await request.text(),
  headers: {
    id: request.headers.get("webhook-id"),
    timestamp: request.headers.get("webhook-timestamp"),
    signature: request.headers.get("webhook-signature"),
  },
  webhookSecret: process.env.MANZO_WEBHOOK_SECRET,
});

Events

Webhook events
EventWhen it arrives
email.sentWe handed the email to our delivery provider.
email.deliveredThe recipient's mail server accepted it.
email.delivery_delayedDelivery is taking longer than usual. We are still trying.
email.bouncedThe email bounced. data.bounce says why.
email.complainedThe recipient marked it as spam.
email.failedWe could not send it. data.failed.reason says why.
email.suppressedWe did not send it because the address is on your suppression list.

email.opened and email.clicked are accepted as subscriptions but do not arrive yet, because open and click tracking is not available.

Payload

email.delivered
{
  "type": "email.delivered",
  "created_at": "2026-11-08T09:41:09.000Z",
  "data": {
    "email_id": "6f1c1e0a-3d3b-4c58-9a3e-1f0c2b7d4a11",
    "created_at": "2026-11-08T09:41:02.000Z",
    "from": "Your app <hi@yourapp.com>",
    "to": ["ada@example.com"],
    "subject": "Your sign-in code"
  }
}

Retries

Return a 2xx status within 15 seconds. If you don't, we try 8 times in all, waiting 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours between attempts. You can send a test event or replay a delivery from your dashboard or the API.