How to Verify a Webhook Signature
A webhook signature is an HMAC of the request body computed with a secret only you and the sender know. To verify one, compute the HMAC over the exact raw body using the algorithm the provider specifies, then compare it with the value in the header using a timing-safe comparison. Sign the raw bytes — re-serialising the JSON first is what breaks most implementations.
HMAC Generator
Sign a message with a secret key using HMAC.
A webhook arrives as an ordinary HTTP request, so anyone who learns the URL can send one. The signature is what proves the sender holds the shared secret. Without verification, a webhook endpoint is an unauthenticated write API pointed at your database.
Almost every failed verification comes down to signing something slightly different from what the sender signed. The algorithm is rarely the problem; the exact bytes usually are.
Step by step
-
Capture the raw body, before any parsing
This is the step that catches people. Most frameworks parse JSON into an object before your handler runs, and re-serialising it changes key order, whitespace and number formatting. The signature covers the original bytes, so you must capture them before parsing.
-
Build the signed string exactly as documented
Some providers sign the body alone. Others sign a timestamp joined to the body with a separator, or prefix a version marker. Read the documentation rather than assuming, because a missing prefix produces a valid HMAC of the wrong thing.
-
Compute the HMAC with the right algorithm and encoding
HMAC-SHA256 is the common choice. Note whether the provider sends hex or Base64 — comparing a hex digest against a Base64 one always fails, and it is an easy mistake because both look like noise.
-
Compare in constant time
Use a timing-safe comparison such as hash_equals or crypto.timingSafeEqual, never ==. An ordinary comparison returns as soon as it finds a differing byte, and that timing difference can leak the expected signature one byte at a time.
-
Reject stale timestamps
A valid signature stays valid forever, so an intercepted request can be replayed. If the provider includes a timestamp in the signed string, reject anything older than a few minutes.
-
Debug a mismatch by reproducing it
Take the exact raw body and your secret, compute the HMAC by hand, and compare it with the header. If they differ, the signed string is wrong — check for a re-serialised body, a missing timestamp prefix, or a trailing newline.
Example
A common signed-string format: version, timestamp and raw body joined by full stops.
Signed string
v1.1754521200.{"event":"order.paid","id":42}
Secret: whsec_abc123
Algorithm: HMAC-SHA256, hex
Header sent
X-Signature: v1,t=1754521200,
s=4f8a2c... (64 hex characters)
Verify by recomputing s over the
same string and comparing safely.
Frequently asked questions
Why does my signature never match?
Why use HMAC rather than hashing the secret and body together?
Does the comparison really need to be timing-safe?
What stops someone replaying a captured webhook?
Should I verify before or after parsing the body?
Tools used in this guide
All tools →- HMAC Generator Sign a message with a secret key using HMAC. In your browser
- SHA-256 Hash Generator Generate a SHA-256 digest — the modern default. In your browser
- JSON Formatter Beautify, validate and sort JSON with precise error messages. In your browser
- Unix Timestamp Converter Convert between Unix timestamps and human dates. In your browser
- Base64 Encoder and Decoder Encode text to Base64 or decode it back, including URL-safe. In your browser
- HTTP Header Parser Turn a raw header block into a readable table. In your browser
Related guides
All guides →- How to Hash Text with SHA-256 Produce a SHA-256 digest, compare it against a published checksum, and know when SHA-256 is the wrong tool.
- How to Debug an API Request Work through a failing request in order, from the status code to the payload.
- SHA-256 vs MD5 Why MD5 is broken, what that actually means, and where it is still acceptable.
Last reviewed .