- Signed content:
<webhook-id>.<webhook-timestamp>.<raw body>. - Key: the secret without its
whsec_prefix, base64-decoded. - Signature: base64 HMAC-SHA256, sent as
v1,<signature>. Several space-separated signatures can be present; accept the delivery if one matches. - Reject timestamps more than 5 minutes from your clock to stop replays. The libraries below do this for you.
- Verify the raw body bytes. Parsing and re-serializing the JSON changes the bytes and breaks the signature.
svix-* headers carry the same values). To check a verifier before your first delivery, use the test vector.
Node.js with standardwebhooks
Node.js
Python with standardwebhooks
Python
By hand: Node.js
No dependencies;headers are the request headers with lower-case names and body the raw body as a Buffer. now (Unix seconds) is only for checking the test vector.
verify.mjs
By hand: Python
Standard library only;body is the raw request body as bytes. Every failure raises ValueError. now (Unix seconds) is only for checking the test vector.
verify.py
Test vector
Check a verifier offline, before any delivery arrives, with this fixed delivery. Its signature was made by the code that signs real deliveries.Body
1767225600. The hand-written verifiers above take the clock as their last argument:
Node.js, by hand
Python, by hand
sign and compare it with the expected value instead:
Node.js, standardwebhooks
Python, standardwebhooks
When verification fails
- The body was parsed first. Frameworks that parse JSON before your handler change the bytes. Read the raw body (
express.raw,request.get_data()). - The wrong secret. Each webhook has its own. After a rotation, use the new secret; the old one also verifies for 24 hours.
- The prefix. Strip
whsec_and base64-decode the rest to get the key; do not use the string itself as the key. - Clock skew. A timestamp more than 5 minutes off is rejected. Keep your server’s clock synchronized.