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.
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.