Gareth Wilson Gareth Wilson

Guide to SendGrid Webhooks Features and Best Practices

Published


SendGrid (now part of Twilio) is one of the most widely used email delivery platforms, powering transactional and marketing email for businesses of all sizes. Beyond sending email, SendGrid provides webhook integrations that let you track delivery events, monitor engagement, and even receive inbound emails programmatically.

This guide covers everything you need to know about SendGrid webhooks: their features, how to configure them, best practices for production deployments, and the common pain points developers face along with solutions to address them.

What are SendGrid webhooks?

SendGrid provides two distinct webhook integrations that serve different purposes:

Event Webhook — Sends real-time notifications about outbound email events as SendGrid processes them. When something happens to an email you've sent (delivery, bounce, open, click, spam report, etc.), SendGrid POSTs event data to a URL you configure. This is the primary webhook integration most developers work with.

Inbound Parse Webhook — Receives and parses incoming emails sent to your domain. When an email arrives, SendGrid deconstructs it into its component parts (headers, body, attachments) and POSTs the parsed data to your endpoint. This enables use cases like email-to-ticket systems, automated reply processing, and email-triggered workflows.

This guide covers both webhook types, with a focus on the Event Webhook as it's the most commonly used integration.

SendGrid webhook features

FeatureDetails
Webhook typesEvent Webhook (outbound tracking) and Inbound Parse (inbound email processing)
Webhook configurationSendGrid Dashboard UI or HTTP API
Signing algorithmECDSA (Elliptic Curve Digital Signature Algorithm)
AuthenticationECDSA signatures, OAuth 2.0, or both
Timeout10-second response timeout
Retry logicAutomatic retries with exponential backoff for up to 24 hours
Event batchingEvents batched per ~30 seconds or 768 KB, whichever comes first
Payload formatJSON array (Event Webhook), multipart/form-data (Inbound Parse)
Manual retryNot available
Browsable logNot available (Email Activity Feed provides limited event visibility)

Supported event types

SendGrid's Event Webhook tracks events across two categories: delivery events and engagement events.

Delivery events

EventDescription
processedMessage received by SendGrid and prepared for delivery
deliveredMessage successfully accepted by the receiving server
deferredRecipient's email server temporarily rejected the message
bounceReceiving server permanently rejected the message
droppedSendGrid did not deliver the message (suppression list, invalid email, etc.)

Engagement events

EventDescription
openRecipient opened the HTML message (tracked via pixel)
clickRecipient clicked a link within the message
spamreportRecipient marked the email as spam
unsubscribeRecipient clicked the email's subscription management link
group_unsubscribeRecipient unsubscribed from a specific suppression group
group_resubscribeRecipient resubscribed to a specific suppression group

Event Webhook payload structure

When SendGrid sends event data, it delivers a JSON array containing one or more events. Events are batched together — a single POST request may contain anywhere from 1 to over 100 events depending on your email volume.

[
  {
    "email": "john.doe@example.com",
    "timestamp": 1706097400,
    "smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
    "event": "delivered",
    "category": ["transactional"],
    "sg_event_id": "ZGVsaXZlcmVkLTEyMzQ1Njc4OWFiY2RlZg",
    "sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
    "response": "250 2.0.0 OK",
    "ip": "168.1.1.1",
    "tls": 1
  },
  {
    "email": "jane.smith@example.com",
    "timestamp": 1706097450,
    "smtp-id": "<14c5d75ce94.dfd.64b469@ismtpd-555>",
    "event": "open",
    "category": ["marketing"],
    "sg_event_id": "b3BlbmVkLTEyMzQ1Njc4OWFiY2RlZg",
    "sg_message_id": "14c5d75ce94.dfd.64b469.filter0001.16648.5515E0B88.1",
    "useragent": "Mozilla/5.0",
    "ip": "203.0.113.1"
  },
  {
    "email": "bob.wilson@example.com",
    "timestamp": 1706098000,
    "smtp-id": "<14c5d75ce95.dfd.64b469@ismtpd-555>",
    "event": "click",
    "category": ["marketing"],
    "sg_event_id": "Y2xpY2tlZC0xMjM0NTY3ODlhYmNkZWY",
    "sg_message_id": "14c5d75ce95.dfd.64b469.filter0001.16648.5515E0B88.2",
    "url": "https://example.com/promo",
    "useragent": "Mozilla/5.0",
    "ip": "198.51.100.1"
  },
  {
    "email": "alice.brown@example.com",
    "timestamp": 1706098500,
    "smtp-id": "<14c5d75ce96.dfd.64b469@ismtpd-555>",
    "event": "bounce",
    "category": ["transactional"],
    "sg_event_id": "Ym91bmNlLTEyMzQ1Njc4OWFiY2RlZg",
    "sg_message_id": "14c5d75ce96.dfd.64b469.filter0001.16648.5515E0B88.3",
    "reason": "550 5.1.1 The email account does not exist",
    "type": "bounce",
    "status": "5.1.1"
  },
  {
    "email": "charlie.davis@example.com",
    "timestamp": 1706099000,
    "smtp-id": "<14c5d75ce97.dfd.64b469@ismtpd-555>",
    "event": "spamreport",
    "category": ["newsletter"],
    "sg_event_id": "c3BhbXJlcG9ydC0xMjM0NTY3ODlhYmNkZWY",
    "sg_message_id": "14c5d75ce97.dfd.64b469.filter0001.16648.5515E0B88.4"
  }
]

Key payload fields

FieldDescription
emailRecipient's email address
timestampUnix timestamp when the event occurred
eventEvent type (delivered, open, click, bounce, etc.)
sg_event_idUnique identifier for this specific event (URL-safe Base64 encoded)
sg_message_idUnique identifier for the message
smtp-idSMTP transaction ID from the original message
categoryArray of categories assigned to the message
responseSMTP response from the receiving server (delivery events)
reasonReason for bounce or drop
urlURL clicked (click events only)
useragentRecipient's user agent string (open and click events)
ipIP address (varies by event type)

Event-specific fields

Different event types include additional fields relevant to their context. Bounce events include type (bounce, blocked) and status (SMTP status code). Click events include the url that was clicked. Open events include useragent and ip. Deferred events include the SMTP response explaining the temporary rejection. Dropped events include reason explaining why SendGrid didn't attempt delivery.

Inbound Parse payload structure

The Inbound Parse Webhook sends parsed email data as multipart/form-data, the same format used for HTML file uploads. This differs from the Event Webhook's JSON format.

Key Inbound Parse fields

FieldDescription
toRecipient email address(es)
fromSender email address
ccCarbon copy recipients
subjectEmail subject line
textPlain text version of the email body
htmlHTML version of the email body
headersComplete email headers
envelopeSMTP envelope information (JSON string)
attachmentXFile attachments (e.g., attachment1, attachment2)
attachment-infoJSON metadata for all attachments
sender_ipIP address of the sending server
charsetsCharacter encodings for each field (JSON string)
SPFSPF verification result (pass, fail, none)
dkimDKIM verification results
spam_scoreSpam score (if spam checking is enabled)
spam_reportDetailed spam analysis (if spam checking is enabled)

When the "Post the raw, full MIME message" option is enabled, the payload includes an additional email field containing the complete raw MIME message.

Security and authentication

SendGrid provides two independent authentication mechanisms that can be used individually or together for defense-in-depth.

ECDSA signed webhooks

SendGrid signs webhook requests using the Elliptic Curve Digital Signature Algorithm (ECDSA). When enabled, each request includes two headers:

  • X-Twilio-Email-Event-Webhook-Signature — The ECDSA signature
  • X-Twilio-Email-Event-Webhook-Timestamp — The timestamp used in signature generation

You verify the signature using a public key that SendGrid provides when you enable the feature. The signature is generated from the SHA-256 hash of the timestamp concatenated with the raw request body.

OAuth 2.0

SendGrid can authenticate webhook requests using OAuth 2.0 Client Credentials flow. You provide an OAuth token URL, client ID, and client secret. SendGrid obtains an access token and includes it in the Authorization header of each webhook request. Your endpoint validates the token against your OAuth server.

Using both together

ECDSA and OAuth are independent — enabling one doesn't affect the other. For maximum security, you can enable both simultaneously, giving you cryptographic payload verification alongside token-based authentication.

Setting up the Event Webhook

Via the SendGrid Dashboard

  1. Log into the Twilio SendGrid Console
  2. Navigate to Settings > Mail Settings
  3. Click Event Webhooks under Webhook Settings
  4. Click Create new webhook
  5. Enter your POST URL — the HTTPS endpoint that will receive events
  6. Select the event types you want to track (delivery events, engagement events, or both)
  7. Optionally enable security features:
    • Toggle Signed Event Webhook and copy the generated public key
    • Toggle OAuth and provide your OAuth credentials
  8. Click Save
  9. Click Test Your Integration to send a sample POST to your endpoint

Via the API

curl -X POST https://api.sendgrid.com/v3/user/webhooks/event/settings \
  -H "Authorization: Bearer $SENDGRID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "url": "https://your-endpoint.com/webhooks/sendgrid",
    "bounce": true,
    "click": true,
    "deferred": true,
    "delivered": true,
    "dropped": true,
    "group_resubscribe": true,
    "group_unsubscribe": true,
    "open": true,
    "processed": true,
    "spam_report": true,
    "unsubscribe": true
  }'

Setting up Inbound Parse

Setting up Inbound Parse requires both DNS configuration and SendGrid configuration.

Step 1: Configure your MX record

Create an MX record for the subdomain you'll use to receive email:

  • Host/Name: parse.yourdomain.com (or your chosen subdomain)
  • Priority: 10
  • Value: mx.sendgrid.net

MX record propagation can take up to 48 hours.

Step 2: Configure in SendGrid

  1. Navigate to Settings > Inbound Parse
  2. Click Add Host & URL
  3. Enter the receiving domain (the subdomain you configured the MX record for)
  4. Enter the destination URL (your webhook endpoint)
  5. Optionally enable Check incoming emails for spam and Post the raw, full MIME message
  6. Click Save

Step 3: Configure security

Navigate to your Inbound Parse webhook security settings and enable ECDSA signature verification, OAuth 2.0, or both.

Best practices when working with SendGrid webhooks

Verifying ECDSA signatures

When processing webhooks from SendGrid, verify the ECDSA signature to confirm requests originated from SendGrid and haven't been tampered with. The most critical requirement: you must verify against the raw request body, not parsed JSON.

Node.js

const express = require("express");
const { EventWebhook, EventWebhookHeader } = require("@sendgrid/eventwebhook");

const app = express();

// Capture raw body for signature verification
app.use(
  "/webhooks/sendgrid",
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString("utf8");
    },
  })
);

function verifySendGridSignature(publicKey, rawBody, signature, timestamp) {
  const eventWebhook = new EventWebhook();
  const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
  return eventWebhook.verifySignature(
    ecPublicKey,
    rawBody,
    signature,
    timestamp
  );
}

app.post("/webhooks/sendgrid", (req, res) => {
  const signature = req.headers[EventWebhookHeader.SIGNATURE().toLowerCase()];
  const timestamp = req.headers[EventWebhookHeader.TIMESTAMP().toLowerCase()];

  if (
    !verifySendGridSignature(
      process.env.SENDGRID_WEBHOOK_PUBLIC_KEY,
      req.rawBody,
      signature,
      timestamp
    )
  ) {
    return res.status(401).send("Invalid signature");
  }

  // Acknowledge immediately, process asynchronously
  res.status(200).send("OK");

  // Process events in background
  processEvents(req.body);
});

Python

import os
from flask import Flask, request, abort
from sendgrid.helpers.eventwebhook import EventWebhook, EventWebhookHeader

app = Flask(__name__)

def verify_sendgrid_signature(public_key, raw_body, signature, timestamp):
    event_webhook = EventWebhook()
    ec_public_key = event_webhook.convert_public_key_to_ecdsa(public_key)
    return event_webhook.verify_signature(
        raw_body, signature, timestamp, ec_public_key
    )

@app.route('/webhooks/sendgrid', methods=['POST'])
def handle_sendgrid_webhook():
    signature = request.headers.get(EventWebhookHeader.SIGNATURE)
    timestamp = request.headers.get(EventWebhookHeader.TIMESTAMP)

    if not verify_sendgrid_signature(
        os.environ['SENDGRID_WEBHOOK_PUBLIC_KEY'],
        request.get_data(as_text=True),
        signature,
        timestamp
    ):
        abort(401)

    # Acknowledge immediately
    # Queue events for async processing
    return 'OK', 200

Common pitfall: If you parse the JSON body before verification (for example, by using express.json() middleware globally or accessing request.json in Flask), the serialized output will differ from the original raw body, and verification will fail. Always capture and verify against the raw bytes.

Respond immediately, process asynchronously

SendGrid has a strict 10-second timeout. If your endpoint doesn't return a 2xx response within that window, SendGrid treats the delivery as failed and begins retrying. Always return 200 OK immediately and process events in a background job or queue.

Use sg_event_id for idempotent processing

SendGrid may deliver the same event multiple times, especially during retries. Use the sg_event_id field to deduplicate events and ensure idempotent processing:

async function processEvent(event) {
  const eventId = event.sg_event_id;

  // Check if already processed
  const exists = await redis.get(`processed:${eventId}`);
  if (exists) {
    console.log(`Event ${eventId} already processed, skipping`);
    return;
  }

  // Process the event
  await handleEvent(event);

  // Mark as processed with TTL
  await redis.setex(`processed:${eventId}`, 172800, "1"); // 48-hour TTL
}

Handle batched events correctly

A single webhook POST from SendGrid contains an array of events, not a single event. Always iterate over the array and process each event individually.

Don't rely on event ordering

Events may arrive out of chronological order. An open event might be delivered before the corresponding delivered event. Use the timestamp field to determine the actual sequence and design your processing logic to handle events arriving in any order.

SendGrid webhook limitations and pain points

10-second response timeout

The Problem: SendGrid enforces a hard 10-second timeout waiting for your endpoint to respond with a 2xx status code. If your endpoint takes longer (whether due to database writes, external API calls, or processing complex logic) SendGrid treats the delivery as failed and begins retry attempts.

Why It Happens: The timeout is enforced at SendGrid's infrastructure level and cannot be configured through the dashboard or API. It's designed to prevent SendGrid's delivery infrastructure from blocking on slow consumers.

Workarounds:

  • Always return 200 OK immediately and process events asynchronously using a message queue (Hookdeck, Redis, RabbitMQ, SQS, etc.)
  • Avoid making external API calls or database writes in the webhook handler itself
  • Implement a lightweight ingestion layer that buffers events for later processing

How Hookdeck Can Help: Hookdeck accepts webhook deliveries from SendGrid and forwards them to your endpoint with configurable delivery timeouts, giving your application more time to process complex event batches without triggering SendGrid's retry mechanism.

Duplicate event delivery

The Problem: SendGrid's retry mechanism frequently results in duplicate event deliveries. Any response that isn't 2xx (including timeouts, network blips, or temporary server errors) triggers retries that create duplicates.

Why It Happens: SendGrid retries webhook deliveries with exponential backoff for up to 24 hours when it doesn't receive a 2xx response. Combined with the aggressive 10-second timeout, even brief latency spikes can trigger cascading retries. There's no built-in deduplication on SendGrid's side.

Workarounds:

  • Implement idempotent processing using the sg_event_id field as a deduplication key
  • Store processed event IDs in a fast lookup store (Redis, DynamoDB) with a TTL matching or exceeding the 24-hour retry window
  • Add database-level unique constraints on sg_event_id to prevent duplicate records

How Hookdeck Can Help: Hookdeck's deduplication feature automatically filters duplicate webhook deliveries based on payload content, ensuring your endpoint only processes each unique event once regardless of how many times SendGrid retries the delivery.

Out-of-order event delivery

The Problem: SendGrid does not guarantee that events arrive in chronological order. You may receive an open event before the corresponding delivered event for the same message, or a click event before the open. This makes it difficult to build reliable state machines or event timelines.

Why It Happens: Events are processed across SendGrid's distributed infrastructure and batched independently. Different event types may be generated by different systems at different times, and network latency varies across delivery paths. The batching mechanism (approximately every 30 seconds or 768 KB) doesn't sort events before sending.

Workarounds:

  • Use the timestamp field to determine actual event chronology rather than relying on delivery order
  • Design your processing logic to be order-independent where possible
  • Implement a short buffering window to collect related events before processing them together
  • Store events with their timestamps and reconstruct the timeline after the fact

How Hookdeck Can Help: Hookdeck allows you to transform, buffer, and reorder events before forwarding them to your endpoint, helping you normalize event delivery patterns without building custom infrastructure.

Single webhook URL per account

The Problem: SendGrid only allows you to configure one webhook URL per Event Webhook. You cannot route different event types to different endpoints — all selected events go to the same URL. Organizations with multiple internal systems (billing, CRM, analytics, customer support) must receive all events at a single URL and distribute them internally.

Why It Happens: SendGrid's webhook configuration is designed around a single endpoint per webhook. While you can create multiple Event Webhooks, each one receives the full set of selected events, creating duplicate traffic rather than true event routing.

Workarounds:

  • Build a custom routing layer that receives all events and forwards them to the appropriate internal service based on event type
  • Use a middleware service to fan out events to multiple destinations
  • Accept the single-URL constraint and have one service process all event types

How Hookdeck Can Help: Hookdeck is purpose-built to solve this problem. Configure your SendGrid webhook to point to a Hookdeck source URL, then create multiple connections with filters to route specific event types to different destinations. For example, route bounce events to your deliverability system, open/click events to your analytics service, and spamreport events to your compliance team — all from a single SendGrid webhook.

No manual retry or event replay

The Problem: SendGrid provides automatic retries for up to 24 hours, but offers no way to manually replay failed webhooks, retrieve historical events via the webhook system, or recover events that were lost after the retry window expired. If your endpoint is down for more than 24 hours, those events are permanently lost.

Why It Happens: SendGrid's webhook system is designed as a fire-and-forget push mechanism. There is no persistent event store backing the webhook delivery system so events are generated, batched, delivered (with retries), and then discarded.

Workarounds:

  • Use the Email Activity API as a fallback data source (limited to 7 or 30 days depending on your plan)
  • Implement your own event store that logs all received webhooks for gap analysis and replay
  • Build redundancy into your webhook consumer infrastructure to minimize downtime
  • Run periodic reconciliation against the Email Activity API to detect missed events

How Hookdeck Can Help: Hookdeck automatically stores all webhook events, providing a browsable log with full request and response details. Failed deliveries are preserved and can be manually retried from the dashboard or API at any time, eliminating the risk of permanent event loss.

Signature verification complexity

The Problem: ECDSA signature verification is a frequent source of bugs and failed integrations. The most common failure mode: parsing the JSON body before verification, which changes the raw bytes and breaks the signature check.

Why It Happens: SendGrid signs the raw request body bytes. Most web frameworks automatically parse the request body (JSON for Event Webhook, multipart for Inbound Parse), and the re-serialized output differs from the original payload due to whitespace, key ordering, or encoding differences. The ECDSA algorithm is unforgiving — any byte-level difference causes verification to fail.

Workarounds:

  • Configure your web framework to capture the raw request body before parsing (e.g., using express.json() with a verify callback in Node.js, or request.get_data() in Flask)
  • Exclude your webhook route from global body-parsing middleware
  • Test verification with SendGrid's "Test Your Integration" feature before going live
  • Carefully follow the SDK documentation for your specific language

How Hookdeck Can Help: Hookdeck handles SendGrid signature verification automatically when you enable SendGrid as a verified source. Hookdeck verifies the original request's authenticity and sets an x-hookdeck-verified header to true, so your endpoint can trust all forwarded requests without implementing verification logic.

Limited observability and delivery logging

The Problem: SendGrid provides minimal visibility into webhook delivery success or failure. There's no dedicated webhook delivery log in the dashboard, no metrics on delivery latency or error rates, and no alerting when deliveries consistently fail. The Email Activity Feed tracks email events but not webhook delivery status.

Why It Happens: SendGrid's webhook system was built as a simple notification mechanism rather than a full event delivery platform. Observability features like delivery logs, latency metrics, and failure alerting are outside its design scope.

Workarounds:

  • Implement request logging on your webhook endpoint to track received events
  • Monitor your endpoint's error rate and latency using APM tools (Datadog, New Relic, etc.)
  • Set up synthetic monitoring that watches for gaps in expected event flow
  • Log webhook receipts to a time-series database for anomaly detection

How Hookdeck Can Help: Hookdeck's dashboard provides complete visibility into every webhook delivery, including request/response details, latency, HTTP status codes, and error information. Configure alerts to notify you when delivery issues occur, and use the browsable log to debug problems in real time.

Event data retention constraints

The Problem: SendGrid's Email Activity Feed (the only alternative source for event data) retains events for just 7 days on the free tier and 30 days on paid plans. Webhook events themselves have no retention: once the 24-hour retry window expires, undelivered events are gone. There's no way to query or export historical webhook data from SendGrid.

Why It Happens: SendGrid's event storage is designed for short-term operational visibility rather than long-term data warehousing. The webhook system has no persistent backing store, and the Email Activity API is intended for recent troubleshooting, not historical analysis.

Workarounds:

  • Build a dedicated event store that captures all webhook events as they arrive
  • Export events to a data warehouse (BigQuery, Snowflake, Redshift) for long-term retention
  • Use the Email Activity API for reconciliation within its retention window
  • Implement a dead letter queue for failed events to prevent permanent loss

How Hookdeck Can Help: Hookdeck retains webhook event data with full request and response details, providing a persistent log that extends well beyond SendGrid's native retention limits. Events can be searched, filtered, and replayed from the Hookdeck dashboard or API.

Testing SendGrid webhooks

Use the built-in test feature

The Event Webhook configuration includes a Test Your Integration button that sends a sample POST containing example events to your endpoint. Keep in mind that test payloads may differ from production events in structure and content and some edge cases only surface with real email data.

Use a request inspector

Before building your handler, inspect real SendGrid payloads using a tool like Hookdeck Console:

  1. Create a temporary Hookdeck source URL
  2. Configure it as your SendGrid webhook endpoint
  3. Send a test email to trigger real events
  4. Inspect the full payload structure, headers, and batching behavior in the Hookdeck dashboard

This is especially valuable for understanding batching behavior and the specific fields included with each event type.

Validate in staging

Test your webhook integration against realistic scenarios before deploying to production:

  • Single email delivery lifecycle (processed > delivered > open > click)
  • Bounce handling (hard bounce, soft bounce, block)
  • High-volume batches (send bulk email and observe batching behavior)
  • Failure recovery (take your endpoint offline, let retries accumulate, bring it back)
  • Inbound Parse with various email clients (they produce different payload structures)

Local development with tunneling

SendGrid requires a publicly accessible URL for webhook delivery. For local development, use a tunneling tool like Hookdeck's CLI to expose your local server.

Configure the generated HTTPS URL as your webhook endpoint in SendGrid. Remember to use separate webhook configurations for development and production environments.

Conclusion

SendGrid webhooks provide essential real-time visibility into your email delivery pipeline, from tracking whether messages reached the inbox to monitoring recipient engagement. The Event Webhook's comprehensive event types and ECDSA signature verification make it a capable foundation for building email-driven workflows, while Inbound Parse opens up powerful use cases around receiving and processing incoming email programmatically.

However, production deployments require careful consideration of SendGrid's limitations. The 10-second timeout, frequent duplicate deliveries, out-of-order events, and lack of manual replay mean you'll need to invest in asynchronous processing, deduplication logic, and resilient infrastructure. The single webhook URL constraint and limited observability tools add further complexity for teams routing events to multiple systems.

For straightforward integrations with moderate email volumes and reliable infrastructure, SendGrid's built-in webhook capabilities combined with proper signature verification, idempotent processing, and asynchronous handling will serve you well. For high-volume email operations, multi-destination routing, or mission-critical workflows where delivery guarantees and observability matter, webhook infrastructure like Hookdeck can address SendGrid's limitations by providing configurable timeouts, automatic deduplication, event routing, comprehensive delivery logging, and manual replay capabilities without modifying your SendGrid configuration.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.