SendGrid Webhook: Example Payload, Headers and Signature

SendGrid's Event Webhook posts a batch — a JSON array of email events — and signs it with ECDSA rather than an HMAC. Here is what lands on your endpoint.

No webhook URL yet. Creating one needs no account.

Request details
Requests sent to your webhook URL appear here: method, headers and the body exactly as it arrived.

The button posts the example below to your webhook URL from your browser. It is not sent by SendGrid, it carries your browser's User-Agent, and its signature is a placeholder that will not verify. To see a real delivery, follow the steps further down.

Last updated . How this page is checked.

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

HeaderExampleWhat it is
X-Twilio-Email-Event-Webhook-SignatureMEUCIQCtIHJeH93Y+qpYeWrySphQgpNGNr/U+UyUlBk…Base64 ECDSA signature. Present when signed event webhooks are enabled.
X-Twilio-Email-Event-Webhook-Timestamp1767225600Unix time of the request. It is prepended to the body before signing.
Content-Typeapplication/jsonThe body is a JSON array.
User-AgentSendGrid Event APIIdentifies 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

  1. Press Create webhook URL at the top of this page and copy the address.
  2. In SendGrid, open Settings → Mail Settings → Event Webhooks and create a new webhook.
  3. Paste the address as the Post URL and choose which events to post.
  4. Use Test Integration to send a sample batch, or send yourself an email through your account.
  5. 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 open event 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

Other webhooks signed with a private key

References

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.