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 -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.
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
| Event | When it arrives |
|---|---|
email.sent | We handed the email to our delivery provider. |
email.delivered | The recipient's mail server accepted it. |
email.delivery_delayed | Delivery is taking longer than usual. We are still trying. |
email.bounced | The email bounced. data.bounce says why. |
email.complained | The recipient marked it as spam. |
email.failed | We could not send it. data.failed.reason says why. |
email.suppressed | We 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
{
"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.