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
- Go to SendGrid Dashboard
- Navigate to Settings → Mail Settings → Event Webhook
- Toggle Event Webhook Status to Enabled
- HTTP Post URL:
https://hooksdash-api.ibadsiddiqui.dev/api/h/sendgrid-dev - Select which events to post
- 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
| Event | Description |
|---|---|
processed | Email has been received and is ready to be delivered |
dropped | Email was dropped and not delivered |
delivered | Email was successfully delivered |
bounce | Receiving server could not or would not accept email |
deferred | Receiving server temporarily rejected email |
open | Recipient opened the email |
click | Recipient clicked a link in the email |
spam | Recipient marked email as spam |
unsubscribe | Recipient clicked unsubscribe link |
group_unsubscribe | Recipient unsubscribed from a specific group |
group_resubscribe | Recipient 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