Prompt
How do I handle bounce and complaint webhooks for email sending?
Latest observation
To handle bounce and complaint webhooks for email sending, treat them as signals to protect deliverability and stop sending to risky recipients.
1) Know what the events mean
Bounce
The recipient’s mailbox provider rejected the message.
-
Hard bounce: permanent failure
Examples: invalid address, domain doesn’t exist, recipient blocked.- Usually: suppress immediately.
-
Soft bounce: temporary failure
Examples: mailbox full, server temporarily unavailable.- Usually: retry for a while, but suppress if it keeps happening.
Complaint
The recipient marked your email as spam/junk.
- Usually: suppress immediately.
- These are especially important because repeated complaints can hurt sender reputation fast.
2) Build a webhook handler
Your email provider (SES, SendGrid, Postmark, Mailgun, etc.) will send webhook POST requests to your endpoint.
Your handler should:
-
Verify authenticity
- Validate provider signature or HMAC.
- Reject unsigned/untrusted requests.
-
Parse the event
- Identify event type:
bounce,complaint, etc. - Extract recipient email, message ID, timestamp, bounce type/subtype if available.
- Identify event type:
-
Update your suppression list
- Mark email as suppressed/blocked.
- Store reason:
hard_bounce,soft_bounce,complaint.
-
Log the event
- Keep audit history for debugging and compliance.
-
Acknowledge quickly
- Return
200 OKfast. - Do heavier work asynchronously if possible.
- Return
3) Suggested suppression logic
A simple policy:
- Complaint → suppress immediately
- Hard bounce → suppress immediately
- Soft bounce → increment a counter
- suppress after N soft bounces in a time window
- optionally retry depending on your business rules
Example:
- 1st soft bounce: retry later
- 3 soft bounces in 7 days: suppress
4) Keep a suppression table
Example fields:
emailstatus(active,suppressed)suppression_reason(hard_bounce,soft_bounce,complaint)suppressed_atlast_event_idbounce_countlast_bounce_at
Before sending any email:
- check the suppression list
- do not send to suppressed recipients
5) Distinguish per recipient and per message
A single webhook event may include:
- one recipient or many recipients
- a message ID
- an SES/ESP event ID
Best practice:
- deduplicate using provider event IDs
- track recipients individually
- if your system sends to multiple recipients, suppress only affected addresses
6) Handle duplicates and retries
Webhook providers often retry deliveries.
Make your handler idempotent:
- store processed event IDs
- if the same event arrives again, ignore it
This prevents double-counting bounces/complaints.
7) Example workflow
- User sends email to
alice@example.com - Provider later sends
bouncewebhook - Your endpoint verifies the request
- Your app records:
- email:
alice@example.com - event:
hard_bounce - status:
suppressed
- email:
- Future sends to Alice are blocked
Same for complaint events.
8) What not to do
- Don’t ignore complaints.
- Don’t keep sending to hard-bounced addresses.
- Don’t rely only on manual cleanup.
- Don’t block all recipients because one address bounced.
- Don’t trust webhook payloads without verification.
9) Minimal pseudocode
def handle_webhook(event):
verify_signature(event)
event_id = event["id"]
if already_processed(event_id):
return 200
event_type = event["type"]
recipient = event["recipient"]
if event_type == "complaint":
suppress(recipient, reason="complaint")
elif event_type == "bounce":
bounce_type = event.get("bounce_type")
if bounce_type == "hard":
suppress(recipient, reason="hard_bounce")
else:
increment_soft_bounce(recipient)
if soft_bounce_count(recipient) >= 3:
suppress(recipient, reason="soft_bounce")
mark_processed(event_id)
return 200
10) Good extras
- Add a way for users to manage unsubscribes separately from complaints/bounces.
- Segment suppression reasons for reporting.
- Monitor bounce/complaint rates:
- high bounce rate may indicate bad list hygiene
- high complaint rate may indicate poor content or targeting
If you want, I can also give you:
- a concrete implementation for AWS SES / SendGrid / Mailgun / Postmark
- a database schema for suppression tracking
- sample code in Python, Node.js, or Go