Skip to content
newThe webhook.co MCP server is live — turn any webhook into an agent event. Read the docs
test locally

Test SendGrid webhooks locally

SendGrid's Event Webhook reports delivery and engagement activity in batches. Here's what that means for the handler you're writing, and how to see a batch land on your laptop.

The loop

Create an endpoint and it prints a permanent ingest URL. Everything sent there is captured whole — headers, body, the exact bytes — before anything else happens. Stream those events to a port on your machine, and replay any one of them as often as you like while you fix the handler. Forwarding runs from your own machine, so it isn't a billable delivery.

wbhksendgrid
$ wbhk endpoints create sendgrid-dev
https://wbhk.my/whep_…
$ wbhk listen <endpoint-id> --forward http://localhost:3000/webhooks
$ wbhk replay <event-id> --forward http://localhost:3000/webhooks

What's different about SendGrid

SendGrid's Event Webhook doesn't send one event per request. It batches: a POST fires within approximately 30 seconds, or as soon as the pending batch reaches 768 kilobytes, and the body is a JSON array of event objects. A handler written around a single top-level object breaks. Separately, duplicates are expected — the docs say the same event can appear more than once in the posted data, and name sg_event_id as the field to deduplicate on. Watching one real batch land locally settles both questions at once: how many events arrive together, and which of them you have already seen.

Where the URL goes

In the SendGrid app go to Settings > Mail Settings > Webhook Settings > Event Webhooks and click Create new webhook. Only two fields are required: the Post URL, which is where SendGrid sends the data, and Actions to be posted, which picks the event types. How many Event Webhooks an account gets depends on the SendGrid plan, and downgrading disables the newest ones automatically.

There is a test facility, and it's a button. Test Your Integration, on the Event Webhook settings screen, sends an HTTP POST to your configured Post URL containing a JSON array of example events. The same thing is exposed over the API as POST /v3/user/webhooks/event/test, which answers 204 No Content. The payload is made up of example events and will not include real data from your mail send.

What SendGrid sends

The event field holds a single bare lowercase token — no resource prefix, no dot, no colon. Multi-word names are inconsistent rather than systematic: group_unsubscribe takes an underscore, spamreport runs together without one. Pattern-matching your way to a name will fail here, so copy the strings as written.

  • processed
  • delivered
  • bounce
  • dropped
  • open
  • click

Worth knowing first

Duplicates are expected, not a bug

SendGrid's docs state outright that it's possible to see duplicate events in the data posted by the Event Webhook, and recommend deduplicating on sg_event_id. Size that column generously: the overview page calls it a string up to 100 characters, while the event reference says these URL-safe IDs can exceed 100 characters. The two pages disagree, so don't pin a tight limit to either.

A non-2xx starts a 24-hour clock

Anything other than a 2xx response makes SendGrid retry the POST at increasing intervals for up to 24 hours after the event occurred. That window is rolling, so each newly failing event gets its own 24-hour retry period. The interval schedule itself isn't published, so don't build timing assumptions on top of it.

Don't allowlist SendGrid's IPs

The docs ask you not to block the addresses posting to your server, because their IPs often change as more machines are added, and no fixed range is published. If you want to receive encrypted posts, the callback URL must support TLS 1.2 — that's written as a condition, not as a blanket HTTPS requirement.

The signature headers say Twilio

Verification is ECDSA over two headers named X-Twilio-Email-Event-Webhook-Signature and X-Twilio-Email-Event-Webhook-Timestamp. If you're grepping request logs for a header with SendGrid in the name, you'll come up empty. The docs describe the headers and the scheme but state no acceptable clock skew, so any replay window you enforce is a number you chose.

Signature checking is handled for you once the secret is registered — you can also check a captured signature by hand in the signature verifier.

SendGrid webhooks: common questions

Does SendGrid send one webhook request per event?

No. The Event Webhook batches events and posts them as a JSON array. A request fires within approximately 30 seconds, or sooner if the pending batch reaches 768 kilobytes, so one request can carry many events.

How do I fire a test SendGrid webhook?

The Event Webhook settings screen has a Test Your Integration button that posts a JSON array of example events to your Post URL. The same test is available over the API as POST /v3/user/webhooks/event/test, which returns 204 No Content on success.

Why does SendGrid send the same event twice?

Duplicates are documented behavior. SendGrid says it is possible to see duplicate events in the data posted by the Event Webhook, and recommends deduplicating with sg_event_id as the differentiator. Store that ID and treat a repeat as already handled.

What happens if my server is down when SendGrid posts an event?

SendGrid retries the request until it receives a 2xx response or the maximum time has elapsed. Retries run at increasing intervals for up to 24 hours after the event occurs, and that window is rolling, so each newly failing event gets its own 24 hours.

Are SendGrid Event Webhook payloads signed?

SendGrid documents ECDSA verification using a signature header and a timestamp header, both prefixed X-Twilio-Email-Event-Webhook. The docs give no acceptable timestamp tolerance, so if you reject old requests as replays, that threshold is one you pick rather than one SendGrid publishes.

Point a webhook at it. Watch it land.

Start free: a permanent URL, full inspection, one-command replay — and outbound delivery, because every feature is on every plan. Move up when you need more events.