Every webhook provider signs its requests differently, and the differences are exactly the kind that cost you an afternoon. Some sign the raw body, one signs the URL, most fold in a timestamp for replay protection and one does not. This is a side-by-side comparison of how Stripe, Paddle, HubSpot, Slack, and Twilio sign webhooks, what each one gets wrong most often, and the one trap they nearly all share.
The comparison
| Provider | Header | Algorithm | What it signs | Encoding | Replay protection |
|---|---|---|---|---|---|
| Stripe | Stripe-Signature | HMAC-SHA256 | timestamp . payload | hex | Yes (5-min tolerance) |
| Paddle (Billing) | Paddle-Signature | HMAC-SHA256 | ts : rawBody | hex | Yes (timestamp signed) |
| HubSpot (v3) | X-HubSpot-Signature-v3 | HMAC-SHA256 | method + uri + body + timestamp | Base64 | Yes (5-min) |
| Slack | X-Slack-Signature | HMAC-SHA256 | v0 : timestamp : body | hex (with v0= prefix) | Yes (5-min) |
| Twilio | X-Twilio-Signature | HMAC-SHA1 | full URL + sorted POST params | Base64 | No (URL-based) |
Two things jump out. Twilio is the odd one: it uses SHA1 instead of SHA256, and it signs the request URL rather than the body, which changes the whole failure mode. And four of the five fold a timestamp into the signed string, which is what lets you reject replayed requests. Twilio does not, so its signature is tied to the URL instead.
The trap almost all of them share: the raw body
For Stripe, Paddle, HubSpot, and Slack, the signature is computed over the exact bytes of the request body. HMAC signs bytes, not objects. If your web framework parses the JSON and you rebuild a string from the parsed object to verify against, those bytes are not identical to what the provider sent. Key order shifts, whitespace disappears, numbers get reformatted, and your signature never matches. Every one of these four breaks the same way if you verify against a re-serialized body instead of the raw one.
The fix is always the same: capture the raw, unparsed body, verify the signature against it, and only parse it after the check passes. The mechanics of getting the raw body differ per framework (Express needs express.raw(), Next.js route handlers use await req.text(), Cloudflare Workers use await request.text()), but the rule holds across all four providers.
What each one gets wrong most often
-
Stripe signs
{timestamp}.{payload}with your endpoint's signing secret (thewhsec_value, one per endpoint). The common miss is using the wrong endpoint's secret, or verifying against a re-serialized body. Deep dive on Stripe webhook failures. -
Paddle Billing signs
ts:rawBody, not the body alone. Miss thets:prefix and every signature is wrong even with correct HMAC code. Also, Paddle Classic uses a completely different RSA scheme, so first confirm which product you are on. Paddle signature guide. - HubSpot has three signature versions with similar header names. Use v3, and note it signs the full URL (normalized) plus the body plus the timestamp, and it is Base64 not hex. HubSpot v1/v2/v3 guide.
-
Slack signs
v0:{timestamp}:{body}with your signing secret and prefixes the result withv0=. A failed check can eventually get Slack to disable event delivery to your app, so it is worth getting right. Slack v0 guide. -
Twilio signs the full request URL, so the classic failure is a proxy or load balancer rewriting the scheme or host, making your reconstructed URL differ from the one Twilio signed. Recover the original with
X-Forwarded-Protoand the public host. Twilio validation guide.
Signatures are the easy half
Every scheme above answers the same question: is this request really from the provider? None of them answer the harder one: what about the webhooks you never receive? A verified signature does nothing for the event that arrived while your server was mid-deploy, or the burst that timed out during a traffic spike. Stripe and Shopify retry for days, Slack retries a few times then can disable your endpoint, and Twilio barely retries at all. The behavior you can rely on is uneven, so the events that matter, the ones tied to money and to state, are the ones you can least afford to drop.
That is the layer EventDock adds. You point any of these providers at EventDock instead of your app. EventDock verifies the signature, stores the event, acknowledges the provider immediately so no timeout window matters, then delivers to your app with its own retries and a dead-letter queue you can replay by hand. One reliability layer, the same behavior across every provider, whether the provider itself retries generously or not at all.
You can wire up any provider on the free tier and watch every event get captured and delivered. Start with EventDock free.
Top comments (0)