Back to Blog
·Hook Mesh Team

SendGrid Webhook Events: How to Handle Email Notifications

Learn how to set up and handle SendGrid Event Webhooks for email delivery tracking. Complete guide covering event types, payload structure, signature verification, and best practices with Node.js code examples.

SendGrid Webhook Events: How to Handle Email Notifications

SendGrid Webhook Events: How to Handle Email Notifications

SendGrid webhooks notify your application in real-time when emails are delivered, opened, clicked, or bounced. This guide covers setup, event types, signature verification, and best practices for reliable email event processing.

Understanding SendGrid Event Webhooks

SendGrid webhooks post JSON data when email events occur—processing, delivery, engagement (opens, clicks), and compliance (bounces, spam, unsubscribes). Real-time data enables customer record updates, follow-up actions, and analytics dashboards.

SendGrid webhook event flow showing emails triggering events that post to your endpoint

Setting Up the SendGrid Event Webhook

Before receiving events, you need to configure your webhook endpoint in SendGrid:

  1. Log into your SendGrid dashboard
  2. Navigate to Settings > Mail Settings > Event Webhooks
  3. Click Create new webhook
  4. Enter your endpoint URL (must be HTTPS in production)
  5. Select the events you want to receive
  6. Enable Signed Event Webhook for security (highly recommended)
  7. Save and activate the webhook

SendGrid will send a test POST request to verify your endpoint is reachable. Your endpoint should respond with a 2xx status code within 3 seconds to be considered successful.

Testing Webhooks Locally

Your local development machine isn't accessible from the internet, so SendGrid can't reach localhost. Use these tools:

  • ngrok: Creates a secure tunnel exposing your local server. Run ngrok http 3000 to get a public URL like https://abc123.ngrok.io that forwards to your local port.
  • webhook.site: Instantly captures incoming webhooks for payload inspection without writing code. Useful for verifying SendGrid is sending the expected events.

For production, use separate webhook URLs and verification keys for test and production environments to isolate traffic and prevent test data from polluting production analytics.

SendGrid Event Types Explained

SendGrid tracks two primary categories: delivery events (did the email arrive?) and engagement events (did the recipient interact?).

SendGrid event types categorized into delivery and engagement events

Delivery Events

processed: Email entered the sending pipeline.

delivered: Mail server accepted the email (server acceptance, not inbox placement).

deferred: Mail server temporarily rejected. SendGrid retries for up to 72 hours.

Engagement Events

open: Recipient opened the email (detected via tracking pixel). Requires Open Tracking enabled in SendGrid settings. Note: Some opens are machine-generated by email security scanners—check the sg_machine_open field to filter these.

click: Recipient clicked a tracked link (each click = separate event). Requires Click Tracking enabled.

Problem Events

bounce: Permanent delivery failure (hard bounce). The address doesn't exist or permanently rejects mail. Remove immediately to protect sender reputation. SendGrid classifies bounces by reason: Invalid Address, Technical, Content, Reputation, Frequency/Volume, or Mailbox Unavailable.

blocked: Temporary delivery denial (soft bounce). The server temporarily can't accept mail—full mailbox, server down, or rate limited. These may resolve on retry; don't suppress immediately.

dropped: SendGrid prevented sending (bounced address, unsubscribe, or spam). Protects sender reputation.

spam_report: Recipient marked as spam (serious signal—damages deliverability). Keep hard bounces under 5% to maintain good deliverability.

unsubscribe: Recipient clicked unsubscribe. Honor requests to maintain compliance.

Webhook Payload Structure

SendGrid sends events in batches as JSON arrays. Each event has common fields plus event-specific data:

[
  {
    "email": "recipient@example.com",
    "timestamp": 1706140800,
    "smtp-id": "<14c5d75ce93.dfd.64b469@example.com>",
    "event": "delivered",
    "category": ["welcome-series"],
    "sg_event_id": "ZGVsaXZlcmVkLTAtMjY0NzU4NjctV0JFd0FRPT0",
    "sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
    "response": "250 OK"
  },
  {
    "email": "recipient@example.com",
    "timestamp": 1706140920,
    "event": "open",
    "sg_event_id": "b3BlbmVkLTAtMjY0NzU4NjctV0JFd0FRPT0",
    "sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
    "useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
    "ip": "192.0.2.1"
  }
]

Key fields include:

  • email: The recipient's email address
  • timestamp: Unix timestamp when the event occurred
  • event: The event type (delivered, open, bounce, etc.)
  • sg_event_id: Unique identifier for this specific event
  • sg_message_id: Identifier linking all events for a single email
  • category: Custom categories you assigned when sending

Bounce events include additional fields like reason and type (bounce or blocked), while click events include the url that was clicked.

Handling Events in Node.js

See receiving and verifying webhooks in Node.js for complete implementation patterns:

const express = require('express');
const crypto = require('crypto');

const app = express();

// Use raw body for signature verification
app.use('/webhooks/sendgrid', express.raw({ type: 'application/json' }));

app.post('/webhooks/sendgrid', async (req, res) => {
  const signature = req.headers['x-twilio-email-event-webhook-signature'];
  const timestamp = req.headers['x-twilio-email-event-webhook-timestamp'];

  // Verify signature before processing
  if (!verifySignature(req.body, signature, timestamp)) {
    console.error('Invalid SendGrid webhook signature');
    return res.status(401).send('Unauthorized');
  }

  // Respond immediately to prevent timeouts
  res.status(200).send('OK');

  // Parse and process events asynchronously
  const events = JSON.parse(req.body.toString());
  await processEvents(events);
});

async function processEvents(events) {
  for (const event of events) {
    try {
      switch (event.event) {
        case 'delivered':
          await handleDelivered(event);
          break;
        case 'open':
          await handleOpen(event);
          break;
        case 'click':
          await handleClick(event);
          break;
        case 'bounce':
          await handleBounce(event);
          break;
        case 'dropped':
          await handleDropped(event);
          break;
        case 'spam_report':
          await handleSpamReport(event);
          break;
        case 'unsubscribe':
          await handleUnsubscribe(event);
          break;
        default:
          console.log(`Unhandled event type: ${event.event}`);
      }
    } catch (err) {
      console.error(`Error processing event ${event.sg_event_id}:`, err);
    }
  }
}

Event Handler Examples

async function handleBounce(event) {
  // Mark email as bounced
  await db.emailLogs.update({
    where: { messageId: event.sg_message_id },
    data: { status: 'bounced', bounceReason: event.reason }
  });

  // Suppress future sends to this address
  await db.emailSuppressions.upsert({
    where: { email: event.email },
    create: { email: event.email, reason: 'bounce' },
    update: { reason: 'bounce' }
  });
}

async function handleSpamReport(event) {
  // Immediately suppress and alert team
  await db.emailSuppressions.upsert({
    where: { email: event.email },
    create: { email: event.email, reason: 'spam_report' },
    update: { reason: 'spam_report' }
  });
  await alertTeam(`Spam report received for ${event.email}`);
}

Verifying SendGrid Webhook Signatures

SendGrid uses ECDSA signatures to prevent forged events. Always verify in production. See webhook security best practices.

SendGrid ECDSA signature verification flow

SendGrid includes two headers:

  • X-Twilio-Email-Event-Webhook-Signature: ECDSA signature
  • X-Twilio-Email-Event-Webhook-Timestamp: Signing timestamp

To enable signature verification:

  1. Navigate to Settings > Mail Settings in SendGrid
  2. Under Security Features, toggle Enable Signed Event Webhook
  3. Copy the generated Verification Key and store it as an environment variable

Verify the signature:

const crypto = require('crypto');

// Your verification key from SendGrid dashboard
const SENDGRID_VERIFICATION_KEY = process.env.SENDGRID_WEBHOOK_VERIFICATION_KEY;

function verifySignature(payload, signature, timestamp) {
  if (!signature || !timestamp) {
    return false;
  }

  try {
    // Construct the signed payload
    const payloadString = typeof payload === 'string'
      ? payload
      : payload.toString();
    const signedPayload = timestamp + payloadString;

    // Verify ECDSA signature
    const verifier = crypto.createVerify('sha256');
    verifier.update(signedPayload);

    const publicKey = `-----BEGIN PUBLIC KEY-----\n${SENDGRID_VERIFICATION_KEY}\n-----END PUBLIC KEY-----`;

    return verifier.verify(publicKey, signature, 'base64');
  } catch (err) {
    console.error('Signature verification error:', err);
    return false;
  }
}

Always verify signatures. Skipping verification exposes you to forged events that corrupt analytics or trigger unauthorized actions.

Using the Official SendGrid SDK

The @sendgrid/eventwebhook package provides helpers for signature verification:

const { EventWebhook } = require('@sendgrid/eventwebhook');

function verifyWithSDK(publicKey, payload, signature, timestamp) {
  const eventWebhook = new EventWebhook();
  const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
  return eventWebhook.verifySignature(ecPublicKey, payload, signature, timestamp);
}

Critical: The payload must be the raw request body (Buffer or string), not parsed JSON. If using express.json() or bodyParser.json(), exclude the webhook path from JSON parsing.

Handling Batch Events and Duplicates

SendGrid batches multiple events (single POST may have dozens). Handle gracefully.

Retry Behavior

If your server doesn't return a 2xx response, SendGrid retries at increasing intervals for up to 24 hours. This rolling window means each failing event gets its own 24-hour retry period. Design your handlers to be idempotent since the same event may arrive multiple times.

Event Ordering

SendGrid does not guarantee event delivery order. You might receive an "open" event before "delivered" due to network timing. Use the timestamp field to sort events chronologically, and design state machines that handle out-of-order updates gracefully.

SendGrid may send duplicates during retries. Use idempotent handlers to prevent double-processing:

const processedEvents = new Map(); // Use Redis in production

async function processEventsWithDeduplication(events) {
  for (const event of events) {
    const eventId = event.sg_event_id;

    // Check for duplicate
    if (await isDuplicate(eventId)) {
      console.log(`Skipping duplicate event: ${eventId}`);
      continue;
    }

    // Process the event
    await processEvent(event);

    // Mark as processed with TTL
    await markProcessed(eventId);
  }
}

async function isDuplicate(eventId) {
  // In production, use Redis: return await redis.exists(`event:${eventId}`)
  return processedEvents.has(eventId);
}

async function markProcessed(eventId) {
  // In production, use Redis with TTL: await redis.setex(`event:${eventId}`, 86400, '1')
  processedEvents.set(eventId, Date.now());
}

Best Practices for Production

Respond Quickly, Process Asynchronously

SendGrid expects a 3-second response. Repeated timeouts disable the webhook. Acknowledge immediately, then process in background:

app.post('/webhooks/sendgrid', async (req, res) => {
  // Verify signature
  if (!verifySignature(req.body, signature, timestamp)) {
    return res.status(401).send('Unauthorized');
  }

  // Respond immediately
  res.status(200).send('OK');

  // Queue for async processing
  const events = JSON.parse(req.body.toString());
  await eventQueue.add('process-sendgrid-events', { events });
});

Store Events and Monitor Health

Store event data for analytics and debugging. Track response times under 3 seconds. Alert on verification failures and unusual patterns (bounce spikes, delivery drops).

Common Use Cases

Email Analytics: Aggregate open, click, and delivery rates for dashboards.

Bounce Management: Auto-suppress bounced addresses. Remove hard bounces immediately.

Engagement Scoring: Track engagement to prioritize active subscribers and identify re-engagement.

Transactional Monitoring: Verify critical email delivery and alert support on failures.

Reliable Email Event Processing with Hook Mesh

Hook Mesh provides a reliable ingestion layer between SendGrid and your application with automatic retries, event queuing, and detailed logs. Your events are buffered during deployments, retried on failures, and fully observable.

Conclusion

SendGrid webhooks provide real-time visibility into deliveries, engagement, and compliance.

Key points: Verify signatures, respond within 3 seconds, process asynchronously, handle duplicates, and store for analytics.

See Twilio webhooks for SMS and voice, or our Platform Integration Hub.

Related Posts