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.

circle-exclamation

Signature Headers

Header
Description
Example

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

  1. Extract X-Webhook-Signature, X-Webhook-Timestamp, and X-Webhook-Request-Id from the request headers.

  2. Reject the request if the timestamp is older than 5 minutes — this protects against replay attacks.

  3. Read the raw request body exactly as received. Do not re-serialize or re-encode it.

  4. Build the canonical request string (see format below).

  5. Compute HMAC-SHA256 using your secret key with the whsec_ prefix stripped.

  6. Compare your result against X-Webhook-Signature using a constant-time comparison function.


Canonical Request Format

Construct the string by joining the following lines with \n. There is no trailing newline.

ExamplePOST 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

Scenario
Behavior

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 :port to the host string.

  • Non-constant-time comparison — Always use hash_equals() (PHP), hmac.compare_digest() (Python), or crypto.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?