Skip to content
TryWebhook
New URL

Email · Free · No signup

Test and debug SendGrid Event Webhooks

Point the SendGrid Event Webhook at a throwaway URL and read what actually arrives: a JSON array of many events in one POST, signed with an elliptic-curve key rather than a shared secret.

No signup · No email · Ready in about a second

At a glance

SendGrid webhooks in brief

Category
Email
Where to configure
SendGrid → Settings → Mail Settings → Event Webhooks → Create new webhook
Signature header
X-Twilio-Email-Event-Webhook-Signature
Secret
Verification key (a base64 public key, safe to commit)
Retries
Retried with backoff for up to about 24 hours. Deliveries and their responses are visible under the webhook's activity in the dashboard.
Response deadline
Around 30 seconds — generous, because a single POST can carry hundreds of events.
Treated as success
Any 2xx. Anything else schedules a retry of the whole batch, including the events you already processed.

Cross-checked against SendGrid's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What SendGrid actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}753 B

A JSON array, not an object. One POST can carry hundreds of events.

1 headers
user-agent
SendGrid-Event-Webhook/1.0
Body · application/json · 1 line
[{"email":"buyer@example.com","timestamp":1750000000,"smtp-id":"<fake.0000000000@example.com>","event":"delivered","category":["receipt"],"sg_event_id":"ZmFrZS1kZWxpdmVyZWQtZXZlbnQtaWQ","sg_message_id":"fake-message-id.filterdrecv-0000","response":"250 2.0.0 OK","ip":"198.51.100.10"},{"email":"buyer@example.com","timestamp":1750000042,"event":"open","sg_event_id":"ZmFrZS1vcGVuLWV2ZW50LWlk","sg_message_id":"fake-message-id.filterdrecv-0000","useragent":"Mozilla/5.0","ip":"198.51.100.10"},{"email":"nobody@example.invalid","timestamp":1750000060,"event":"bounce","type":"blocked","status":"5.1.1","reason":"550 5.1.1 The email account does not exist.","sg_event_id":"ZmFrZS1ib3VuY2UtZXZlbnQtaWQ","sg_message_id":"fake-message-id-2.filterdrecv-0000"}]

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a SendGrid signature

Signed payload

An ECDSA signature over the timestamp header concatenated directly with the raw body, verified with a public key. Not an HMAC — there is no shared secret, and the value in the dashboard is a public key you cannot sign with.

TryWebhook cannot verify this one. Every built-in verifier here is an HMAC, and SendGrid's signature is asymmetric ECDSA over the NIST P-256 curve — verifying it needs public-key cryptography rather than a keyed hash. Use SendGrid's own `EventWebhook` helper, or your platform's `crypto.verify` with the base64 key from the dashboard. Everything else on this page still works: capture the delivery, read the array, and check the headers arrived.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
deliveredThe receiving mail server accepted the message. The closest thing to success email offers; it says nothing about the inbox versus the spam folder.
bounceDelivery failed permanently or was blocked. `type` distinguishes `bounce` from `blocked`, and only the first means the address is bad.
openA tracking pixel loaded. Inflated by image proxies and Apple Mail Privacy Protection, and absent for anyone with images off.
clickA tracked link was followed. More reliable than `open`, and it carries the `url` that was clicked.
spamreportThe recipient pressed the spam button. Suppress the address immediately; continuing to send is how a sending domain gets blocklisted.

Pointing the Event Webhook at a test URL

Create a URL above, then go to SendGrid → Settings → Mail Settings → Event Webhooks. Create a webhook, paste the URL, tick the event types you want, and enable Signed Event Webhook Requests — that is what produces the signature headers and reveals the verification key.

SendGrid gives you a Test Your Integration button, which sends one synthetic event of each selected type. That is the fastest way to see the shape. For real events, send yourself a message through the API and then open it and click a link.

The body is an array, and that is the whole story

[
  { "email": "buyer@example.com", "event": "delivered", "sg_event_id": "...", "smtp-id": "<...>" },
  { "email": "buyer@example.com", "event": "open", "sg_event_id": "..." },
  { "email": "bounce@example.com", "event": "bounce", "type": "blocked", "reason": "..." }
]

Nearly every SendGrid integration bug traces back to this. The handler is written the way handlers for every other provider are written — read body.event, switch on it — and because JavaScript returns undefined for a missing property on an array rather than throwing, it fails without a single error in the logs.

const events = await request.json();
for (const event of events) {
  await handle(event); // one at a time, and never let one throw out of the loop
}
return new Response(null, { status: 200 });

The for loop and the unconditional 200 are both load-bearing. SendGrid retries the entire batch on a non-2xx, so one bad row otherwise causes the other 199 events to be delivered again — and again.

Batching is not optional

You cannot ask for one event per request. High-volume accounts routinely see hundreds of events in a single POST, which is why the timeout is 30 seconds rather than 5. Design the handler to enqueue rather than to process inline, and the batch size stops mattering.

Verifying the signature

Two headers arrive together:

X-Twilio-Email-Event-Webhook-Signature: MEUCIQ...
X-Twilio-Email-Event-Webhook-Timestamp: 1750000000

The signed data is the timestamp followed immediately by the raw body, with no separator. The signature is DER-encoded ECDSA over P-256, base64-encoded. The dashboard’s “verification key” is a base64 public key.

const signed = timestampHeader + rawBody;
const ok = crypto.verify('sha256', Buffer.from(signed), publicKeyPem, Buffer.from(sigHeader, 'base64'));

This is genuinely different from every HMAC scheme on this site, and it is better: the key on your server cannot forge a signature, so a compromised web server does not let an attacker mint fake events. It also means there is nothing to paste into a signature-verification tool that only speaks HMAC, including this one. SendGrid’s official SDKs each ship an EventWebhook helper that does the four lines above with the DER-to-PEM conversion handled.

Check the timestamp as well — the signature does not expire on its own.

Reading the events

sg_message_id identifies the message, sg_event_id identifies the event. Deduplicate on the second one; grouping by the first is how you assemble a timeline for one send.

bounce needs care. A type of bounce means the address is bad and should be suppressed permanently. A type of blocked means the receiving server refused this message for reputational or content reasons — the address is fine and suppressing it loses a real recipient. reason carries the SMTP response, which is where the actual explanation lives.

Failure modes

Where SendGrid integrations usually break

The body is an array

SendGrid POSTs `[{...},{...},{...}]`, not a single object. Code written as `body.event` reads `undefined` and silently does nothing. Iterate, always, even when the array has one element.

One batch mixes recipients and event types

A single POST can contain a delivery for one recipient, a bounce for another, and three opens, with no ordering guarantee. There is no per-batch subject; the only grouping key is `sg_message_id` on each event.

A single failure retries the whole batch

Return non-2xx because event 47 broke and all 200 events come back. Process each event independently, catch per-event errors, and still return 200 — otherwise one malformed row makes you reprocess everything, repeatedly.

smtp-id is hyphenated

The key is `"smtp-id"`, so it needs bracket access in most languages and cannot be destructured by name. Easy to miss when the surrounding keys are all underscored.

The verification key is public

It is a public key, not a secret. It cannot be used to forge signatures and it is safe to commit — but it does mean you cannot verify with an HMAC library, which is where most integrations get stuck.

Answers

SendGrid webhook questions

Why is my SendGrid webhook handler receiving nothing?+

Most likely it is receiving an array and looking for an object. The body is always a JSON array of events, even for a single event. The capture above shows the raw bytes so you can see the leading `[`.

Can TryWebhook verify the SendGrid signature?+

No, and the reason is worth knowing: SendGrid signs with ECDSA on the P-256 curve, so verification needs the public key and an elliptic-curve verify operation, not a keyed hash. Every verifier built in here is an HMAC. Use SendGrid's helper library instead.

How do I verify it myself?+

Build the signed string as the `X-Twilio-Email-Event-Webhook-Timestamp` header followed immediately by the raw body, base64-decode the signature header, and verify both against the base64 DER public key from the dashboard. SendGrid's `EventWebhook` class in each official SDK wraps all of it.

How do I stop processing the same event twice?+

Use `sg_event_id`. It is unique per event and stable across retries, which `sg_message_id` is not — one message produces many events that share it. Store the ids you have handled and skip repeats.

Why do open counts look impossibly high?+

Because opens are measured with a tracking pixel and mail privacy proxies fetch every image before the recipient sees anything. Apple Mail Privacy Protection alone registers an open for messages nobody read. Treat `click` as the real engagement signal.

Other providers

Testing something else?

One click

Point SendGrid at a URL and watch the payload land.

SendGrid → Settings → Mail Settings → Event Webhooks → Create new webhook — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second