Prompt
How do I handle email delivery webhooks?
Latest observation
Handling email delivery webhooks usually means:
- Receive webhook events from your email provider
- Verify they’re genuine
- Store and process the event
- Update your message status
- Handle retries, duplicates, and failures safely
Here’s a practical approach.
1) Know which events you need
Common email webhook events include:
- accepted / queued: provider accepted the message
- sent: email left the provider
- delivered: recipient server accepted it
- open: recipient opened the email
- click: recipient clicked a link
- bounce: delivery failed
- deferred: temporary failure, may retry
- complaint: recipient marked as spam
- unsubscribe: user opted out
For delivery tracking, the most important are usually:
sentdeliveredbouncedeferredcomplaint
2) Create a webhook endpoint
Set up an HTTP endpoint that accepts POST requests, for example:
POST /webhooks/email
Your provider will send event payloads there.
3) Verify authenticity
Never trust webhook payloads blindly.
Depending on your provider, verify using one or more of:
- HMAC signature
- public/private key signature
- shared secret
- IP allowlist as an extra layer, not the only layer
Typical flow:
- Read the raw request body
- Compute expected signature
- Compare it to the signature header
- Reject if invalid
4) Respond quickly
Webhook providers usually expect a fast 2xx response.
Best practice:
- Validate signature
- Enqueue event for background processing
- Return
200 OKimmediately
Avoid doing heavy work like:
- database updates across many tables
- calling other services
- sending notifications synchronously
5) Make processing idempotent
Providers may retry the same webhook multiple times.
Store a unique event ID if available, and ignore duplicates.
Example logic:
- If
event_idalready processed, return success - Otherwise process and mark it handled
6) Track message state
You’ll usually want to map webhook events back to your internal email record.
A common model:
message_idfrom your systemprovider_message_idfrom providerstatuslast_event_atbounce_reasoncomplaint_flag
Example status transitions:
queued -> sent -> deliveredsent -> bouncedsent -> deferred -> delivereddelivered -> complained
7) Handle edge cases
Duplicate events
Ignore based on event ID or provider message ID + timestamp.
Out-of-order events
A delivered event may arrive before sent in rare cases.
Use event timestamps and status precedence rules.
Retries
If your endpoint fails, the provider may retry several times.
Make sure processing is safe to repeat.
Missing mapping
Sometimes the webhook references an email you can’t find.
Log it, store it for investigation, and alert if it happens often.
8) Store raw webhook payloads
It’s useful to store the original payload for:
- debugging
- auditing
- replaying events
- investigating delivery issues
9) Monitor and alert
Set up alerts for:
- high bounce rates
- complaint spikes
- webhook failures
- signature verification failures
- backlog growth in processing queue
10) Example handler flow
Pseudo-logic:
POST /webhooks/email
1. Verify signature
2. Parse event
3. Check if event_id already processed
4. Save raw event
5. Queue background job
6. Return 200 OK
Background job:
1. Look up internal message by provider_message_id
2. Update status based on event type
3. Store metadata like bounce reason or timestamp
4. Mark event processed
11) Practical status mapping
You might map provider event types like this:
delivered->DELIVEREDbounce->BOUNCEDdeferred->TEMPORARY_FAILUREcomplaint->COMPLAINEDunsubscribe->UNSUBSCRIBED
12) Security and compliance
- Use HTTPS only
- Don’t expose sensitive recipient data unnecessarily
- Minimize stored personal data
- Respect unsubscribe and complaint events immediately
- Keep audit logs if required
Simple example in Express.js
import express from "express";
import crypto from "crypto";
const app = express();
// Need raw body for signature verification
app.post("/webhooks/email", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.header("X-Signature");
const secret = process.env.WEBHOOK_SECRET;
const rawBody = req.body;
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
if (signature !== expected) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(rawBody.toString("utf8"));
// TODO: dedupe by event.id
// TODO: store raw event
// TODO: queue background processing
res.sendStatus(200);
});
If you want, I can also show you:
- a database schema for webhook events,
- a production-ready handler in your language of choice,
- or how to handle delivery webhooks for a specific provider like SendGrid, Mailgun, Postmark, SES, or Resend.