SendGrid Integration

Learn how to integrate SendGrid Event Webhooks with HooksDash for tracking email delivery, opens, clicks, and bounces.

Setup

1. Create a HooksDash Endpoint

# Via CLI
hooksdash endpoints create --name "SendGrid Events" --slug sendgrid-dev

# Your webhook URL:
https://hooksdash-api.ibadsiddiqui.dev/api/h/sendgrid-dev

2. Configure in SendGrid

  1. Go to SendGrid Dashboard
  2. Navigate to Settings → Mail Settings → Event Webhook
  3. Toggle Event Webhook Status to Enabled
  4. HTTP Post URL: https://hooksdash-api.ibadsiddiqui.dev/api/h/sendgrid-dev
  5. Select which events to post
  6. Click Save

Implementation

Basic Event Handler

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

app.use(express.json());

app.post('/webhook', (req, res) => {
  const events = req.body; // Array of events

  events.forEach((event) => {
    console.log('Email event:', event.event);
    console.log('To:', event.email);
    console.log('Timestamp:', new Date(event.timestamp * 1000));

    switch (event.event) {
      case 'delivered':
        // Update delivery status in database
        handleDelivered(event);
        break;
      case 'open':
        // Track email opens
        handleOpen(event);
        break;
      case 'click':
        // Track link clicks
        handleClick(event);
        break;
      case 'bounce':
        // Handle bounced emails
        handleBounce(event);
        break;
      case 'spam':
        // Handle spam reports
        handleSpam(event);
        break;
    }
  });

  res.json({ received: true });
});

function handleDelivered(event) {
  console.log('Email delivered to:', event.email);
}

function handleOpen(event) {
  console.log('Email opened by:', event.email);
}

function handleClick(event) {
  console.log('Link clicked:', event.url);
}

function handleBounce(event) {
  console.log('Email bounced:', event.reason);
}

function handleSpam(event) {
  console.log('Marked as spam by:', event.email);
}

app.listen(3000);

TypeScript Implementation

import express from 'express';

const app = express();
app.use(express.json());

interface SendGridEvent {
  event: string;
  email: string;
  timestamp: number;
  sg_event_id: string;
  sg_message_id: string;
  url?: string;
  reason?: string;
  status?: string;
  response?: string;
  attempt?: string;
}

app.post('/webhook', (req, res) => {
  const events = req.body as SendGridEvent[];

  events.forEach((event) => {
    console.log('Email event:', event.event);
    console.log('To:', event.email);
    console.log('Timestamp:', new Date(event.timestamp * 1000));

    switch (event.event) {
      case 'delivered':
        handleDelivered(event);
        break;
      case 'open':
        handleOpen(event);
        break;
      case 'click':
        handleClick(event);
        break;
      case 'bounce':
        handleBounce(event);
        break;
      case 'spam':
        handleSpam(event);
        break;
      case 'unsubscribe':
        handleUnsubscribe(event);
        break;
    }
  });

  res.json({ received: true });
});

function handleDelivered(event: SendGridEvent): void {
  console.log('Email delivered to:', event.email);
  // Update database, analytics, etc.
}

function handleOpen(event: SendGridEvent): void {
  console.log('Email opened by:', event.email);
  // Track engagement metrics
}

function handleClick(event: SendGridEvent): void {
  console.log('Link clicked:', event.url);
  // Track click-through rates
}

function handleBounce(event: SendGridEvent): void {
  console.log('Email bounced:', event.reason);
  // Mark email as invalid, remove from list
}

function handleSpam(event: SendGridEvent): void {
  console.log('Marked as spam by:', event.email);
  // Unsubscribe user, investigate content
}

function handleUnsubscribe(event: SendGridEvent): void {
  console.log('Unsubscribed:', event.email);
  // Remove from mailing list
}

app.listen(3000);

Event Types

EventDescription
processedEmail has been received and is ready to be delivered
droppedEmail was dropped and not delivered
deliveredEmail was successfully delivered
bounceReceiving server could not or would not accept email
deferredReceiving server temporarily rejected email
openRecipient opened the email
clickRecipient clicked a link in the email
spamRecipient marked email as spam
unsubscribeRecipient clicked unsubscribe link
group_unsubscribeRecipient unsubscribed from a specific group
group_resubscribeRecipient resubscribed to a specific group

Testing Locally

1. Start Your Server

node server.js

2. Start HooksDash Tunnel

hooksdash forward --port 3000 --endpoint sendgrid-dev

3. Send Test Email

Use SendGrid's API or dashboard to send a test email:

const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);

const msg = {
  to: 'test@example.com',
  from: 'noreply@yourapp.com',
  subject: 'Test Email',
  text: 'Testing SendGrid webhooks with HooksDash',
};

sgMail.send(msg);

Example Use Cases

Email Analytics Dashboard

const emailStats = {
  sent: 0,
  delivered: 0,
  opened: 0,
  clicked: 0,
  bounced: 0,
  spam: 0,
};

app.post('/webhook', (req, res) => {
  const events = req.body;

  events.forEach((event) => {
    switch (event.event) {
      case 'processed':
        emailStats.sent++;
        break;
      case 'delivered':
        emailStats.delivered++;
        break;
      case 'open':
        emailStats.opened++;
        break;
      case 'click':
        emailStats.clicked++;
        break;
      case 'bounce':
        emailStats.bounced++;
        break;
      case 'spam':
        emailStats.spam++;
        break;
    }
  });

  res.json({ received: true });
});

// API endpoint to get stats
app.get('/stats', (req, res) => {
  const openRate = (emailStats.opened / emailStats.delivered * 100).toFixed(2);
  const clickRate = (emailStats.clicked / emailStats.delivered * 100).toFixed(2);

  res.json({
    ...emailStats,
    openRate: `${openRate}%`,
    clickRate: `${clickRate}%`,
  });
});

Automatic Bounce Handling

const db = require('./database');

app.post('/webhook', async (req, res) => {
  const events = req.body;

  for (const event of events) {
    if (event.event === 'bounce') {
      // Mark email as invalid in database
      await db.users.update(
        { email: event.email },
        {
          emailValid: false,
          bounceReason: event.reason,
          bouncedAt: new Date(event.timestamp * 1000),
        }
      );

      console.log(`Marked ${event.email} as bounced: ${event.reason}`);

      // Send notification to admin
      if (event.status === '5.0.0') {
        await notifyAdmin(`Hard bounce: ${event.email}`);
      }
    }
  }

  res.json({ received: true });
});

Engagement Tracking

const db = require('./database');

app.post('/webhook', async (req, res) => {
  const events = req.body;

  for (const event of events) {
    // Log all events
    await db.emailEvents.create({
      email: event.email,
      eventType: event.event,
      timestamp: new Date(event.timestamp * 1000),
      messageId: event.sg_message_id,
      url: event.url || null,
    });

    // Update user engagement score
    if (event.event === 'open') {
      await db.users.increment(
        { email: event.email },
        { engagementScore: 1 }
      );
    }

    if (event.event === 'click') {
      await db.users.increment(
        { email: event.email },
        { engagementScore: 2 }
      );
    }
  }

  res.json({ received: true });
});

Unsubscribe Management

const db = require('./database');

app.post('/webhook', async (req, res) => {
  const events = req.body;

  for (const event of events) {
    if (event.event === 'unsubscribe' || event.event === 'spam') {
      // Unsubscribe user
      await db.users.update(
        { email: event.email },
        {
          subscribed: false,
          unsubscribedAt: new Date(event.timestamp * 1000),
          reason: event.event === 'spam' ? 'spam_complaint' : 'unsubscribe',
        }
      );

      console.log(`Unsubscribed: ${event.email}`);

      // Send confirmation email
      if (event.event === 'unsubscribe') {
        await sendUnsubscribeConfirmation(event.email);
      }
    }
  }

  res.json({ received: true });
});

Best Practices

1. Verify Signatures (Optional)

SendGrid can sign webhook requests. To verify:

const crypto = require('crypto');

function verifySendGridSignature(req) {
  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;

  // Verify signature using public key
  // Implementation depends on your crypto library
}

2. Handle Duplicates

SendGrid may send duplicate events. Use sg_event_id for deduplication:

const processedEvents = new Set();

app.post('/webhook', (req, res) => {
  const events = req.body;

  events.forEach((event) => {
    if (processedEvents.has(event.sg_event_id)) {
      return; // Skip duplicate
    }

    processedEvents.add(event.sg_event_id);
    // Process event...
  });

  res.json({ received: true });
});

3. Batch Processing

Process events in batches for better performance:

app.post('/webhook', async (req, res) => {
  const events = req.body;

  // Respond immediately
  res.json({ received: true });

  // Process asynchronously in batches
  try {
    await processBatch(events);
  } catch (error) {
    console.error('Batch processing failed:', error);
  }
});

async function processBatch(events) {
  const operations = events.map((event) => ({
    updateOne: {
      filter: { email: event.email },
      update: {
        $push: { events: event },
        $set: { lastEventAt: new Date(event.timestamp * 1000) },
      },
    },
  }));

  await db.users.bulkWrite(operations);
}

Troubleshooting

Events Not Being Sent

Issue: No events appearing in HooksDash

Solution:

  • Verify Event Webhook is enabled in SendGrid
  • Check that the webhook URL is correct
  • Ensure you've selected the events to post
  • Send a test email to trigger events

Missing Event Data

Issue: Some event fields are undefined

Solution:

  • Not all fields are present in all event types
  • Check SendGrid's event documentation for available fields
  • Use optional chaining: event.url?.toString()

High Volume Issues

Issue: Webhook endpoint timing out with high email volume

Solution:

  • Respond with 200 immediately, process asynchronously
  • Use a queue system (Redis, RabbitMQ) for processing
  • Implement batch database operations
  • Consider scaling your webhook endpoint

Resources