Webhook event handling patterns for email tracking (sent, delivered, bounced, opened, clicked). Use when implementing email event webhooks, signature verification, processing delivery events, logging email analytics, or building real-time email status tracking.
Provides patterns for implementing secure Resend webhook handlers with signature verification and event processing for email tracking (sent, delivered, bounced, opened, clicked). Use when building email analytics systems or processing delivery events.
/plugin marketplace add vanman2024/ai-dev-marketplace/plugin install resend@ai-dev-marketplaceThis skill is limited to using the following tools:
examples/event-processing/README.mdexamples/fastapi-webhook/README.mdexamples/nextjs-webhook/README.mdscripts/generate-test-payload.shscripts/test-webhook.shscripts/verify-signature.shtemplates/fastapi-endpoint.pytemplates/nextjs-route.tstemplates/prisma-schema.prismaComprehensive patterns and templates for implementing secure webhook handlers with Resend, covering event types, signature verification, and event processing strategies.
Resend sends webhooks for the following email events:
Triggered when email is accepted by Resend.
{
"type": "email.sent",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"email_id": "123e4567-e89b-12d3-a456-426614174000",
"from": "notifications@example.com",
"to": "recipient@example.com",
"subject": "Welcome to Example",
"created_at": "2024-01-15T10:30:00Z"
}
}
Triggered when email reaches recipient's mail server.
{
"type": "email.delivered",
"created_at": "2024-01-15T10:35:00Z",
"data": {
"email_id": "123e4567-e89b-12d3-a456-426614174000",
"from": "notifications@example.com",
"to": "recipient@example.com",
"created_at": "2024-01-15T10:35:00Z"
}
}
Triggered when email cannot be delivered (hard bounce).
{
"type": "email.bounced",
"created_at": "2024-01-15T10:40:00Z",
"data": {
"email_id": "123e4567-e89b-12d3-a456-426614174000",
"from": "notifications@example.com",
"to": "invalid@example.com",
"reason": "Mailbox does not exist",
"created_at": "2024-01-15T10:40:00Z"
}
}
Triggered when recipient opens the email (requires pixel tracking).
{
"type": "email.opened",
"created_at": "2024-01-15T11:00:00Z",
"data": {
"email_id": "123e4567-e89b-12d3-a456-426614174000",
"from": "notifications@example.com",
"to": "recipient@example.com",
"user_agent": "Mozilla/5.0...",
"ip_address": "192.168.1.1",
"created_at": "2024-01-15T11:00:00Z"
}
}
Triggered when recipient clicks a link in the email.
{
"type": "email.clicked",
"created_at": "2024-01-15T11:05:00Z",
"data": {
"email_id": "123e4567-e89b-12d3-a456-426614174000",
"from": "notifications@example.com",
"to": "recipient@example.com",
"link": "https://example.com/promo",
"user_agent": "Mozilla/5.0...",
"ip_address": "192.168.1.1",
"created_at": "2024-01-15T11:05:00Z"
}
}
Triggered when recipient marks email as spam.
{
"type": "email.complained",
"created_at": "2024-01-15T11:10:00Z",
"data": {
"email_id": "123e4567-e89b-12d3-a456-426614174000",
"from": "notifications@example.com",
"to": "recipient@example.com",
"created_at": "2024-01-15T11:10:00Z"
}
}
Verify webhook authenticity using HMAC-SHA256:
import crypto from 'crypto';
interface WebhookEvent {
type: string;
created_at: string;
data: Record<string, any>;
}
function verifyWebhookSignature(
payload: string,
signature: string,
signingSecret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', signingSecret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage in Express middleware
import express from 'express';
const webhookRouter = express.Router();
webhookRouter.post('/webhooks/resend', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-resend-signature'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.RESEND_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event: WebhookEvent = JSON.parse(payload);
handleWebhookEvent(event);
res.json({ success: true });
});
export default webhookRouter;
import hmac
import hashlib
import json
from typing import Tuple
def verify_webhook_signature(
payload: str,
signature: str,
signing_secret: str
) -> bool:
"""Verify Resend webhook signature using HMAC-SHA256."""
expected_signature = hmac.new(
signing_secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
def get_signature_from_headers(headers: dict) -> str:
"""Extract signature from request headers."""
return headers.get('x-resend-signature', '')
interface EmailEvent {
type: 'sent' | 'delivered' | 'bounced' | 'opened' | 'clicked' | 'complained';
created_at: string;
data: {
email_id: string;
from: string;
to: string;
[key: string]: any;
};
}
async function handleWebhookEvent(event: EmailEvent): Promise<void> {
try {
// Log the event
console.log(`Processing webhook: ${event.type}`, {
email_id: event.data.email_id,
timestamp: event.created_at,
});
// Route to specific handler
switch (event.type) {
case 'email.sent':
await handleEmailSent(event.data);
break;
case 'email.delivered':
await handleEmailDelivered(event.data);
break;
case 'email.bounced':
await handleEmailBounced(event.data);
break;
case 'email.opened':
await handleEmailOpened(event.data);
break;
case 'email.clicked':
await handleEmailClicked(event.data);
break;
case 'email.complained':
await handleEmailComplained(event.data);
break;
default:
console.warn(`Unknown event type: ${event.type}`);
}
} catch (error) {
console.error('Error handling webhook event:', error);
throw error;
}
}
// Individual event handlers
async function handleEmailSent(data: any): Promise<void> {
// Update email status in database
await db.emails.update(
{ id: data.email_id },
{
status: 'sent',
sent_at: new Date(data.created_at),
updated_at: new Date(),
}
);
}
async function handleEmailDelivered(data: any): Promise<void> {
await db.emails.update(
{ id: data.email_id },
{
status: 'delivered',
delivered_at: new Date(data.created_at),
updated_at: new Date(),
}
);
}
async function handleEmailBounced(data: any): Promise<void> {
// Update status and mark recipient as invalid
await db.emails.update(
{ id: data.email_id },
{
status: 'bounced',
bounce_reason: data.reason,
bounced_at: new Date(data.created_at),
updated_at: new Date(),
}
);
// Add to bounce list
await db.bounced_emails.create({
email: data.to,
reason: data.reason,
bounced_at: new Date(data.created_at),
});
}
async function handleEmailOpened(data: any): Promise<void> {
await db.email_events.create({
email_id: data.email_id,
event_type: 'opened',
user_agent: data.user_agent,
ip_address: data.ip_address,
created_at: new Date(data.created_at),
});
}
async function handleEmailClicked(data: any): Promise<void> {
await db.email_events.create({
email_id: data.email_id,
event_type: 'clicked',
link: data.link,
user_agent: data.user_agent,
ip_address: data.ip_address,
created_at: new Date(data.created_at),
});
}
async function handleEmailComplained(data: any): Promise<void> {
await db.emails.update(
{ id: data.email_id },
{
status: 'complained',
complained_at: new Date(data.created_at),
updated_at: new Date(),
}
);
// Add to suppression list
await db.suppressed_emails.create({
email: data.to,
reason: 'complaint',
created_at: new Date(data.created_at),
});
}
from enum import Enum
from datetime import datetime
from typing import Any, Dict
class EventType(Enum):
SENT = "email.sent"
DELIVERED = "email.delivered"
BOUNCED = "email.bounced"
OPENED = "email.opened"
CLICKED = "email.clicked"
COMPLAINED = "email.complained"
async def handle_webhook_event(event: Dict[str, Any]) -> None:
"""Route webhook events to appropriate handlers."""
event_type = event.get('type')
event_data = event.get('data', {})
handlers = {
EventType.SENT.value: handle_email_sent,
EventType.DELIVERED.value: handle_email_delivered,
EventType.BOUNCED.value: handle_email_bounced,
EventType.OPENED.value: handle_email_opened,
EventType.CLICKED.value: handle_email_clicked,
EventType.COMPLAINED.value: handle_email_complained,
}
handler = handlers.get(event_type)
if handler:
await handler(event_data)
else:
print(f"Unknown event type: {event_type}")
async def handle_email_sent(data: Dict[str, Any]) -> None:
"""Update email status to sent."""
await db.emails.update(
{"id": data["email_id"]},
{
"status": "sent",
"sent_at": datetime.fromisoformat(data["created_at"]),
}
)
async def handle_email_delivered(data: Dict[str, Any]) -> None:
"""Update email status to delivered."""
await db.emails.update(
{"id": data["email_id"]},
{
"status": "delivered",
"delivered_at": datetime.fromisoformat(data["created_at"]),
}
)
async def handle_email_bounced(data: Dict[str, Any]) -> None:
"""Handle bounce event and add to suppression list."""
await db.emails.update(
{"id": data["email_id"]},
{
"status": "bounced",
"bounce_reason": data.get("reason"),
}
)
await db.bounced_emails.create({
"email": data["to"],
"reason": data.get("reason"),
"bounced_at": datetime.fromisoformat(data["created_at"]),
})
async def handle_email_opened(data: Dict[str, Any]) -> None:
"""Log email open event."""
await db.email_events.create({
"email_id": data["email_id"],
"event_type": "opened",
"user_agent": data.get("user_agent"),
"ip_address": data.get("ip_address"),
"created_at": datetime.fromisoformat(data["created_at"]),
})
async def handle_email_clicked(data: Dict[str, Any]) -> None:
"""Log email click event."""
await db.email_events.create({
"email_id": data["email_id"],
"event_type": "clicked",
"link": data.get("link"),
"user_agent": data.get("user_agent"),
"ip_address": data.get("ip_address"),
"created_at": datetime.fromisoformat(data["created_at"]),
})
async def handle_email_complained(data: Dict[str, Any]) -> None:
"""Handle complaint event and add to suppression list."""
await db.emails.update(
{"id": data["email_id"]},
{
"status": "complained",
}
)
await db.suppressed_emails.create({
"email": data["to"],
"reason": "complaint",
"created_at": datetime.fromisoformat(data["created_at"]),
})
// Prisma schema example
model Email {
id String @id @default(uuid())
resend_id String @unique
from String
to String
subject String
status String @default("sent") // sent, delivered, bounced, opened, complained
sent_at DateTime?
delivered_at DateTime?
bounced_at DateTime?
complained_at DateTime?
bounce_reason String?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
events EmailEvent[]
@@index([status])
@@index([to])
@@index([created_at])
}
model EmailEvent {
id String @id @default(uuid())
email_id String
email Email @relation(fields: [email_id], references: [id], onDelete: Cascade)
event_type String // opened, clicked
link String?
user_agent String?
ip_address String?
created_at DateTime @default(now())
@@index([email_id])
@@index([event_type])
@@index([created_at])
}
model BouncedEmail {
id String @id @default(uuid())
email String @unique
reason String
bounced_at DateTime
created_at DateTime @default(now())
@@index([email])
}
model SuppressedEmail {
id String @id @default(uuid())
email String @unique
reason String // complaint, bounce, unsubscribe
created_at DateTime @default(now())
@@index([email])
}
CREATE TABLE emails (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resend_id VARCHAR UNIQUE NOT NULL,
from_address VARCHAR NOT NULL,
to_address VARCHAR NOT NULL,
subject TEXT NOT NULL,
status VARCHAR DEFAULT 'sent',
sent_at TIMESTAMP,
delivered_at TIMESTAMP,
bounced_at TIMESTAMP,
complained_at TIMESTAMP,
bounce_reason TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE email_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email_id UUID NOT NULL REFERENCES emails(id) ON DELETE CASCADE,
event_type VARCHAR NOT NULL,
link TEXT,
user_agent TEXT,
ip_address INET,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE bounced_emails (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR UNIQUE NOT NULL,
reason TEXT,
bounced_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE suppressed_emails (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR UNIQUE NOT NULL,
reason VARCHAR NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_emails_status ON emails(status);
CREATE INDEX idx_emails_to ON emails(to_address);
CREATE INDEX idx_emails_created ON emails(created_at);
CREATE INDEX idx_email_events_email_id ON email_events(email_id);
CREATE INDEX idx_email_events_type ON email_events(event_type);
CREATE INDEX idx_bounced_emails_email ON bounced_emails(email);
CREATE INDEX idx_suppressed_emails_email ON suppressed_emails(email);
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
async function setupWebhook() {
const response = await resend.webhooks.create({
events: [
'email.sent',
'email.delivered',
'email.bounced',
'email.opened',
'email.clicked',
'email.complained',
],
url: 'https://your-domain.com/api/webhooks/resend',
});
console.log('Webhook created:', response.data);
// Save the webhook ID and signing secret securely
}
async function getWebhookDetails(webhookId: string) {
const response = await resend.webhooks.get(webhookId);
return response.data;
}
async function listWebhooks() {
const response = await resend.webhooks.list();
return response.data;
}
RESEND_API_KEY=your_resend_key_here
RESEND_WEBHOOK_SECRET=your_webhook_signing_secret_here
DATABASE_URL=your_database_connection_string
interface WebhookTask {
id: string;
event: WebhookEvent;
retries: number;
max_retries: number;
next_retry_at: Date;
}
async function processWebhookWithRetry(
event: WebhookEvent,
maxRetries: number = 3
): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await handleWebhookEvent(event);
console.log(`Webhook processed successfully: ${event.data.email_id}`);
return;
} catch (error) {
lastError = error as Error;
console.error(`Attempt ${attempt} failed:`, error);
if (attempt < maxRetries) {
// Exponential backoff: 5s, 25s, 125s
const delay = Math.pow(5, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Store failed event for manual review
await db.failed_webhooks.create({
event_id: event.data.email_id,
event_type: event.type,
payload: JSON.stringify(event),
error: lastError?.message,
created_at: new Date(),
});
throw lastError;
}
nextjs-webhook/ - Next.js API route webhook handlerfastapi-webhook/ - FastAPI webhook handler with FastAPI patternsevent-processing/ - Database logging and event analyticsSee individual example README files for complete code and usage patterns.