Skip to content

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:

Add a custom secret header to your SendGrid webhook URL as a query parameter or use Hookbase's HMAC verification with a shared secret:

bash
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:

bash
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}/sendgrid

Save your webhook URL:

https://api.hookbase.app/ingest/{orgSlug}/sendgrid

2. Configure SendGrid Event Webhook

  1. Go to SendGrid DashboardSettingsMail Settings
  2. Click Event Webhook
  3. Set the HTTP POST URL to your Hookbase webhook URL
  4. Select the events you want to receive:
    • Delivery: Processed, Dropped, Delivered, Deferred, Bounce
    • Engagement: Open, Click, Spam Report, Unsubscribe, Group Unsubscribe, Group Resubscribe
  5. 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 WebhookSigned Event Webhook and note the Verification Key for custom verification logic.

3. Create Destinations and Routes

bash
# 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:

HeaderDescription
X-Twilio-Email-Event-Webhook-SignatureECDSA signature of the payload
X-Twilio-Email-Event-Webhook-TimestampTimestamp 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.

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:

json
{
  "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:

json
{
  "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.

json
[
  {
    "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.

json
[
  {
    "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).

json
[
  {
    "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).

json
[
  {
    "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.

json
[
  {
    "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).

json
[
  {
    "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

EventDescription
processedEmail accepted by SendGrid for delivery
droppedEmail dropped (invalid, unsubscribed, or previously bounced)
deliveredEmail accepted by the receiving server
deferredReceiving server temporarily rejected the email
bounceEmail permanently rejected by the receiving server
openRecipient opened the email
clickRecipient clicked a link in the email
spamreportRecipient marked the email as spam
unsubscribeRecipient clicked the unsubscribe link
group_unsubscribeRecipient unsubscribed from a suppression group
group_resubscribeRecipient 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

javascript
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:

javascript
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:

javascript
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

javascript
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

json
{
  "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

json
{
  "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

json
{
  "name": "Engagement Events",
  "logic": "or",
  "conditions": [
    {
      "field": "[0].event",
      "operator": "equals",
      "value": "open"
    },
    {
      "field": "[0].event",
      "operator": "equals",
      "value": "click"
    }
  ]
}

Compliance Events

json
{
  "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:

HeaderDescription
Content-Typeapplication/json
User-AgentSendGrid Event API
X-Twilio-Email-Event-Webhook-SignatureECDSA signature (only with Signed Event Webhook enabled)
X-Twilio-Email-Event-Webhook-TimestampSignature timestamp (only with Signed Event Webhook enabled)

Testing

Using SendGrid's Test Feature

  1. Go to SettingsMail SettingsEvent Webhook
  2. Click Test Your Integration
  3. SendGrid sends a sample batch of events to your URL

Manual Test

Send a test batch to verify your setup:

bash
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

  1. Handle batches: Always treat the payload as an array — SendGrid batches multiple events per request for efficiency

  2. Use sg_event_id for deduplication: Each event has a unique sg_event_id to prevent duplicate processing

  3. Subscribe selectively: Only enable the event types you need in SendGrid's webhook settings to reduce noise

  4. Monitor bounce rates: High bounce rates affect your sender reputation — route bounce events to an alerting system

  5. Handle compliance events promptly: Process spamreport, unsubscribe, and group_unsubscribe events quickly to maintain compliance with email regulations

  6. Use categories: Add categories to your outgoing emails so webhook events include them for easier filtering and routing

  7. Separate environments: Create separate sources for production and staging SendGrid accounts

Troubleshooting

Events Not Arriving

  1. Verify the Event Webhook is enabled in SendGrid → Settings → Mail Settings
  2. Check the webhook URL is correct and publicly accessible
  3. Confirm you have selected at least one event type
  4. Check SendGrid's Activity Feed for recent email activity
  5. Ensure you are actually sending emails — the webhook only fires for real sends

Signature Verification Failed

  1. If using Basic Auth, verify the credentials in the URL match your source configuration exactly
  2. If using a custom header secret, ensure the header name and value match between SendGrid and Hookbase
  3. For native ECDSA verification, confirm you copied the full Verification Key from SendGrid settings
  4. Check for URL encoding issues in the webhook URL

Missing Event Types

  1. Not all event types are enabled by default — check your webhook settings
  2. Open tracking requires Open Tracking to be enabled in Mail Settings
  3. Click tracking requires Click Tracking to be enabled in Mail Settings
  4. Some events only fire for specific email types (e.g., group_unsubscribe requires suppression groups)

Duplicate Events

Enable deduplication using sg_event_id:

bash
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:

  1. Ensure your destination endpoints respond within the timeout window
  2. Consider splitting routes to send different event types to different destinations
  3. Use transforms to reduce payload size before forwarding

Released under the MIT License.