4.1 KiB
4.1 KiB
Webhooks and Events
Receiving and processing email delivery events in real-time.
Event Types
| Event | When Fired | Use For |
|---|---|---|
email.sent |
Email accepted by Resend | Confirming send initiated |
email.delivered |
Email delivered to recipient server | Confirming delivery |
email.bounced |
Email bounced (hard or soft) | List hygiene, alerting |
email.complained |
Recipient marked as spam | Immediate unsubscribe |
email.opened |
Recipient opened email | Engagement tracking |
email.clicked |
Recipient clicked link | Engagement tracking |
Webhook Setup
1. Create Endpoint
Your endpoint must:
- Accept POST requests
- Return 2xx status quickly (within 5 seconds)
- Handle duplicate events (idempotent processing)
app.post('/webhooks/resend', async (req, res) => {
// Return 200 immediately to acknowledge receipt
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(req.body).catch(console.error);
});
2. Verify Signatures
Always verify webhook signatures to prevent spoofing.
import { Webhook } from 'svix';
const webhook = new Webhook(process.env.RESEND_WEBHOOK_SECRET);
app.post('/webhooks/resend', (req, res) => {
try {
const payload = webhook.verify(
JSON.stringify(req.body),
{
'svix-id': req.headers['svix-id'],
'svix-timestamp': req.headers['svix-timestamp'],
'svix-signature': req.headers['svix-signature'],
}
);
// Process verified payload
} catch (err) {
return res.status(400).send('Invalid signature');
}
});
3. Register Webhook URL
Configure your webhook endpoint in the Resend dashboard or via API.
Processing Events
Bounce Handling
async function handleBounce(event) {
const { email_id, email, bounce_type } = event.data;
if (bounce_type === 'hard') {
// Permanent failure - remove from all lists
await suppressEmail(email, 'hard_bounce');
await removeFromAllLists(email);
} else {
// Soft bounce - track and remove after threshold
await incrementSoftBounce(email);
const count = await getSoftBounceCount(email);
if (count >= 3) {
await suppressEmail(email, 'soft_bounce_limit');
}
}
}
Complaint Handling
async function handleComplaint(event) {
const { email } = event.data;
// Immediate suppression - no exceptions
await suppressEmail(email, 'complaint');
await removeFromAllLists(email);
await logComplaint(event); // For analysis
}
Delivery Confirmation
async function handleDelivered(event) {
const { email_id } = event.data;
await updateEmailStatus(email_id, 'delivered');
}
Idempotent Processing
Webhooks may be sent multiple times. Use event IDs to prevent duplicate processing.
async function processWebhook(event) {
const eventId = event.id;
// Check if already processed
if (await isEventProcessed(eventId)) {
return; // Skip duplicate
}
// Process event
await handleEvent(event);
// Mark as processed
await markEventProcessed(eventId);
}
Error Handling
Retry Behavior
If your endpoint returns non-2xx, webhooks will retry with exponential backoff:
- Retry 1: ~30 seconds
- Retry 2: ~1 minute
- Retry 3: ~5 minutes
- (continues for ~24 hours)
Best Practices
- Return 200 quickly - Process asynchronously to avoid timeouts
- Be idempotent - Handle duplicate deliveries gracefully
- Log everything - Store raw events for debugging
- Alert on failures - Monitor webhook processing errors
- Queue for processing - Use a job queue for complex handling
Testing Webhooks
Local development: Use ngrok or similar to expose localhost.
ngrok http 3000
# Use the ngrok URL as your webhook endpoint
Verify handling: Send test events through Resend dashboard or manually trigger each event type.
Related
- List Management - What to do with bounce/complaint data
- Sending Reliability - Retry logic when sends fail