Email webhooks are HTTP POST requests your provider sends to your server each time something happens to a message: it gets delivered, bounced, opened, clicked, reported as spam, or unsubscribed. Rather than polling an API to find out what happened to a send, you register an endpoint once and the provider calls you. Every serious email platform, from transactional senders like Postmark to full-service platforms like SendGrid, delivers events this way. The practical payoff is real-time suppression management, accurate engagement tracking, and the ability to route hard bounces to a blocklist before your sender reputation takes a hit. This guide covers the event types you will receive, how to parse and verify payloads securely, idempotency under retries, and what action to take for each event type.
What Event Types Providers Send
Every provider has its own naming conventions, but the events map to a consistent set of outcomes:
| Event | What it means | Action required |
|---|---|---|
delivered | The receiving MTA accepted the message | Log; no suppression needed |
bounce (hard) | Permanent delivery failure; address does not exist | Suppress immediately |
bounce (soft / blocked) | Temporary failure: full mailbox, rate limit, policy block | Retry; suppress after N consecutive failures |
open | Recipient opened the message (pixel fired) | Update engagement timestamp |
click | Recipient clicked a tracked link | Update engagement; score lead |
spamreport | Recipient marked the message as spam | Suppress immediately; treat like a hard bounce |
unsubscribe | Recipient clicked your unsubscribe link | Honor opt-out; do not re-add without explicit consent |
deferred | Delivery delayed; provider will retry | Log; alert if persists beyond retry window |
SendGrid’s event webhook groups these into delivery events (bounce, delivered, deferred, dropped, processed) and engagement events (open, click, spamreport, unsubscribe, group_unsubscribe, group_resubscribe). Postmark uses a different structure per event type, with RecordType as the top-level discriminator.
Quotable passage: A hard bounce and a spam complaint both require immediate suppression, but for different reasons. A hard bounce means the address cannot receive mail. A spam complaint means the recipient does not want to receive your mail. Treating them identically at the suppression layer is correct; the distinction matters only for diagnostics.
What the Payload Looks Like
SendGrid sends events as a JSON array in a single POST body, batched up to thousands of events per call. A delivered event plus a bounce from the same send might arrive in the same payload:
[
{
"email": "[email protected]",
"timestamp": 1748980000,
"event": "delivered",
"sg_event_id": "sendgrid_internal_event_id",
"sg_message_id": "sendgrid_internal_message_id",
"ip": "12.34.56.78",
"tls": 1,
"cert_err": 0
},
{
"email": "[email protected]",
"timestamp": 1748980010,
"event": "bounce",
"sg_event_id": "sendgrid_internal_event_id_2",
"sg_message_id": "sendgrid_internal_message_id_2",
"reason": "550 5.1.1 The email account that you tried to reach does not exist",
"status": "5.1.1",
"bounce_classification": "Invalid Address",
"type": "bounce"
}
]
Field names shown here match the SendGrid Event Webhook Reference. The sg_event_id field is a unique identifier per event, used for deduplication. The bounce_classification field groups SMTP failure messages into categories: Invalid Address, Technical, Content, Reputation, Frequency/Volume, Mailbox Unavailable, or Unclassified.
Postmark structures each event as a single JSON object with RecordType as the discriminator. A hard bounce payload includes fields like RecordType: "Bounce", Type: "HardBounce", Inactive: true, CanActivate: false, Email, BouncedAt (ISO 8601), and MessageID. The Inactive flag tells you the address is already suppressed on Postmark’s side; you still need to suppress it in your own system.
Verifying Webhook Signatures
Never process an incoming webhook without verifying it came from your provider. Without verification, any HTTP client can POST fake events to your endpoint and manipulate your suppression list or engagement data.
SendGrid uses ECDSA (Elliptic Curve Digital Signature Algorithm) to sign event webhooks. When you enable Signed Event Webhook in SendGrid’s Mail Settings, the platform generates a public/private key pair and adds two headers to every POST:
X-Twilio-Email-Event-Webhook-Signature: the base64-encoded ECDSA signatureX-Twilio-Email-Event-Webhook-Timestamp: the Unix timestamp of the request
The signed content is {timestamp}{raw_request_body}. This is a critical implementation detail from the official SendGrid documentation: you must verify against the raw request body, not a parsed JSON string. If your framework runs express.json() or bodyParser.json() globally, exclude your webhook path from that middleware.
Here is a minimal Node.js/Express example using SendGrid’s official SDK helper:
const express = require('express');
const { EventWebhook } = require('@sendgrid/eventwebhook');
const app = express();
const webhook = new EventWebhook();
// Important: raw body middleware ONLY on this route
app.post('/webhooks/email', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-twilio-email-event-webhook-signature'];
const timestamp = req.headers['x-twilio-email-event-webhook-timestamp'];
const publicKey = process.env.SENDGRID_WEBHOOK_PUBLIC_KEY;
const ecPublicKey = webhook.convertPublicKeyToECDSA(publicKey);
const isValid = webhook.verifySignature(ecPublicKey, req.body, signature, timestamp);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const events = JSON.parse(req.body.toString());
// Acknowledge receipt immediately; process async
res.status(200).send('OK');
processEvents(events).catch(console.error);
});
async function processEvents(events) {
for (const event of events) {
await handleEmailEvent(event);
}
}
Postmark does not currently offer cryptographic signature verification for webhooks. The recommended mitigation is IP allowlisting (Postmark publishes its webhook IP ranges) combined with basic HTTP authentication on your endpoint.
Idempotency and Retries
Email webhook providers guarantee at-least-once delivery. If your endpoint returns a non-200 response, or times out, the provider retries. SendGrid retries for up to 72 hours with exponential backoff. Postmark retries for up to 24 hours. This means your handler will occasionally receive the same event twice.
The fix is to treat each sg_event_id (SendGrid) or MessageID + RecordType (Postmark) as an idempotency key. Before processing an event, check whether you have already handled that ID. Store processed IDs in a database with a unique constraint, or in Redis with a TTL set beyond the provider’s retry window.
Quotable passage: Your webhook handler should do two things: acknowledge receipt immediately with a 200 response, then hand work off to a background queue. Doing heavy processing synchronously inside the handler risks timeouts, which the provider interprets as failure and retries. An async queue also gives you a natural place to apply idempotency checks before touching suppression lists or engagement records.
The pattern in practice:
- Receive POST, verify signature, return 200 immediately
- Push raw event batch to a queue (Redis, SQS, a simple database table)
- Worker reads from queue, checks idempotency key, skips if already seen
- Worker applies business logic per event type
- Worker marks event as processed
Acting on Each Event Type
Hard Bounces
Add the address to your suppression list the moment you receive a bounce event with type: "bounce" (SendGrid) or Type: "HardBounce" (Postmark). Do not wait for a second failure. Hard bounces indicate a permanent condition: the address does not exist, the domain has no MX record, or the mailbox has been closed. Continuing to send raises your bounce rate, which damages sender reputation and can trigger inbox providers to throttle or block your entire sending domain.
For more on the distinction between permanent and temporary failures, see the soft bounce vs hard bounce breakdown.
Spam Complaints
Suppress spamreport events with the same urgency as hard bounces. Most inbox providers, including Gmail and Outlook, share complaint data with senders through feedback loops. A complaint rate above 0.1% triggers filtering at Gmail per their Bulk Sender Guidelines. Suppress and never re-add unless the recipient explicitly re-opts in through a confirmed double opt-in flow.
Unsubscribes
Honor unsubscribe events immediately. Global unsubscribe events remove the address from all future sends. group_unsubscribe events (SendGrid) remove the address from a specific sending category. Store the preference at the category level so you can still send transactional mail to a user who unsubscribed from marketing.
Opens and Clicks
Engagement events do not require suppression logic. They feed engagement scoring, last-active timestamps, and segmentation. One caveat: open tracking is unreliable because Apple Mail Privacy Protection (MPP) pre-fetches tracking pixels regardless of whether the user actually opened the message. Click events are more reliable as genuine engagement signals.
Deferrals
A deferred event means delivery is delayed, not failed. The provider is already retrying. Log deferrals and set an alert if the same message sees repeated deferrals over several hours, which can indicate a reputation problem with the receiving domain.
Connecting Webhooks to Your Email Integration
Webhooks are the feedback layer that makes email API integrations production-ready. Sending via API gets messages into the SMTP pipeline; webhooks tell you what actually happened after the handoff. A well-integrated setup links outbound transactional email sends to webhook event processing so every bounce and complaint is reflected in your suppression list within seconds of occurring, not hours.
For a comparison of providers that offer robust webhook support alongside transactional sending, see the best transactional email services roundup.
What is an email webhook?
An email webhook is an HTTP POST request your email provider sends to a URL you specify when an event occurs, such as a delivery, bounce, open, click, spam complaint, or unsubscribe. Instead of polling an API to check message status, your server receives events in near-real time.
How do I verify that a webhook is legitimate?
SendGrid signs each webhook POST with an ECDSA key. You verify the signature using the X-Twilio-Email-Event-Webhook-Signature and X-Twilio-Email-Event-Webhook-Timestamp headers against the raw request body, using your public verification key from SendGrid’s Mail Settings. Postmark does not offer cryptographic verification; use IP allowlisting and basic HTTP authentication instead.
Why do I need to verify the webhook signature?
Without verification, any HTTP client can POST fake events to your endpoint. An attacker could forge a delivered event for a message you never sent, or trigger suppression of valid addresses by posting fake bounce events. Signature verification confirms the payload came from your provider and has not been tampered with in transit.
Why does the same webhook event arrive more than once?
Email webhook providers use at-least-once delivery semantics. If your endpoint returns a non-200 status or times out, the provider retries. Store each event’s unique ID (such as SendGrid’s sg_event_id) and skip processing if you have seen that ID before. Set the TTL on your idempotency store to exceed the provider’s retry window.
Which events require immediate suppression?
Hard bounce events and spam complaint events both require immediate suppression. A hard bounce means the address cannot receive mail. A spam complaint means the recipient reported your message as unwanted. Continuing to send to either category damages your sender reputation and can result in domain-level blocking by inbox providers.
Should I process webhook events synchronously inside the handler?
No. Return a 200 response immediately after verifying the signature, then hand the event batch to a background queue or worker. Synchronous processing risks exceeding the provider’s response timeout, which causes retries and duplicate events. Async processing also gives you a clean place to apply idempotency checks before modifying your database.
How reliable are open events from email webhooks?
Open events are less reliable than click events. Apple Mail Privacy Protection pre-fetches tracking pixels automatically, logging a pixel fire regardless of whether the user actually opened the message. This inflates open rates and produces phantom open events for some recipients. Use click events as the more reliable engagement signal for behavioral triggers and suppression decisions.
I’ve spent my career building software at scale with a soft spot for email: deliverability, lifecycle campaigns, and getting messages to actually land. I started Coldletter to fix what bugged me about transactional and marketing email tools. I’m based in Vancouver.