Verifying the Signature
Verifying the Signature
Each webhook delivery includes a set of signature headers. Your endpoint should use these to authenticate every request before processing it.
If a request fails signature verification, reject it immediately. This prevents untrusted sources from injecting data into your webhook endpoint.
Signature Headers
X-Webhook-Signature
HMAC-SHA256 hex digest to verify against
2603ad057b…
X-Webhook-Signature-Algorithm
Algorithm used to sign the request
hmac-sha256
X-Webhook-Timestamp
Unix timestamp of when the request was sent (seconds)
1709467498
X-Webhook-Request-Id
Unique UUID for this delivery
8aaaabcd-0f85-…
X-Webhook-Signature-Version
Key rotation version
1
Verification Steps
Extract
X-Webhook-Signature,X-Webhook-Timestamp, andX-Webhook-Request-Idfrom the request headers.Reject the request if the timestamp is older than 5 minutes — this protects against replay attacks.
Read the raw request body exactly as received. Do not re-serialize or re-encode it.
Build the canonical request string (see format below).
Compute HMAC-SHA256 using your secret key with the
whsec_prefix stripped.Compare your result against
X-Webhook-Signatureusing a constant-time comparison function.
Canonical Request Format
Construct the string by joining the following lines with \n. There is no trailing newline.
Example — POST to https://example.com/webhooks:
Key Handling
Your secret key has the format whsec_<64 hex chars>. Strip the whsec_ prefix before passing it to your HMAC function. The remaining hex string is used as-is (ASCII bytes) — do not hex-decode it to binary.
Edge Cases
Port in URL (host:8443)
Port is not included in the host component
No path (https://example.com)
Defaults to /
Trailing slash (/webhooks/)
Preserved as-is
URL-encoded characters (/abc%20def)
Used as-is (encoded form)
Query strings (?foo=bar)
Not included
Empty body
SHA-256 of empty string: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Code Examples
PHP
Python
Node.js
Common Pitfalls
Re-serializing the body — JSON serialization is not deterministic across languages. Always use the raw body bytes exactly as received over the wire.
Leaving the
whsec_prefix in — This prefix is a type indicator only. Always strip it before computing the signature.Binary-decoding the key — The hex string must be used as ASCII bytes (64 bytes). Do not hex-decode it to binary (32 bytes).
Including the port in the host — The port is excluded from the canonical request. Do not append
:portto the host string.Non-constant-time comparison — Always use
hash_equals()(PHP),hmac.compare_digest()(Python), orcrypto.timingSafeEqual()(Node.js). Standard equality checks are vulnerable to timing attacks.Ignoring the timestamp — Always validate that the timestamp falls within your tolerance window (5 minutes recommended) to prevent replay attacks.
Last updated
Was this helpful?

