SendGrid webhook: the complete Event Webhook guide
How the SendGrid webhook works - the events it sends, how to configure it, verifying the ECDSA signature, retry behavior, and why it fails.
Dvir Atias
Founder
The SendGrid Event Webhook is a callback that posts email activity to an HTTPS endpoint you control. Each time one of your messages is processed, delivered, opened, clicked, bounced, dropped, marked as spam, or unsubscribed, SendGrid batches those events into a single JSON POST and sends it to your URL. It is the standard way to pull delivery and engagement data out of SendGrid without polling an API, and configuring the SendGrid webhook correctly - then verifying its signature - is what this guide walks through.
There are two different SendGrid webhooks, and people constantly confuse them. The Event Webhook reports what happened to mail you sent. The Inbound Parse Webhook delivers mail your domain receives. This post is about the Event Webhook. For the receiving side, see the SendGrid Inbound Parse guide.
What events does the SendGrid Event Webhook send?
SendGrid splits events into two families: delivery events (what the receiving mail server did) and engagement events (what the recipient did). Engagement events only fire if you have open and click tracking switched on.
| Event | What it means | Typical action |
|---|---|---|
| processed | SendGrid accepted the message and is sending it | Log only; no action |
| dropped | SendGrid did not send it (suppressed or invalid address, spam content) | Investigate; clean your list |
| delivered | The receiving server accepted the message | Mark as delivered |
| deferred | The receiving server temporarily rejected it; SendGrid will retry | Monitor; no action |
| bounce | The receiving server permanently rejected it | Suppress the address |
| open | The recipient opened the message | Update engagement score |
| click | The recipient clicked a tracked link | Update engagement score |
| spamreport | The recipient marked it as spam | Suppress immediately |
| unsubscribe | The recipient unsubscribed via SendGrid tracking | Honor the opt-out |
Events arrive as a JSON array, and one POST can carry many events for many messages, so your handler has to loop. Each event includes an sg_event_id, an sg_message_id, the recipient email, a timestamp, and any custom arguments you attached when sending.
How do you set up the SendGrid Event Webhook?
- In the SendGrid dashboard, open Settings, then Mail Settings, then Event Webhook (the exact path shifts between console versions, as of July 2026).
- Set the HTTP Post URL to your public HTTPS endpoint. It must be reachable from the internet and return a 2xx status quickly.
- Select the events you want. Start narrow with delivered, bounce, dropped, and spamreport, then add open and click only if you track engagement.
- Enable Signature Verification. SendGrid generates an ECDSA key pair and shows you the public key. Copy it into your app config.
- Use the "Test Your Integration" button, confirm you got a 2xx, then toggle the webhook on.
You can also drive all of these settings over the SendGrid API instead of the dashboard, which is the right move when you provision webhooks programmatically.
How do you verify a SendGrid webhook signature?
Signature verification proves a request genuinely came from SendGrid and was not tampered with in transit. When you enable it, SendGrid signs every request with the private half of an ECDSA key pair and adds two headers: X-Twilio-Email-Event-Webhook-Signature and X-Twilio-Email-Event-Webhook-Timestamp. You verify with the public key SendGrid gave you.
The one detail that trips everyone up: the signature is computed over the timestamp concatenated with the raw request body bytes. If your framework parses the body into JSON before you verify, the re-serialized bytes will not match and every check fails. Capture the raw body first, then verify, then parse.
const { EventWebhook, EventWebhookHeader } = require("@sendgrid/eventwebhook");
const express = require("express");
const app = express();
const publicKey = process.env.SENDGRID_WEBHOOK_PUBLIC_KEY;
// express.raw keeps the body as raw bytes - required for verification
app.post("/webhooks/sendgrid", express.raw({ type: "application/json" }), (req, res) => {
const ew = new EventWebhook();
const key = ew.convertPublicKeyToECDSA(publicKey);
const signature = req.get(EventWebhookHeader.SIGNATURE());
const timestamp = req.get(EventWebhookHeader.TIMESTAMP());
if (!ew.verifySignature(key, req.body, signature, timestamp)) {
return res.status(403).send("invalid signature");
}
const events = JSON.parse(req.body.toString());
for (const event of events) {
// route on event.event: "delivered", "open", "bounce", "spamreport", ...
}
res.status(200).send("ok");
});The @sendgrid/eventwebhook helper ships with the official Node library, with equivalents for Go, Python, C#, and PHP, so you rarely implement the ECDSA math yourself.
How does SendGrid retry failed webhooks?
If your endpoint does not return a 2xx, SendGrid treats the delivery as failed and retries it with a backoff (roughly a day of progressively less frequent attempts, as of July 2026). Three consequences shape how you write the handler:
- Return 200 fast. Do the heavy work - database writes, downstream calls - after you acknowledge, or push it onto a queue. A slow handler that times out reads as a failure and gets retried.
- Expect duplicates. Retries plus at-least-once delivery mean the same event can arrive more than once. Deduplicate on
sg_event_id. - Expect events out of order. An
opencan land before thedeliveredthat logically precedes it. Order bytimestamp, not by arrival.
If your SendGrid webhook is not working, the usual culprits are an endpoint that is not publicly reachable, a non-2xx response (redirects count as failures), TLS certificate problems, a handler slow enough to time out, or signature verification rejecting valid requests because the body was parsed before it was verified.
Event Webhook vs Inbound Parse
These are the two halves of SendGrid's webhook surface, solving opposite problems. The Event Webhook is outbound telemetry: what happened to messages you already sent. Inbound Parse is an inbound mailbox: you point an MX record at SendGrid, and mail addressed to your domain is parsed into headers, text, HTML, and attachments and POSTed to your endpoint. To receive and act on replies rather than track sends, Inbound Parse is the tool, and it has its own dedicated guide.
A simpler event model with AgenticEmail
AgenticEmail is email infrastructure for AI agents, and its event system covers both directions - sent and received - through one webhook API. You register an endpoint once:
curl -X POST https://api.agenticemail.dev/v1/webhooks \
-H "Authorization: Bearer $AGENTICEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/email",
"event_types": ["message.received", "message.delivered", "message.bounced"],
"inbox_ids": ["ibx_123"]
}'The event types are message.received, message.sent, message.delivered, message.bounced, message.complained, message.opened, message.clicked, domain.verified, and reply.suggested. Scope a webhook to specific mailboxes with inbox_ids, or leave it off to receive events account-wide. Two things differ from SendGrid's model:
- Signing follows the Standard Webhooks spec. You verify
webhook-id,webhook-timestamp, andwebhook-signatureagainst yourwhsec_secret using any off-the-shelf Standard Webhooks library, rather than an ECDSA scheme specific to one vendor. The email webhook guide walks through that verification pattern. - There is a WebSocket alternative. Connect to
wss://api.agenticemail.dev/v1/events?token=am_...and receive the same events on a long-lived connection - no public endpoint to host, no retry semantics to reason about, which fits an agent that is already running.
Every delivery is logged - GET /v1/webhooks/<id>/deliveries shows what was sent and lets you redeliver a failed event by hand. For sending the mail itself, see SendGrid API send email, or read the full reference in the AgenticEmail docs.
Frequently asked questions
What is the difference between the Event Webhook and Inbound Parse?
The Event Webhook reports events about mail you sent - delivered, opened, bounced, and so on. Inbound Parse does the reverse: it receives mail addressed to your domain, parses it into fields and attachments, and POSTs it to your endpoint. One is outbound telemetry, the other is an inbound mailbox. Full detail lives in the Inbound Parse guide.
How does SendGrid retry failed webhooks?
When your endpoint returns anything other than a 2xx, SendGrid retries the delivery with a backoff over roughly a day (as of July 2026), lowering the frequency with each attempt. Because of that, design for at-least-once delivery: acknowledge with a 200 quickly and deduplicate on sg_event_id.
How do I test a SendGrid webhook locally?
Expose your local server to the internet with a tunnel such as ngrok or Cloudflare Tunnel, point the webhook's HTTP Post URL at the tunnel URL, and use the "Test Your Integration" button in the dashboard. Log the raw body and both signature headers so you can confirm the payload shape and your verification code before writing real handling logic.
Why is my SendGrid webhook not working?
The most common causes are an endpoint that is not publicly reachable, a non-2xx response (redirects count as failures), a handler that is too slow and times out, TLS certificate issues, or signature verification failing because the body was parsed into JSON before it was verified against the raw bytes.
Do I have to verify the signature?
It is optional but strongly recommended. Without verification, anyone who discovers your webhook URL can POST fake events to it. With it enabled, you reject any request whose signature does not validate against your SendGrid public key.