SendGrid email webhooks let your application receive delivery, bounce, engagement, and suppression-related signals as SendGrid processes email. The useful outcome is not simply seeing a stream of JSON: it is keeping customer records accurate, protecting your sending reputation, and triggering the right product workflow when an email succeeds or fails.

What SendGrid email webhooks do

A webhook is an HTTP callback: instead of your application repeatedly asking for email activity, SendGrid sends an HTTP POST request to a URL you control when an event occurs. The SendGrid Event Webhook is designed for email-processing events and can deliver event data in near real time. (twilio.com)

This is different from the Inbound Parse Webhook. Event Webhook tells you what happened to outbound email—such as processed, delivered, bounce, open, or click. Inbound Parse is for receiving and parsing messages sent to a domain you configure. Do not point an Event Webhook at a handler built to expect inbound messages and attachments.

For a product team, the main jobs for SendGrid email webhooks are usually:

  • Marking a transactional email as accepted for processing or delivered.
  • Identifying invalid addresses and preventing further sends to them.
  • Recording temporary delivery failures separately from permanent failures.
  • Tracking unsubscribes and spam reports in a contact or consent system.
  • Connecting events back to a user, tenant, notification, campaign, or internal message record.
  • Investigating support tickets with an event timeline rather than guessing whether an email was sent.
  • Sending event data to a warehouse, observability platform, or internal analytics pipeline.

SendGrid groups its tracked events into delivery, engagement, and account categories. Delivery events include bounced, delivered, deferred, dropped, and processed messages; engagement events describe recipient interactions. (twilio.com)

A webhook is therefore an event feed, not a replacement for your own delivery model. Your system should retain its own durable records of messages, recipients, state changes, and business decisions.

Choose the events that support a real decision

It is tempting to switch on every event available. That can be reasonable for a data warehouse, but it is not automatically useful for an application database. Begin with the decision each event will power.

The minimum production event set

For most transactional-email systems, enable these events first:

  1. Processed — SendGrid accepted the message for processing. This is helpful for tracing, but it is not proof that the recipient received the message.
  2. Delivered — the receiving mail system accepted the message. Treat this as delivery confirmation from the recipient’s mail server, not proof that a person read it.
  3. Bounce — the recipient server rejected delivery. The payload can include fields such as reason, status, type, and bounce_classification.
  4. Deferred — delivery is delayed and may be tried later; do not treat this as a permanent failure.
  5. Dropped — SendGrid did not send the message, often because a suppression or other sending rule applied.
  6. Spam report — a recipient reported the message as spam; this should feed a suppression or compliance workflow.
  7. Unsubscribe and, where relevant, group-level unsubscribe events — these should update consent immediately.

The Event Webhook reference identifies email, event, timestamp, sg_event_id, and sg_message_id among the expected fields for delivery events, while additional properties vary by event type. In delayed or asynchronous bounce cases, sg_message_id may be absent. (twilio.com)

Events that need a careful interpretation

Open and click events are useful behavioral signals, especially for lifecycle email and campaigns. But they are not a reliable measure of human attention. Image proxying, privacy features, link scanners, security gateways, and mail-client behavior can create misleading opens or clicks. Use them to understand patterns, trigger low-risk follow-up flows, or enrich reporting—not to make irreversible conclusions about an individual.

Processed is also commonly misread. It means the message entered SendGrid’s processing path. It does not mean it was accepted by a recipient server. A practical internal lifecycle is:

queued internally -> accepted by SendGrid -> processed -> delivered | deferred | bounce | dropped

The stages are not a universal finite-state machine. A single email can produce several related events over time, and event delivery may not align with the order your business system expects. Preserve the original events, then derive a current status using rules that suit your product.

Design the endpoint before turning on the webhook

Your endpoint must be reachable from the public internet and accept HTTPS POST requests. A local address such as http://localhost:3000/webhooks/sendgrid cannot receive requests from SendGrid directly. For local testing, SendGrid’s setup documentation specifically suggests using a tunnel such as ngrok, or using a temporary webhook inspection service only with the dashboard’s sample test data. (twilio.com)

Use a dedicated route, for example:

POST https://api.example.com/webhooks/sendgrid/events

Avoid sharing this route with a browser form handler, a generic JSON ingestion endpoint, or a route that performs expensive work before acknowledging the request.

A production-ready endpoint checklist

Build the endpoint to do the following in this order:

  1. Receive the request body without changing its bytes.
  2. Read the SendGrid signature and timestamp headers.
  3. Verify the signature before trusting or parsing the event data.
  4. Reject a request with a missing or invalid signature.
  5. Parse the JSON payload only after verification succeeds.
  6. Store each event durably, deduplicating with an event identifier.
  7. Put any slow downstream work onto a queue or background worker.
  8. Return a successful HTTP response promptly after durable acceptance.

This structure minimizes a dangerous failure mode: accepting a forged webhook because the code trusts an ordinary JSON request, or losing a legitimate event because the handler waits for a CRM, analytics tool, or database-heavy workflow to complete.

Keep a raw payload copy

Signed webhooks require the original body. If middleware parses JSON and reserializes it before verification, whitespace or encoding differences can make a valid signature fail. The official SendGrid Node.js example explicitly warns that the request body must be verified raw—as a Buffer or string—not after JSON parsing. (github.com)

This detail matters more than most webhook tutorials suggest. A handler can log a body that appears identical to the original while still failing verification because it is no longer byte-for-byte identical.

Configure the SendGrid Event Webhook

In the SendGrid dashboard, go to the Event Webhook settings area, create a webhook, enter your HTTPS endpoint, and select the actions you want posted. SendGrid’s configuration guide describes the setup as pairing a URL you provide with SendGrid’s POST operation, and the dashboard includes a Test Your Integration action that posts an example JSON array to the configured URL. (twilio.com)

Give the webhook a meaningful operational name, such as production-event-ingest or staging-event-ingest. A name makes the dashboard easier to manage, but do not use a friendly name as a programmatic identifier: SendGrid’s API documentation says friendly names do not have to be unique, while the webhook ID is the reliable identifier. (twilio.com)

Configure it through the API when infrastructure is code

If your team manages infrastructure declaratively, SendGrid also exposes an API operation to create Event Webhooks:

POST /v3/user/webhooks/event/settings

The documented global base URL is https://api.sendgrid.com; SendGrid documents https://api.eu.sendgrid.com for EU regional subusers. The create operation returns a webhook ID that you can later use to update settings, retrieve the public key, or manage signature verification. (twilio.com)

Use the API approach only if you also have a safe process for API-key permissions, secret storage, environment separation, and configuration review. A dashboard-created webhook is perfectly acceptable for a small team; consistency and secure change control are more important than the provisioning method.

Enable signed webhook verification

After saving the webhook, enable Signed Event Webhook and copy the public verification key into your secrets manager or environment configuration. SendGrid generates a public/private key pair for this purpose and includes the request signature in the X-Twilio-Email-Event-Webhook-Signature header. (twilio.com)

There is an important sequencing detail: SendGrid generates the key pair when the webhook is saved. Its documentation notes that testing before saving will not test signature verification because the key pair does not yet exist. (twilio.com)

Do not mistake a private endpoint URL, an obscure path, a firewall rule, or an IP allowlist for signature verification. Those may add defense in depth, but cryptographic verification is what establishes that the request was signed for your configured webhook.

Verify the SendGrid signature correctly

SendGrid supports two independent Event Webhook security mechanisms: cryptographic signing and OAuth 2.0. They can be used separately or together. For most teams, signed webhooks are the practical baseline because they let the receiver verify payload authenticity with the webhook’s public key. (twilio.com)

Signature verification should be mandatory in production. If it fails, return an error and log a safe diagnostic record—such as request ID, timestamp header presence, and validation result—without storing sensitive payload contents in plaintext logs.

Worked example: Node.js and Express

The following example uses SendGrid’s @sendgrid/eventwebhook package. The package is part of the official SendGrid Node.js library and is intended to validate Event Webhook requests. (github.com)

Install dependencies:

npm install express @sendgrid/eventwebhook

Set the webhook’s copied public key as an environment variable. Preserve line breaks if your deployment platform requires them, or store the key in a managed secret service.

export SENDGRID_EVENT_WEBHOOK_PUBLIC_KEY='-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----'

Create server.js:

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

const app = express();
const webhook = new EventWebhook();

// Important: this route receives raw bytes. Do not put express.json()
// in front of this route.
app.post(
  '/webhooks/sendgrid/events',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.get(EventWebhookHeader.SIGNATURE());
    const timestamp = req.get(EventWebhookHeader.TIMESTAMP());

    if (!signature || !timestamp) {
      return res.status(400).send('Missing webhook signature headers');
    }

    const publicKey = webhook.convertPublicKeyToECDSA(
      process.env.SENDGRID_EVENT_WEBHOOK_PUBLIC_KEY
    );

    const verified = webhook.verifySignature(
      publicKey,
      req.body,
      signature,
      timestamp
    );

    if (!verified) {
      return res.status(401).send('Invalid webhook signature');
    }

    let events;
    try {
      events = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.status(400).send('Invalid JSON');
    }

    if (!Array.isArray(events)) {
      return res.status(400).send('Expected an event array');
    }

    for (const event of events) {
      // Insert with a UNIQUE constraint on sg_event_id.
      // Then enqueue follow-up work rather than doing it inline.
      console.log({
        id: event.sg_event_id,
        type: event.event,
        email: event.email,
        timestamp: event.timestamp,
        messageId: event.sg_message_id
      });
    }

    return res.status(204).end();
  }
);

// JSON middleware for ordinary application routes must come after the
// raw webhook route, or be scoped so it excludes that route.
app.use(express.json());

app.listen(3000, () => {
  console.log('Listening on port 3000');
});

The critical parts are the route-specific express.raw() middleware, use of the signature and timestamp headers, the raw req.body passed into verifySignature, and JSON parsing only after verification. The official package example follows the same pattern: it excludes the webhook route from JSON parsing and verifies the raw body with verifySignature. (github.com)

Do not write your own cryptography unless necessary

A custom verification implementation has to get the signature algorithm, header values, timestamp, payload bytes, key representation, and encoding exactly right. That is a poor place to optimize or improvise. Use SendGrid’s maintained helper for the runtime you use, or follow the provider’s current verification documentation precisely.

If you must use a language without a helper, build isolated verification tests from captured, non-production test requests. Test valid signatures, invalid signatures, missing headers, altered bodies, altered timestamps, malformed JSON, and arrays containing multiple events.

Understand the payload and identify your own messages

SendGrid Event Webhook payloads are JSON arrays. Each element represents an event, so your handler must iterate over the array rather than assuming one request equals one event. SendGrid’s examples show an array containing event objects, and its Node.js guidance similarly processes the request body as a collection of events. (twilio.com)

A simplified delivery event can look like this:

[
  {
    "email": "alex@example.com",
    "timestamp": 1513299569,
    "event": "bounce",
    "sg_event_id": "6g4ZI7SA-xmRDv57GoPIPw==",
    "sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
    "reason": "500 unknown recipient",
    "status": "5.0.0",
    "type": "bounce",
    "bounce_classification": "Invalid Address"
  }
]

The fields shown above are based on SendGrid’s bounce example. Actual fields vary by event type, so make your ingestion schema flexible enough to preserve the original JSON. (twilio.com)

Use custom arguments for correlation

The strongest way to map a webhook event back to your application is to send a stable internal reference with the message. With SendGrid’s V3 Mail Send API, use custom_args; older SMTP and V2 Mail Send patterns use unique_args. SendGrid documents that unique arguments appear in Event Webhook data and that V3 Mail Send uses custom arguments instead. (twilio.com)

For example, when sending a password-reset email, attach non-sensitive identifiers:

{
  "from": { "email": "security@example.com" },
  "personalizations": [
    {
      "to": [{ "email": "alex@example.com" }],
      "custom_args": {
        "message_ref": "msg_01JXYZ",
        "template_kind": "password_reset",
        "tenant_ref": "tenant_42"
      }
    }
  ],
  "subject": "Reset your password",
  "content": [
    {
      "type": "text/plain",
      "value": "Use the secure reset link in this email."
    }
  ]
}

Then a later event can be connected to msg_01JXYZ without attempting to infer ownership from an email address alone. This is particularly important for multi-tenant products, resend workflows, and systems where a user can change their email address.

Never put PII in categories or custom tracking data

Do not put names, email addresses, reset tokens, order contents, account numbers with real-world sensitivity, or other personal data in custom_args, unique_args, or categories. SendGrid warns that categories and unique arguments are treated as non-PII fields, cannot generally be redacted or removed, may be stored long-term, and may be visible to SendGrid personnel. (twilio.com)

Use opaque references instead. user_8921 may be acceptable if it is meaningless outside your systems; alex@example.com and a password-reset token are not. If an identifier can be looked up only in your database, it gives you correlation without exporting the underlying customer information.

Store events idempotently and derive state carefully

A reliable webhook consumer assumes the same event may be received more than once and that events may arrive close together or in an unexpected sequence. The practical defense is idempotency.

Create a table or collection with at least:

sendgrid_event_id       unique
received_at             server timestamp
provider_timestamp      event timestamp
message_ref             your custom argument, if present
sg_message_id           provider message identifier, nullable
recipient               protected or minimized as appropriate
event_type              delivered, bounce, open, etc.
payload_json            original verified event
processed_at            nullable

Use sg_event_id as the first-choice uniqueness key when it exists. Insert the raw verified event with a unique database constraint. If a second attempt hits the same constraint, acknowledge it without repeating side effects.

This prevents duplicate actions such as sending two Slack alerts for one bounce, deleting a customer twice, or repeatedly creating CRM activity records. It also lets you replay downstream processing safely if a worker fails after the event was stored.

Keep raw facts and derived facts separate

Store raw events as immutable facts. Store a separate, derived record for your application’s current status:

message_ref: msg_01JXYZ
latest_delivery_state: bounced
latest_event_at: 2025-01-10T14:20:00Z
recipient_eligible_for_transactional_send: false

The raw record answers, “What did SendGrid report?” The derived record answers, “What should our product do now?” Keeping them separate makes it possible to fix state-transition bugs and recompute status from a known event history.

For example:

  • A delivered event updates message delivery state to delivered.
  • A hard bounce can mark the address as undeliverable, subject to your suppression policy.
  • A deferred event increments a monitoring counter but should not immediately disable the recipient.
  • An unsubscribe changes marketing eligibility, while a critical service notice may be governed by a distinct consent and legal policy.
  • A spam report should create a high-priority review or suppression action.

Do not treat every bounce alike. SendGrid distinguishes a bounce—a permanent delivery denial—from blocked, which it describes as a temporary delivery denial. (twilio.com)

Handle bounces, drops, spam reports, and unsubscribes safely

The most valuable SendGrid email webhook workflows are usually suppression and data-quality workflows, not open-rate dashboards.

Bounces and blocked delivery

A bounce event can include SMTP status and a human-readable reason. Persist both because they are essential for debugging. SendGrid’s sample bounce data includes reason, status, type, and bounce_classification, while the exact shape is event-specific. (twilio.com)

A sensible policy is:

  • Treat a confirmed invalid-address hard bounce as a signal to stop attempting ordinary sends to that address.
  • Treat a blocked or temporary condition as a delivery-health issue to monitor, not proof the address is permanently invalid.
  • Never auto-delete the user account because an address bounced. The address may be outdated while the account remains legitimate.
  • Surface bounce details to support and operations staff through an internal event timeline.

Before adding a new address to a high-value workflow, you can also use an email address verification tool as an extra quality check. Verification is not a substitute for handling real delivery events, because final acceptance still depends on the recipient system and the context of the send.

Drops are not delivery attempts

A dropped event means SendGrid did not send the email. Investigate the event reason and your suppression state before retrying. Blind retries can create repeated failures or send to an address the recipient previously unsubscribed from.

Your application should distinguish:

send request accepted by API
!= processed by SendGrid
!= delivered to recipient server
!= read by recipient

That distinction avoids a common support mistake: telling someone “the email was delivered” because the application successfully called the mail API.

Unsubscribes and spam reports

Treat unsubscribe and spam-report webhooks as compliance-sensitive events. Apply the change to the appropriate sending category or consent model promptly, preserve an audit record, and make the process idempotent. If your product uses multiple SendGrid unsubscribe groups, map each group deliberately instead of storing a vague global boolean that loses the user’s actual preference.

A spam report is a stronger negative signal than an ordinary non-open. It should not trigger an automated “please tell us why” email. It should reduce the chance of another unwanted message and prompt a review of list source, targeting, frequency, and content.

Test the full integration, not only the endpoint

SendGrid’s dashboard test is useful because it sends example event data to the exact configured Post URL. Use it after saving the webhook and after enabling signing. (twilio.com)

But a dashboard test is only the first layer. A complete test plan should cover behavior in your own systems.

A practical testing sequence

  1. Deploy the endpoint to a staging environment with HTTPS.
  2. Configure a staging Event Webhook URL and select a small event set.
  3. Enable signing, retrieve the public key, and store it as a staging secret.
  4. Use Test Your Integration to confirm the endpoint receives an event array.
  5. Confirm a valid signed request returns a success response.
  6. Alter one byte of a captured test payload and confirm verification fails.
  7. Remove the signature header and confirm verification fails.
  8. Send a real test email with a unique message_ref custom argument.
  9. Confirm the stored event has the expected internal reference and provider event ID.
  10. Replay the same event and verify the database does not repeat side effects.
  11. Simulate a downstream worker failure and verify the raw event remains available for retry.

A webhook integration has worked when all of the following are true:

  • The configured URL receives SendGrid’s test request.
  • Valid signatures pass and invalid signatures fail.
  • One request containing multiple events is processed correctly.
  • Every stored event is traceable to a provider ID and, ideally, your own message reference.
  • Duplicate deliveries do not duplicate business actions.
  • A bounce, unsubscribe, or spam report visibly updates the intended internal state.
  • Operational logs show failures without exposing unnecessary recipient data or message contents.

Common SendGrid email webhook problems and fixes

Signature verification always fails

The usual cause is parsing JSON before verification. Place express.raw({ type: 'application/json' }) on the webhook route and ensure global JSON middleware does not run first. SendGrid’s official Node example calls this out explicitly. (github.com)

Other causes include copying the wrong public key, failing to preserve PEM line breaks in an environment variable, using a key from another environment, reading the wrong headers, or testing before the signed webhook configuration was saved.

The webhook test never reaches the app

Confirm that the endpoint is public, HTTPS is configured correctly, DNS resolves, and your reverse proxy forwards POST requests and request bodies. For local work, use a tunnel; SendGrid cannot reach localhost on your laptop. (twilio.com)

Also verify that your deployment platform is not redirecting the request from one URL to another. Webhook endpoints should be direct and stable; avoid paths that require a browser cookie, interactive login, or a trailing-slash redirect.

Events arrive, but you cannot map them to a customer

Fix this at send time. Add opaque custom_args to V3 Mail Send requests and store your own outbound message record before sending. Do not depend only on the recipient address: one address may belong to multiple contacts, accounts, or notification attempts over time.

You receive a bounce but no sg_message_id

Do not reject the event. SendGrid documents that delayed or asynchronous bounces can lack sg_message_id. Use sg_event_id for event-level idempotency and your message_ref custom argument for business correlation. (twilio.com)

The handler is slow or times out

Keep the HTTP handler narrow: verify, validate, insert, enqueue, acknowledge. Do not make it wait for analytics aggregation, a CRM API, a large search-index update, or an email resend decision. If those systems are unavailable, durable event storage plus a queue prevents the webhook endpoint from becoming an accidental dependency chain.

Operational practices that keep the integration reliable

Treat the webhook as production infrastructure. Monitor it with the same care as your public API.

Track at least:

  • Incoming request count by environment.
  • Signature-verification failures.
  • Non-successful response count.
  • JSON parsing failures after successful signature verification.
  • Event count by event type.
  • Duplicate insert count.
  • Queue depth and worker failures.
  • Time from event receipt to derived-state update.
  • Number of events that cannot be correlated to an internal message reference.

Create alerts for a sudden fall to zero in incoming events, a sharp rise in invalid signatures, and sustained storage or queue errors. A healthy endpoint returning 204 while silently discarding unrecognized events is not healthy; log schema drift and preserve raw payloads so you can add support safely.

Separate development, staging, and production webhooks. Each should have its own URL, signing key, secrets, database target, and alert routing. Mixing environments is a frequent cause of confusing signature failures and, worse, test events modifying production customer records.

Finally, plan for vendor changes. Keep webhook verification behind a small adapter module, pin and review SDK upgrades, and run captured-payload integration tests in continuous integration. The Event Webhook’s role is stable—SendGrid posts signed email event data to your endpoint—but exact dashboard labels, SDK interfaces, and optional API fields are vendor-specific and can evolve.

FAQ

What is the difference between SendGrid Event Webhook and Inbound Parse?

SendGrid Event Webhook reports events for outbound email, such as delivery, bounces, opens, clicks, unsubscribes, and spam reports. Inbound Parse receives messages sent to a domain you configure and posts parsed email content to your application.

Should I enable signed Event Webhooks?

Yes. Signed Event Webhooks let your server verify that the request was signed for your configured webhook. SendGrid also supports OAuth 2.0 verification, and the two mechanisms can be used together. (twilio.com)

Why does SendGrid webhook signature verification fail in Express?

Most often, JSON middleware parsed or changed the body before verification. Configure the webhook route with raw-body middleware, verify using the original Buffer, and only then parse the JSON. (github.com)

Can I use opens and clicks as proof a person engaged with an email?

No. They are useful telemetry but can be affected by privacy protections, image loading behavior, and automated security scanners. Use them as contextual signals, not definitive proof of a person reading or intentionally clicking.

What should I use to prevent duplicate processing?

Persist the event with a unique constraint on sg_event_id whenever it is present, then make downstream work idempotent as well. Keep your own message reference in custom_args so you can map events to business records even when provider message fields are missing or delayed.