Email Webhooks: Handling Bounces, Opens, and Delivery Events

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:

EventWhat it meansAction required
deliveredThe receiving MTA accepted the messageLog; no suppression needed
bounce (hard)Permanent delivery failure; address does not existSuppress immediately
bounce (soft / blocked)Temporary failure: full mailbox, rate limit, policy blockRetry; suppress after N consecutive failures
openRecipient opened the message (pixel fired)Update engagement timestamp
clickRecipient clicked a tracked linkUpdate engagement; score lead
spamreportRecipient marked the message as spamSuppress immediately; treat like a hard bounce
unsubscribeRecipient clicked your unsubscribe linkHonor opt-out; do not re-add without explicit consent
deferredDelivery delayed; provider will retryLog; 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 signature
  • X-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:

  1. Receive POST, verify signature, return 200 immediately
  2. Push raw event batch to a queue (Redis, SQS, a simple database table)
  3. Worker reads from queue, checks idempotency key, skips if already seen
  4. Worker applies business logic per event type
  5. 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.