The Critical Role of Webhook Verification
Webhooks power real-time asynchronous architectures: notifying your backend when a payment succeeds on Stripe, an order is placed on Shopify, or code is pushed to GitHub. However, because webhook listener endpoints are publicly accessible URLs, any bad actor could send fraudulent HTTP POST requests to trigger orders, credit accounts, or execute pipelines unless verified.
How Webhook HMAC Signatures Work
When a provider sends a webhook, it takes your private shared webhook secret and computes a cryptographic hash over the request body using HMAC-SHA256:
- Stripe: Combines the current UNIX epoch timestamp and payload:
t=timestamp,v1=HMAC(secret, timestamp + "." + payload). - GitHub: Computes the SHA-256 hex digest of the payload and sends it in the
X-Hub-Signature-256header. - Shopify: Encodes the HMAC-SHA256 digest in Base64 and sends it in
X-Shopify-Hmac-Sha256. - Slack: Prepends a version prefix and timestamp:
v0=HMAC(secret, "v0:" + timestamp + ":" + payload).
Best Practices for Implementing Webhook Handlers
- Always Preserve the Raw Body: Never parse JSON before calculating the signature. Even a single added space or newline will invalidate the cryptographic hash.
- Enforce Replay Tolerance: Reject any webhook where the embedded timestamp deviates by more than 300 seconds (5 minutes) from your server's current clock.
- Use Timing-Safe Comparison: Never use
===or==when comparing signatures. Always use constant-time functions likecrypto.timingSafeEqual().
Frequently Asked Questions
Why do webhooks use HMAC cryptographic signatures?
HMAC (Hash-based Message Authentication Code) ensures that an incoming webhook request genuinely originated from the service provider (e.g. Stripe, GitHub, Shopify) and that the payload has not been intercepted, forged, or altered in transit by an attacker.
Why is a timestamp included in Stripe and Slack webhook signatures?
Timestamps protect your application against replay attacks. Even if an attacker intercepts a valid webhook payload and signature, your server will reject the request if the timestamp is older than your configured tolerance threshold (typically 5 minutes / 300 seconds).
Why must I verify webhook signatures using timing-safe comparisons?
Standard string equality operators ('===') exit immediately upon encountering the first mismatched character, creating subtle microsecond execution time differences (timing attacks) that allow sophisticated attackers to guess secret signatures byte-by-byte. Timing-safe algorithms (like crypto.timingSafeEqual in Node.js or hmac.compare_digest in Python) compare the entire buffer in constant time.
Why does my webhook signature fail when testing locally with express.json()?
Common culprit! Webhook signatures are computed using the exact raw byte stream of the request body. If body-parser or express.json() parses and reformats the JSON payload (reordering keys or changing whitespace), the resulting HMAC hash will completely change. Always capture req.rawBody or use express.raw({ type: 'application/json' }) before JSON parsing.
Are my webhook secrets or payloads uploaded to your server?
No. 100% of cryptographic calculations are executed locally inside your browser's Web Cryptography API (window.crypto.subtle). Zero data is transmitted across the network.
Which hash algorithms are supported by this tool?
The tool natively supports HMAC-SHA256 (the industry standard used by Stripe, GitHub, Shopify, and Slack), as well as HMAC-SHA512 and HMAC-SHA1 with Hex or Base64 output encoding.