A webhook signature can fail even when the secret is correct. The verifier may hash reserialised JSON instead of the original body, decode the key using the wrong format or compare a hexadecimal signature with Base64 text. HMAC verification depends on an exact agreement about bytes.
Write down the provider's signature contract before implementing it: algorithm, key representation, message construction, timestamp rules and output encoding. Then verify an official test vector or a controlled sample. Guessing from a header name is not a reliable integration method.
Understand what the shared key establishes
HMAC combines a secret key with message bytes through a defined construction based on a hash function. A party with the same key can compute or verify the authentication code. A plain hash of the message does not provide this shared-key authentication property.
The HMAC generator computes an HMAC locally using the chosen supported hash, the entered message encoded as UTF-8 and the entered secret encoded as UTF-8. Its report excludes the secret and message, while showing the resulting code and message byte count.
This input model may differ from a provider that gives you a Base64-encoded or hexadecimal key. Entering that encoded text directly uses its text bytes, not the decoded key bytes. Decode and handle keys according to the provider's contract in your production verifier.
Preserve the original request body
Many webhook providers sign the raw body bytes. Parse the body only after capturing the exact bytes needed for verification. A JSON parser can discard whitespace and member ordering, and a serializer can produce different numeric or Unicode representations.
The objects represented by {"a":1,"b":2} and a spaced version can be equivalent to the application while having different raw hashes. If the provider signed the original bytes, rebuilding a similar object is not enough. Configure middleware so the verifier receives the unmodified body.
Check transfer and content decoding expectations in the provider's documentation. The signed message may be the body alone or a construction containing a timestamp and separator. Follow that definition exactly rather than concatenating fields in an assumed order.
| Part | Example question | Failure if ambiguous |
|---|---|---|
| Algorithm | HMAC-SHA-256 or another specified hash? | Different code bytes |
| Key encoding | Raw UTF-8, Base64 or hexadecimal? | Correct-looking secret becomes the wrong key |
| Message bytes | Raw body or timestamp plus body? | Reserialisation or separator mismatch |
| Output encoding | Hexadecimal or Base64? | Comparing different representations |
| Freshness | Which signed timestamp and tolerance? | Valid old requests can be replayed |
| Key selection | Which endpoint and rotation version? | Using a secret from the wrong integration |
Test a known HMAC vector first
A standard HMAC-SHA-256 test uses the ASCII key Jefe and the message what do ya want for nothing?. The resulting hexadecimal code is shown below. The question mark is part of the message; quotes and a trailing newline are not.
Key: Jefe
Message: what do ya want for nothing?
HMAC-SHA-256:
5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
This checks the basic implementation and representation. It does not prove your webhook message construction is correct. Next use the provider's test vector, including any timestamp prefix and separators, then a controlled real delivery through your endpoint.
If the standard vector fails, inspect algorithm selection and exact UTF-8 input. If it passes but the provider sample fails, focus on the provider-specific byte contract and key encoding. This sequence isolates the problem without changing several assumptions at once.
Compare decoded codes with an appropriate primitive
Parse the signature header according to its documented format, validate the expected representation and length, and decode it to bytes. Compare it with the computed code using a suitable constant-time comparison primitive available in your server environment.
Do not write a character-by-character loop that exits on the first mismatch for a security-sensitive verifier. Also avoid accepting a short prefix because it matches the beginning of the computed value. The protocol defines the expected code length and any allowed truncation; the receiver should not invent its own.
Handle malformed and missing signatures as verification failures before processing the event. Keep error responses appropriate to the provider's retry behaviour without exposing secret material or detailed comparison results to the caller.
Verify freshness and event handling separately
A valid HMAC proves consistency with the shared key and message, but an old valid request can still be replayed. If the provider signs a timestamp, verify that timestamp as part of the signed message and apply a documented acceptance window using a reliable server clock.
Do not trust an unsigned timestamp merely because it arrives beside a valid body signature. An attacker could change the unsigned field. Follow the provider's exact construction so freshness evidence is bound to the authenticated request.
Use event identifiers and idempotent processing to handle legitimate retries. Signature validity does not mean the event has never been processed. Store enough state to avoid charging twice, sending duplicate notifications or applying the same transition repeatedly.
Debug a verifier that fails after adding JSON middleware
Suppose webhook verification worked until a framework upgrade moved JSON parsing before the signature handler. The handler now receives an object and serialises it with JSON.stringify. The resulting text has different whitespace or ordering from the provider's body.
Capture the raw bytes through the framework's supported mechanism before parsing. Compute the HMAC over those bytes and only then hand verified data to the application parser. Test a body with spaces, Unicode and a final newline to confirm the boundary is preserved.
Keep a synthetic signed fixture in the regression suite. It should exercise the real route and middleware order, not only a helper function fed a hand-created string. That test will catch a future refactor that silently reintroduces body transformation.
After the repair, test invalid signatures, expired signed timestamps and duplicate valid events. These cases verify the security and processing workflow around the cryptographic calculation.
Rotate keys without guessing which secret applies
Providers may issue separate secrets for endpoints, environments or accounts. Label them clearly in your secret store. A test-mode secret should not be tried indiscriminately against production traffic, and logs should not print candidate secrets while debugging.
During rotation, follow the provider's supported overlap or version-selection mechanism. If several signatures or keys can be valid temporarily, bound that acceptance period and retire old secrets deliberately. An indefinite list of historical keys increases the consequences of a past exposure.
Record when the new key became active and verify controlled deliveries after deployment. Keep secret values out of source control, screenshots and ordinary reports. A hash prefix of a secret is not automatically a safe public identifier; use an independent label instead.
Log enough to diagnose without storing the payload
Useful fields include endpoint, provider event identifier where safe, signature version, verification outcome, body byte count and timing. Avoid recording the full body when it contains customer data. Use controlled fixtures to investigate byte differences instead of copying live payloads into public tools.
The local HMAC tool is useful for synthetic examples and compatible UTF-8 contracts. It is not a complete webhook verifier and does not enforce timestamp windows, replay rules or endpoint authorisation. Keep those responsibilities in the server integration.
Test the empty-body contract explicitly
Some providers send event types with an empty body or sign a timestamp prefix even when the body is empty. Check the provider’s exact rule and exercise it through the server route. An empty byte sequence is different from the text null, an empty JSON object or a newline. The local text generator requires nonempty input, so use a suitable server test vector for this boundary instead of substituting another message.
Frequently asked questions
Is HMAC the same as hashing secret plus message?
No. HMAC is a defined construction with specific security properties. Use a standard library implementation and the provider's specified algorithm. Do not replace it with a homemade concatenation scheme because the output also looks like a hash.
Why does the same JSON object produce a different code?
The raw text can differ in whitespace, member order, Unicode or numeric representation. If the contract signs raw bytes, preserve them before parsing. Application-level equivalence does not imply signature-input equality.
Can the signature be compared as ordinary text?
Validate and decode the defined representation, then use the server platform's appropriate comparison primitive. Keep expected length and format strict. Avoid partial matches and early-exit comparison code in a security-sensitive verifier.
Does a valid HMAC prevent duplicate processing?
No. The provider or another party with a captured valid request can send it again. Apply signed freshness checks where specified and design idempotent event handling. Authentication and once-only business processing are separate requirements.
What if my secret is Base64 text?
Determine whether the provider means those characters as the key or uses Base64 to represent binary key bytes. The local generator treats entered secrets as UTF-8 text. A production verifier must follow the provider's explicit decoding rule.
Should I log failed request bodies?
Prefer minimal diagnostic metadata and synthetic reproductions. Failed bodies can still contain sensitive information or malicious content. If deeper capture is necessary, use a controlled authorised process with appropriate access and retention.
What proves the integration is ready?
Verify standard and provider vectors, real-route raw-body handling, valid and invalid signatures, freshness rules, key rotation and duplicate events. A correct HMAC helper alone does not establish a correct webhook processing system.