SendGrid Event Webhooks to a Private Endpoint Using Skooh
Map SendGrid email events to an internal deliverability dashboard. Use replay to backfill historical data.
Overview
SendGrid's Event Webhooks notify you about email delivery, opens, clicks, bounces, and spam reports. This data is gold for understanding email deliverability—but your analytics service is internal.
Skooh connects SendGrid to your private deliverability dashboard.
Setup
1. Create Skooh Endpoint
- Go to Skooh Hooks
- Create endpoint "SendGrid Events"
- Copy URL: `https://api.skooh.io/hook/sg-abc123\`
2. Configure SendGrid
In SendGrid:
- Go to Settings → Mail Settings → Event Webhook
- Enter your Skooh URL
- Select events:
- Processed, Dropped, Delivered
- Bounce, Deferred
- Open, Click
- Spam Report, Unsubscribe
Note: See SendGrid's Event Webhook documentation for current setup instructions.
3. Run the Skooh Proxy
Using the Desktop App:
- Download the Skooh Desktop App
- Enter your API key and hook token
- Set forwarding URL (e.g., `http://localhost:3000/webhooks/sendgrid\`)
- Click Connect
Using Docker:
docker run -d \\
--name skooh-sendgrid \\
-e SKOOH_TOKEN=sg-abc123 \\
-e SKOOH_API_KEY=your_api_key \\
-e FORWARD_URL=http://email-analytics:3000/webhooks/sendgrid \\
--restart unless-stopped \\
skooh/proxy
Handling SendGrid Events
SendGrid sends batches of events:
app.post('/webhooks/sendgrid', express.json(), async (req, res) => {
const events = req.body; // Array of events
for (const event of events) {
await processEvent(event);
}
res.status(200).send('OK');
});
async function processEvent(event) {
const {
email,
timestamp,
event: eventType,
sg_message_id,
category,
reason,
url
} = event;
switch (eventType) {
case 'delivered':
await recordDelivery(email, sg_message_id, timestamp);
break;
case 'open':
await recordOpen(email, sg_message_id, timestamp);
break;
case 'click':
await recordClick(email, sg_message_id, url, timestamp);
break;
case 'bounce':
await handleBounce(email, reason, timestamp);
break;
case 'dropped':
await handleDropped(email, reason, timestamp);
break;
case 'spamreport':
await handleSpamReport(email, timestamp);
await suppressEmail(email); // Important!
break;
case 'unsubscribe':
await handleUnsubscribe(email, timestamp);
break;
}
}
Building a Deliverability Dashboard
Track key metrics:
// Store events in your analytics DB
async function recordDelivery(email, messageId, timestamp) {
await db.emailEvents.insert({
email,
message_id: messageId,
event: 'delivered',
timestamp: new Date(timestamp * 1000)
});
// Update aggregate stats
await db.deliveryStats.increment('delivered', 1);
}
// Calculate deliverability rate
async function getDeliverabilityRate(period) {
const stats = await db.deliveryStats.get(period);
return {
deliveryRate: stats.delivered / stats.sent * 100,
openRate: stats.opened / stats.delivered * 100,
clickRate: stats.clicked / stats.delivered * 100,
bounceRate: stats.bounced / stats.sent * 100,
spamRate: stats.spam_reports / stats.delivered * 100
};
}
Replay to Backfill
Setting up a new dashboard? Use Skooh's replay to backfill:
- Skooh stores 30 days of webhooks
- Go to your SendGrid endpoint in Skooh
- Find historical events
- Click Replay to resend them
- Your dashboard populates with historical data
This is also useful after fixing a bug—replay failed events without waiting for new email sends.
Verifying SendGrid Webhooks
SendGrid supports signed webhooks (recommended):
const crypto = require('crypto');
function verifySignature(publicKey, payload, signature, timestamp) {
const timestampPayload = timestamp + payload;
const decodedSignature = Buffer.from(signature, 'base64');
const verifier = crypto.createVerify('sha256');
verifier.update(timestampPayload);
return verifier.verify(publicKey, decodedSignature);
}
app.post('/webhooks/sendgrid',
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'];
if (!verifySignature(PUBLIC_KEY, req.body, signature, timestamp)) {
return res.status(401).send('Invalid signature');
}
const events = JSON.parse(req.body);
// Process events...
}
);
Handling High Volume
SendGrid can send many events. Process efficiently:
app.post('/webhooks/sendgrid', express.json(), async (req, res) => {
// Immediately acknowledge receipt
res.status(200).send('OK');
// Process in background
const events = req.body;
await eventQueue.addBulk(events.map(e => ({ data: e })));
});
Written by Skooh Team