Read the body as a raw string – do not parse JSON first. Parsing and re-serializing can change whitespace or key order, breaking the signature.
2
Read the headers
Extract svix-id, svix-timestamp, and svix-signature from the request headers.
3
Compute the expected signature
Build the signed content string: {svix-id}.{svix-timestamp}.{body}Compute HMAC-SHA256 of this string using your webhook signing secret (the part after whsec_, base64url-decoded).
4
Compare signatures
Base64-encode your HMAC result and compare it to the signature value (after the v1, prefix) using constant-time comparison.
Don’t parse the body before verifying. If your framework automatically parses JSON, use a raw body parser for the webhook route. JSON.parse(JSON.stringify(body)) may reorder keys or change whitespace, producing a different signature.
Use express.raw() in Express (not express.json()) for the webhook route
In Next.js App Router, use await request.text() to get the raw body
Always use constant-time comparison (crypto.timingSafeEqual in Node, hmac.compare_digest in Python) to prevent timing attacks
Validate the timestamp – reject events older than 5 minutes to prevent replay attacks