raw_body is the bytes of the request body as received, not a re-serialized JSON string.
Verification steps
- Parse
t=andv1=from theX-Webhook-Signatureheader. - Reject the request if
|now - t| > 300seconds (anti-replay window of ±5 minutes). - Recompute the HMAC with your stored signing secret over the string
"{t}.{raw_body}". - Compare the recomputed signature with
v1using a constant-time equality function.
400 Bad Request and do not process the payload.
Node.js
In Express, use
express.raw({ type: 'application/json' }) on the webhook route to get the raw body bytes before JSON parsing. Calling JSON.parse then JSON.stringify re-serializes the payload and breaks the signature.Python
Kotlin
Go
Common mistakes
- Re-serializing JSON. Sign the raw bytes as received, not a pretty-printed or re-normalized JSON string. Middleware in Express, Django, or Spring often re-parses the body; use a raw-body reader specifically on the webhook route.
- Plain string comparison. Use a constant-time comparison (
crypto.timingSafeEqual,hmac.compare_digest,MessageDigest.isEqual,hmac.Equal). Plain==leaks timing information that can be exploited. - Skipping the timestamp check. Without the ±5 min check, a captured valid request can be replayed indefinitely by an attacker.
- Trimming or decoding the header. Pass the
X-Webhook-Signatureheader value verbatim — do not URL-decode or trim whitespace before splitting on,.