MailAnvil Webhooks Deep-Dive — Track Email Delivery, Bounces, and Opens in Real-Time
You sent the email. The API returned 202 Accepted. Now what?
Did it actually land in the inbox? Did it bounce? Did the recipient open it? Did they mark it as spam?
Without webhooks, you're blind after POST /v1/send. With webhooks, you have a real-time audit trail of every email's life.
Let me show you exactly how to set this up with MailAnvil.
The Problem: REST Polling Doesn't Scale
You could poll GET /v1/emails/:id every few seconds. But:
- Wastes API quota on "still pending" responses
- Misses events between polls
- Doesn't scale past a few emails
- Useless for bounces — you need to know immediately to stop sending to that address
Webhooks flip the model: MailAnvil pushes events to your server. You react, not poll.
MailAnvil Webhook Event Types
| Event | Trigger | Payload |
|---|---|---|
email.delivered |
Email accepted by recipient's mail server | email_id, to, delivered_at |
email.bounced |
Hard or soft bounce from recipient server | email_id, to, bounce_type, reason, bounced_at |
email.opened |
Recipient opened the email (tracking pixel) | email_id, to, opened_at, user_agent |
email.clicked |
Recipient clicked a link in the email | email_id, to, clicked_at, url |
email.complained |
Recipient marked as spam | email_id, to, complained_at |
email.rejected |
MailAnvil rejected the send (invalid domain, no DKIM, etc.) | email_id, to, reason, rejected_at |
Step 1: Create a Webhook Endpoint
First, deploy a receiver. Here's a minimal Cloudflare Worker:
// webhook-receiver.ts — deploy with `wrangler deploy`
export default {
async fetch(request: Request): Promise<Response> {
// 1. Verify MailAnvil signature
const signature = request.headers.get('X-MailAnvil-Signature');
const body = await request.text();
if (!verifySignature(body, signature, WEBHOOK_SECRET)) {
return new Response('Invalid signature', { status: 401 });
}
// 2. Parse event
const event = JSON.parse(body) as MailAnvilEvent;
// 3. Route by event type
switch (event.type) {
case 'email.delivered':
await handleDelivery(event);
break;
case 'email.bounced':
await handleBounce(event);
break;
case 'email.opened':
await handleOpen(event);
break;
case 'email.complained':
await handleComplaint(event);
break;
}
return new Response('OK', { status: 200 });
}
};
Step 2: Register the Webhook with MailAnvil
curl -X POST https://api.mailanvil.com/v1/webhooks \
-H "Authorization: Bearer re_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://webhooks.yoursaas.com/mailanvil",
"events": ["email.delivered", "email.bounced", "email.opened", "email.complained"],
"secret": "whsec_your_generated_secret"
}'
Response:
{
"id": "wh_01J3N8K4M2P5Q7R9",
"url": "https://webhooks.yoursaas.com/mailanvil",
"events": ["email.delivered", "email.bounced", "email.opened", "email.complained"],
"created_at": "2026-07-22T08:00:00Z",
"status": "active"
}
Step 3: Handle Critical Events
Bounce Handling — Auto-Suppress Hard Bounces
Hard bounces mean "this address doesn't exist." Sending again hurts your sender reputation. MailAnvil auto-suppresses after 2 hard bounces, but you should react on the first:
# bounce_handler.py
async def handle_bounce(event: dict):
email = event['to']
bounce_type = event['bounce_type'] # 'hard' or 'soft'
reason = event['reason']
if bounce_type == 'hard':
# Immediately flag in your DB — don't send again
await db.execute(
"UPDATE users SET email_status = 'bounced', bounced_at = ? WHERE email = ?",
(event['bounced_at'], email)
)
log.warning(f"Hard bounce: {email} — {reason}")
elif bounce_type == 'soft':
# Soft bounce = temporary (mailbox full, server down)
# MailAnvil retries for 72h, then converts to hard bounce
log.info(f"Soft bounce: {email} — will retry")
Delivery Confirmation — Tell Your Users
async function handleDelivery(event: DeliveryEvent) {
// Update email log
await db.updateEmailStatus(event.email_id, 'delivered', event.delivered_at);
// Notify your app user (optional)
await notifyUser({
userId: event.metadata.user_id,
message: `Your email to ${event.to} was delivered at ${event.delivered_at}`
});
}
Step 4: Verify Webhook Signatures
Every webhook payload carries an X-MailAnvil-Signature header. Verify it to prevent spoofed events:
function verifySignature(
body: string,
signature: string | null,
secret: string
): boolean {
if (!signature) return false;
const encoder = new TextEncoder();
const key = encoder.encode(secret);
// HMAC-SHA256
const expected = crypto.subtle
? await crypto.subtle.digest('SHA-256', encoder.encode(body + secret))
: simpleHmac(body, secret);
return timingSafeEqual(signature, expected);
}
Step 5: Monitor Webhook Health
MailAnvil retries failed deliveries with exponential backoff (up to 3 times over 24 hours). Check your dashboard or use:
# List all webhooks
curl https://api.mailanvil.com/v1/webhooks \
-H "Authorization: Bearer re_your_api_key"
# Get delivery stats for a webhook
curl https://api.mailanvil.com/v1/webhooks/wh_01J3N8K4M2P5Q7R9/stats \
-H "Authorization: Bearer re_your_api_key"
Production Checklist
- [ ] Deploy webhook receiver behind HTTPS (required)
- [ ] Store
whsec_*secret in environment variables, never in code - [ ] Verify
X-MailAnvil-Signatureon every request - [ ] Respond
200 OKwithin 10 seconds (timeout = retry) - [ ] Handle duplicate events (use
email_idas idempotency key) - [ ] Log all events for debugging — at least 30-day retention
- [ ] Alert on complaint spikes (recipe for deliverability disaster)
- [ ] Auto-suppress hard-bounced addresses in your own DB
What MailAnvil Handles for You
You don't need webhooks for everything. MailAnvil does these automatically:
- Hard bounce suppression — stops sending after 2 hard bounces per address
- Complaint rate monitoring — auto kill switch if bounce+complaint rate exceeds 5%
- DKIM enforcement — rejects sends from unverified domains before they fail
- At-most-once delivery —
Idempotency-Keyprevents duplicates end-to-end - 72-hour soft bounce retry — automatic retry with exponential backoff
Webhooks are for your app's awareness. Let MailAnvil handle the email reliability.
Cost: Zero Extra
Webhooks are included in every plan — Free (500/mo) through Scale (1M/mo). No per-event pricing, no hidden fees. Because transactional email infrastructure should include delivery tracking.
Start building: app.mailanvil.com
Docs: docs.mailanvil.com
GitHub: github.com/richaferry/mailanvil