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.

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.

SendGrid Event Types Explained

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 HTML and images).

click: Recipient clicked a tracked link (each click = separate event).

Problem Events

bounce: Permanent delivery failure (invalid address or domain). Remove immediately.

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

spam_report: Recipient marked as spam (serious signal—damages 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 includes two headers:

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

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.

Handling Batch Events and Duplicates

SendGrid batches multiple events (single POST may have dozens). Handle 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