How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a SendGrid webhook?
The SendGrid Event Webhook is an HTTP POST that reports what happened to email you sent: processed, delivered, deferred, bounce, dropped, open, click, spamreport, unsubscribe. It is how an application learns that an address hard-bounced without scraping a dashboard.
The body is a JSON array. SendGrid groups events and posts them together, so one request may describe many messages, and a handler that reads the body as a single object fails on the first real delivery.
What a SendGrid webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-Twilio-Email-Event-Webhook-Signature: MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBkU6n7RAwIgJTz2C+8a8xonZGi6BpSzoQsbVRamr2nlxFDWYNH3j/0=
X-Twilio-Email-Event-Webhook-Timestamp: 1767225600
Content-Type: application/json
User-Agent: SendGrid Event API
[
{
"email": "jon@example.com",
"timestamp": 1767225600,
"smtp-id": "<14c5d75ce93.dfd.64b469@ismtpd-555>",
"event": "delivered",
"category": [
"receipts"
],
"sg_event_id": "ZGVsaXZlcmVkLTAtMTIzNDU2Nzg",
"sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
"response": "250 OK",
"order_id": "6735"
},
{
"email": "jon@example.com",
"timestamp": 1767225660,
"event": "open",
"category": [
"receipts"
],
"sg_event_id": "b3Blbi0wLTEyMzQ1Njc4OQ",
"sg_message_id": "14c5d75ce93.dfd.64b469.filter0001.16648.5515E0B88.0",
"useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"ip": "203.0.113.10",
"order_id": "6735"
}
]
Custom arguments you attached when sending — order_id here — are merged into each event at the top level, which is how you tie an event back to your own records. The set of other fields depends on the event type: a bounce has reason and status, a click has url.
Headers SendGrid sends
| Header | Example | What it is |
|---|---|---|
X-Twilio-Email-Event-Webhook-Signature | MEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBk… | Base64 ECDSA signature. Present when signed event webhooks are enabled. |
X-Twilio-Email-Event-Webhook-Timestamp | 1767225600 | Unix time of the request. It is prepended to the body before signing. |
Content-Type | application/json | The body is a JSON array. |
User-Agent | SendGrid Event API | Identifies the sender. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How SendGrid signs the Event Webhook
Signing is opt-in. When you enable it, SendGrid generates a key pair, keeps the private key and shows you the public key. Each request is then signed with ECDSA over the SHA-256 of the timestamp header concatenated with the raw body, and the base64 signature is sent in X-Twilio-Email-Event-Webhook-Signature.
Because it is a public-key scheme there is no secret on your server to leak — but the usual rule holds harder than ever: the signed bytes are the raw payload, including its exact whitespace and line breaks, so a framework that parses the JSON first makes verification impossible.
const crypto = require('crypto');
// rawBody: Buffer of the body exactly as received
// publicKey: the base64 key shown in the Event Webhook settings
function verifySendGrid(rawBody, signature, timestamp, publicKey) {
const key = crypto.createPublicKey({
key: Buffer.from(publicKey, 'base64'), format: 'der', type: 'spki',
});
const signed = Buffer.concat([Buffer.from(String(timestamp)), rawBody]);
return crypto.verify('sha256', signed, key, Buffer.from(signature, 'base64'));
}
Batching, retries and duplicates
Respond with a 2xx promptly. If the endpoint returns anything else or times out, SendGrid retries the batch for up to 24 hours, per its documentation at the time of writing, after which those events are gone — the Event Webhook is a stream, not a store.
Duplicates happen, both from retries and because the same event can occasionally be posted twice. sg_event_id is unique per event and is the right de-duplication key. Events are also not ordered: an open can be recorded before the delivered that logically precedes it.
Catch a real SendGrid event batch
- Press Create webhook URL at the top of this page and copy the address.
- In SendGrid, open Settings → Mail Settings → Event Webhooks and create a new webhook.
- Paste the address as the Post URL and choose which events to post.
- Use Test Integration to send a sample batch, or send yourself an email through your account.
- Read the array of events in the inbox above.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- It is an array. Loop over it. A batch can hold events for many recipients and many messages.
- Opens are unreliable. Open tracking depends on an image loading. Mail privacy features pre-fetch images and some clients block them, so an
openevent is a weak signal in both directions. - This is not Inbound Parse. Inbound Parse is a different SendGrid webhook that posts received email as multipart form data. Different settings, different format.
api-tester.com is independent and is not affiliated with SendGrid. Providers change header names, limits and retry schedules; treat the official SendGrid documentation as the source of truth.
Related examples and tools
WooCommerce Webhook Example
What a WooCommerce store posts when an order, product or customer changes — the X-WC-Webhook-* headers, the REST-shaped JSON, and the rule that switches a failing webhook off.
Mailchimp Webhook Example
Mailchimp audience webhooks are form posts with bracketed field names, and they carry no signature at all.
HubSpot Webhook Example
HubSpot delivers CRM events in batches: one request, a JSON array, each element a small notification that names a record without containing it.
Square Webhook Example
Square signs the notification URL together with the body, so a request that is perfectly genuine fails verification when your server disagrees about its own address.
Bitbucket Webhook Example
Bitbucket Cloud webhooks look like GitHub's at first glance and differ in the details that break copied code: the event name header, the signature header's name, and the shape of a push.
Twilio Webhook Example
The other half of the same company: form-encoded SMS webhooks signed with HMAC-SHA1.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks signed with a private key
References
- Official SendGrid documentation — The provider's own guide to the requests described on this page.
- SendGrid Docs: Event Webhook security features — Turning on signed events and verifying them.
- SendGrid Docs: getting started with the Event Webhook — Setting the endpoint up and choosing events.
These links leave api-tester.com, which is independent of the companies and projects they point to.
SendGrid webhook FAQ
What does a SendGrid Event Webhook payload look like?
A JSON array of event objects. Each has email, timestamp, event, sg_event_id and sg_message_id, plus fields specific to the event type and any custom arguments you attached to the message.
How do I verify a SendGrid webhook signature?
Concatenate the X-Twilio-Email-Event-Webhook-Timestamp value and the raw request body, then verify the base64 ECDSA signature from X-Twilio-Email-Event-Webhook-Signature against it using the public key from your Event Webhook settings.
Why does SendGrid send multiple events in one request?
Events are batched for efficiency. A single POST can contain many events for different messages and recipients, so handlers must iterate over the array.
Does SendGrid retry failed Event Webhook posts?
Yes, for up to 24 hours according to its documentation at the time of writing. After that the events are discarded.
How do I de-duplicate SendGrid events?
Use sg_event_id, which is unique for each event, and treat an id you have already processed as a no-op.