A webhook receiver is a security boundary, not just an endpoint
Anyone who learns your webhook URL can POST to it. That makes three properties non-negotiable on the receiving side — build the payload here, but design the handler around these:
- Authenticity — verify the provider’s signature so you only act on payloads they actually sent.
- Idempotency — the same event will arrive more than once; processing it twice must not double-charge, double-ship, or double-email.
- Freshness — reject payloads old enough to be a replay of a captured request.
Verifying signatures correctly
The mechanics are simple and easy to get subtly wrong:
- Use the raw body. (See the FAQ above — this is the single most common failure.)
- Compare in constant time. A naive
==on signature strings leaks timing information that can be exploited to forge a signature byte by byte. Use a timing-safe comparison:crypto.timingSafeEqualin Node,hmac.compare_digestin Python. - Check the timestamp. Stripe-style schemes sign
timestamp.payloadand include the timestamp in the header. Reject anything outside a tolerance window (five minutes is typical) so a captured request can’t be replayed tomorrow.
Designing for duplicates
Every major provider documents at-least-once delivery — duplicates are a feature of reliability, not a bug. The defence is an idempotency key. Each event carries a stable id (evt_… from Stripe, the X-GitHub-Delivery UUID from GitHub). Record processed ids in a table with a unique constraint; if an insert collides, you’ve seen this event before — acknowledge with 2xx and do nothing else. This single table turns “we got charged twice during a provider retry storm” into a non-event.
Test the payload here, then test the transport
This builder validates and formats the JSON body so you can confirm shape and field names against a provider’s schema before you wire anything up. To exercise the full round trip, copy the generated payload and POST it to your endpoint — the cURL to Code tool will turn a curl -X POST into a snippet in your language. One caveat: a payload you build here won’t carry a valid provider signature, so point your handler’s signature check at test mode (or temporarily log-and-skip) when replaying hand-built payloads, and reserve real signature verification for traffic that genuinely came from the provider.