SendGrid Integration
Receive and route SendGrid Event Webhooks for email delivery tracking, engagement analytics, and compliance events.
Batched Events
SendGrid sends events as a JSON array (batch of events), not as individual objects. Each webhook request may contain multiple events. See Transform Examples for how to handle this.
Setup
1. Create a Source in Hookbase
SendGrid's native Signed Event Webhook uses ECDSA verification (elliptic curve signatures). For simpler integration with Hookbase, you have two options:
Option A: Custom Secret Header (Recommended)
Add a custom secret header to your SendGrid webhook URL as a query parameter or use Hookbase's HMAC verification with a shared secret:
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "SendGrid Production",
"slug": "sendgrid",
"provider": "sendgrid",
"verificationConfig": {
"type": "hmac",
"secret": "your-sendgrid-webhook-secret",
"algorithm": "sha256",
"header": "X-Webhook-Secret",
"encoding": "hex"
}
}'Option B: Basic Auth in URL
Embed credentials directly in the ingest URL for simple token-based verification:
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "SendGrid Production",
"slug": "sendgrid",
"provider": "sendgrid",
"verificationConfig": {
"type": "basic_auth",
"username": "sendgrid",
"password": "your-secret-token"
}
}'Then use this as your webhook URL in SendGrid:
https://sendgrid:[email protected]/ingest/{orgSlug}/sendgridSave your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/sendgrid2. Configure SendGrid Event Webhook
- Go to SendGrid Dashboard → Settings → Mail Settings
- Click Event Webhook
- Set the HTTP POST URL to your Hookbase webhook URL
- Select the events you want to receive:
- Delivery: Processed, Dropped, Delivered, Deferred, Bounce
- Engagement: Open, Click, Spam Report, Unsubscribe, Group Unsubscribe, Group Resubscribe
- Click Save
Signed Event Webhook
SendGrid also offers a Signed Event Webhook that uses ECDSA signatures with the X-Twilio-Email-Event-Webhook-Signature and X-Twilio-Email-Event-Webhook-Timestamp headers. If you need ECDSA verification, enable it under Event Webhook → Signed Event Webhook and note the Verification Key for custom verification logic.
3. Create Destinations and Routes
# Create a destination for your email event handler
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-d '{"name": "Email Analytics Service", "url": "https://myapp.com/webhooks/sendgrid"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-d '{"name": "SendGrid to Analytics", "sourceId": "src_...", "destinationIds": ["dst_..."]}'Signature Verification
ECDSA (Native Signed Event Webhook)
SendGrid's Signed Event Webhook uses ECDSA with the following headers:
| Header | Description |
|---|---|
X-Twilio-Email-Event-Webhook-Signature | ECDSA signature of the payload |
X-Twilio-Email-Event-Webhook-Timestamp | Timestamp used in signature generation |
The verification key is an ECDSA public key available in your SendGrid Event Webhook settings. This method provides the strongest security but requires ECDSA support.
Recommended: Basic Auth or Custom Header
For simpler setup with Hookbase, use one of these approaches:
Basic Auth embeds credentials in the webhook URL itself. SendGrid sends the credentials with every request, and Hookbase verifies them automatically:
{
"verificationConfig": {
"type": "basic_auth",
"username": "sendgrid",
"password": "your-secret-token"
}
}Custom Header uses an HMAC signature with a shared secret. Add the secret as a query parameter to your webhook URL in SendGrid (e.g., ?secret=your-token), then configure Hookbase to verify it:
{
"verificationConfig": {
"type": "hmac",
"secret": "your-sendgrid-webhook-secret",
"algorithm": "sha256",
"header": "X-Webhook-Secret",
"encoding": "hex"
}
}TIP
Basic Auth is the simplest approach since SendGrid natively supports URLs with embedded credentials. No extra configuration is needed beyond the URL.
Common Events
SendGrid sends events as a JSON array. Each request may contain one or more events in a single batch.
Delivered
Sent when the receiving server accepts the email.
[
{
"email": "[email protected]",
"timestamp": 1706000000,
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
"event": "delivered",
"category": ["welcome-emails"],
"sg_event_id": "ZGVsaXZlcmVkLTEt",
"sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
"response": "250 2.0.0 OK",
"ip": "168.1.1.1",
"tls": 1
}
]Bounce
Sent when the receiving server rejects the email.
[
{
"email": "[email protected]",
"timestamp": 1706000000,
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
"event": "bounce",
"category": ["transactional"],
"sg_event_id": "Ym91bmNlLTEt",
"sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
"reason": "550 5.1.1 The email account does not exist",
"type": "bounce",
"status": "5.1.1",
"ip": "168.1.1.1",
"tls": 1
}
]Open
Sent when a recipient opens an email (requires open tracking enabled).
[
{
"email": "[email protected]",
"timestamp": 1706000100,
"event": "open",
"category": ["newsletter"],
"sg_event_id": "b3Blbi0xLQ",
"sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
"useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"ip": "203.0.113.1"
}
]Click
Sent when a recipient clicks a link in an email (requires click tracking enabled).
[
{
"email": "[email protected]",
"timestamp": 1706000200,
"event": "click",
"category": ["newsletter"],
"sg_event_id": "Y2xpY2stMS0",
"sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
"url": "https://example.com/promo?utm_source=sendgrid",
"url_offset": {
"index": 0,
"type": "html"
},
"useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"ip": "203.0.113.1"
}
]Spam Report
Sent when a recipient marks the email as spam.
[
{
"email": "[email protected]",
"timestamp": 1706000300,
"event": "spamreport",
"sg_event_id": "c3BhbXJlcG9ydC0x",
"sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
"category": ["marketing"]
}
]Dropped
Sent when SendGrid drops a message (e.g., to a previously bounced or unsubscribed address).
[
{
"email": "[email protected]",
"timestamp": 1706000400,
"event": "dropped",
"sg_event_id": "ZHJvcHBlZC0xLQ",
"sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
"reason": "Bounced Address",
"status": "5.0.0"
}
]SendGrid Event Types
| Event | Description |
|---|---|
processed | Email accepted by SendGrid for delivery |
dropped | Email dropped (invalid, unsubscribed, or previously bounced) |
delivered | Email accepted by the receiving server |
deferred | Receiving server temporarily rejected the email |
bounce | Email permanently rejected by the receiving server |
open | Recipient opened the email |
click | Recipient clicked a link in the email |
spamreport | Recipient marked the email as spam |
unsubscribe | Recipient clicked the unsubscribe link |
group_unsubscribe | Recipient unsubscribed from a suppression group |
group_resubscribe | Recipient resubscribed to a suppression group |
Transform Examples
Batch Handling
SendGrid payloads are arrays of events. Your transforms receive the entire array. If you need to process events individually, iterate over the array or use filters to select specific event types first.
Slack Alert for Bounces
function transform(payload) {
// payload is an array of events
const bounces = payload.filter(e => e.event === 'bounce');
if (bounces.length === 0) return null;
const bounceList = bounces
.map(b => `• ${b.email} — ${b.reason || 'Unknown reason'} (${b.status || 'N/A'})`)
.join('\n');
return {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: `Email Bounce Alert (${bounces.length})`
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Bounced Emails:*\n${bounceList}`
}
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `Source: SendGrid | ${new Date().toISOString()}`
}
]
}
]
};
}Database Format
Flatten the batched events into individual records for storage:
function transform(payload) {
// payload is an array of events — map each to a normalized record
return payload.map(event => ({
sg_event_id: event.sg_event_id,
sg_message_id: event.sg_message_id,
event_type: event.event,
email: event.email,
category: Array.isArray(event.category) ? event.category.join(',') : event.category || null,
reason: event.reason || null,
bounce_type: event.type || null,
status_code: event.status || null,
url: event.url || null,
useragent: event.useragent || null,
ip: event.ip || null,
tls: event.tls || null,
timestamp: new Date(event.timestamp * 1000).toISOString(),
processed_at: new Date().toISOString()
}));
}Engagement Summary for Analytics
Aggregate events by type for a dashboard or analytics endpoint:
function transform(payload) {
const summary = payload.reduce((acc, event) => {
acc[event.event] = (acc[event.event] || 0) + 1;
return acc;
}, {});
return {
batch_size: payload.length,
event_counts: summary,
emails: [...new Set(payload.map(e => e.email))],
timestamp: new Date().toISOString()
};
}Alert for Spam Reports
function transform(payload) {
const spamReports = payload.filter(e => e.event === 'spamreport');
if (spamReports.length === 0) return null;
const emails = spamReports.map(e => e.email).join(', ');
return {
channel: "#email-alerts",
username: "SendGrid Bot",
icon_emoji: ":warning:",
attachments: [{
color: "danger",
title: `Spam Report (${spamReports.length})`,
fields: [
{ title: "Emails", value: emails, short: false },
{ title: "Category", value: spamReports[0].category?.join(', ') || "N/A", short: true }
],
footer: "SendGrid Event Webhook",
ts: Math.floor(Date.now() / 1000)
}]
};
}Filter Examples
Bounce Events Only
{
"name": "Bounces Only",
"logic": "or",
"conditions": [
{
"field": "[0].event",
"operator": "equals",
"value": "bounce"
}
]
}Filtering Batched Events
Since SendGrid sends arrays, filters evaluate against the batch. For precise per-event filtering, use transforms to extract the events you need, or configure SendGrid to only send specific event types in the webhook settings.
Delivery Failure Events
{
"name": "Delivery Failures",
"logic": "or",
"conditions": [
{
"field": "[0].event",
"operator": "equals",
"value": "bounce"
},
{
"field": "[0].event",
"operator": "equals",
"value": "dropped"
},
{
"field": "[0].event",
"operator": "equals",
"value": "deferred"
}
]
}Engagement Events
{
"name": "Engagement Events",
"logic": "or",
"conditions": [
{
"field": "[0].event",
"operator": "equals",
"value": "open"
},
{
"field": "[0].event",
"operator": "equals",
"value": "click"
}
]
}Compliance Events
{
"name": "Compliance Events",
"logic": "or",
"conditions": [
{
"field": "[0].event",
"operator": "equals",
"value": "spamreport"
},
{
"field": "[0].event",
"operator": "equals",
"value": "unsubscribe"
},
{
"field": "[0].event",
"operator": "equals",
"value": "group_unsubscribe"
}
]
}Headers
SendGrid sends these headers with Event Webhook requests:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | SendGrid Event API |
X-Twilio-Email-Event-Webhook-Signature | ECDSA signature (only with Signed Event Webhook enabled) |
X-Twilio-Email-Event-Webhook-Timestamp | Signature timestamp (only with Signed Event Webhook enabled) |
Testing
Using SendGrid's Test Feature
- Go to Settings → Mail Settings → Event Webhook
- Click Test Your Integration
- SendGrid sends a sample batch of events to your URL
Manual Test
Send a test batch to verify your setup:
curl -X POST https://api.hookbase.app/ingest/{orgSlug}/sendgrid \
-H "Content-Type: application/json" \
-d '[
{
"email": "[email protected]",
"timestamp": 1706000000,
"event": "delivered",
"sg_event_id": "dGVzdC0xLQ",
"sg_message_id": "test.filter0001.16648.5515E0B88.0",
"response": "250 2.0.0 OK",
"category": ["test"]
},
{
"email": "[email protected]",
"timestamp": 1706000100,
"event": "open",
"sg_event_id": "dGVzdC0yLQ",
"sg_message_id": "test.filter0001.16648.5515E0B88.0",
"useragent": "Mozilla/5.0",
"category": ["test"]
}
]'Best Practices
Handle batches: Always treat the payload as an array — SendGrid batches multiple events per request for efficiency
Use
sg_event_idfor deduplication: Each event has a uniquesg_event_idto prevent duplicate processingSubscribe selectively: Only enable the event types you need in SendGrid's webhook settings to reduce noise
Monitor bounce rates: High bounce rates affect your sender reputation — route bounce events to an alerting system
Handle compliance events promptly: Process
spamreport,unsubscribe, andgroup_unsubscribeevents quickly to maintain compliance with email regulationsUse categories: Add categories to your outgoing emails so webhook events include them for easier filtering and routing
Separate environments: Create separate sources for production and staging SendGrid accounts
Troubleshooting
Events Not Arriving
- Verify the Event Webhook is enabled in SendGrid → Settings → Mail Settings
- Check the webhook URL is correct and publicly accessible
- Confirm you have selected at least one event type
- Check SendGrid's Activity Feed for recent email activity
- Ensure you are actually sending emails — the webhook only fires for real sends
Signature Verification Failed
- If using Basic Auth, verify the credentials in the URL match your source configuration exactly
- If using a custom header secret, ensure the header name and value match between SendGrid and Hookbase
- For native ECDSA verification, confirm you copied the full Verification Key from SendGrid settings
- Check for URL encoding issues in the webhook URL
Missing Event Types
- Not all event types are enabled by default — check your webhook settings
- Open tracking requires Open Tracking to be enabled in Mail Settings
- Click tracking requires Click Tracking to be enabled in Mail Settings
- Some events only fire for specific email types (e.g.,
group_unsubscriberequires suppression groups)
Duplicate Events
Enable deduplication using sg_event_id:
curl -X PATCH .../sources/src_sendgrid \
-d '{"dedupEnabled": true, "dedupStrategy": "auto"}'Large Batches Timing Out
SendGrid may send large batches with hundreds of events. If processing takes too long:
- Ensure your destination endpoints respond within the timeout window
- Consider splitting routes to send different event types to different destinations
- Use transforms to reduce payload size before forwarding