All tools

Webhook Signature Verifier & HMAC Playground

Verify HMAC webhook signatures locally, inspect the exact canonical string being hashed, and diagnose why signature checks fail.

All HMAC calculations happen entirely in your browser using the Web Crypto API. Your secret, payload, and signature are never sent to a server. CodeAva does not log, transmit, or store any inputs from this tool.

Provider preset

HMAC-SHA256 of the raw body. No prefix. Hex output. Adjust as needed.

Raw payload (request body)
Use the raw request body bytes — not parsed JSON, reformatted, or modified in any way.
0 bytes
Webhook secret

The secret is used locally only — it is never transmitted.

Received signature header value

Paste the full header value including any prefix (e.g. sha256=).

Algorithm & encoding
Canonical string format

Enter payload and secret

Enter a payload and secret to see results.

All HMAC calculations happen entirely in your browser using the Web Crypto API. Your webhook secret, raw payload, and signature values are never transmitted to a server. CodeAva does not log, store, or process any data entered into this tool. For production webhook verification, implement constant-time comparison on your server to prevent timing side-channel attacks.

Overview

Webhook signature verification failures are almost never caused by a wrong secret alone. In the vast majority of cases, the mismatch happens because the string being hashed by your server code is not byte-for-byte identical to the string the provider signed. This includes differences in whitespace, key ordering, delimiter choice, timestamp placement, JSON reformatting, and encoding format — any one of which produces a completely different HMAC output even with the correct secret.

The most frequent single cause is hashing a parsed JSON object instead of the raw request body. When a framework deserializes and re-serializes JSON, it may change whitespace, key order, or number formatting. The provider signed the original byte sequence from the network — your code must use exactly those same bytes. Using body-parser, request.body after JSON parsing, or JSON.stringify(parsedBody) as the HMAC input are all common mistakes that produce a signature mismatch even when every other parameter is correct.

A local browser-based verifier is safer for this debugging task than sending data to a server because your webhook secret should be treated like a password. This tool uses the browser's native Web Crypto API — the same cryptographic primitives used in production TLS — to compute HMAC signatures locally, so the secret and payload never leave your machine.

Use cases

When to use it

  • Debugging failed Stripe webhook verificationuse the Stripe preset to configure the canonical string format (timestamp.body), strip the v1= prefix, and compare the computed hex signature against the Stripe-Signature header value.
  • Checking GitHub X-Hub-Signature-256 logicuse the GitHub preset for raw body HMAC-SHA256 with sha256= prefix stripping. The canonical string is the raw body only — no timestamp or version prefix.
  • Verifying Slack's version:timestamp:body formatuse the Slack preset to build the canonical string as v0:<X-Slack-Request-Timestamp>:<raw_body> and strip the v0= prefix from X-Slack-Signature before comparing.
  • Diagnosing Shopify HMAC-SHA256 with Base64 outputuse the Shopify preset for raw body with Base64 encoding instead of hex. X-Shopify-Hmac-Sha256 is Base64-encoded — a common source of mismatch when verifying against a hex output.
  • Comparing different encodings and prefixesswitch between hex and base64, toggle hex case, and add or remove the signature prefix to identify whether an encoding difference is causing the mismatch.
  • Generating copy-ready server-side verification codethe Code Snippets tab generates Node.js, Python, and Go verification examples configured for the current algorithm, encoding, and canonical mode — with constant-time comparison.

When it's not enough

  • Production secret storage or transmissionthis is a debugging tool, not a signing service. Never paste production webhook secrets into any tool you do not fully control. This tool is local, but treat secret handling with care.
  • Automated webhook replay or forwardingthis tool verifies signatures for debugging — it does not send, replay, or forward webhooks. Use it to diagnose your server-side logic, not to generate valid signatures for external services.
  • Replacing provider documentationprovider signature formats can change. Always verify against the current official documentation for Stripe, GitHub, Slack, Shopify, or your specific provider. Presets are convenience helpers based on documented formats.

How to use it

  1. 1

    Select a provider preset or configure manually

    Click a preset button (Stripe, GitHub, Slack, Shopify, Generic) to preconfigure the canonical format, algorithm, encoding, and prefix. All settings remain editable after selecting a preset.

  2. 2

    Paste the raw request body exactly

    The payload field must contain the raw body bytes as received by your server — before JSON parsing, reformatting, or any mutation. Check the byte count and trailing-whitespace indicator.

  3. 3

    Enter the webhook secret and received signature

    The secret field is masked by default and never transmitted. Paste the full signature header value including any prefix (sha256=, v0=, v1=).

  4. 4

    Inspect the canonical string preview

    The Canonical String tab shows the exact input fed to the HMAC function. For Stripe, this is timestamp.rawBody. For Slack, v0:timestamp:rawBody. Any character difference from what the provider signed produces a mismatch.

  5. 5

    Fix your server code using diagnostics and snippets

    The Diagnostics tab identifies specific causes (encoding mismatch, prefix issues, whitespace, algorithm difference). The Code Snippets tab generates production-ready verification code for Node.js, Python, and Go.

Common errors and fixes

Signature mismatch despite correct secret

Almost always a canonical string issue. The most common cause is hashing parsed JSON (request.body after JSON.parse()) instead of the raw body bytes. Use the raw buffer from the request stream before any parsing. Check the Canonical String tab to inspect the exact input being hashed.

Hex vs Base64 mismatch

Shopify uses Base64 HMAC output. Stripe and GitHub use lowercase hex. If the encoding selector doesn't match the provider, the signatures will never match even with the correct secret and canonical string. Switch the encoding and re-check.

Signature prefix not stripped before comparison

GitHub prefixes with sha256=. Slack prefixes with v0=. Stripe uses v1=. If you compare the full header value (including prefix) against the raw HMAC output (without prefix), they will never match. Set the prefix field to strip it before comparison.

Timestamp or version missing from canonical string

Stripe requires timestamp.rawBody — the timestamp value from the Stripe-Signature t= field must be included. Slack requires v0:timestamp:rawBody — both version and timestamp must be present. Add the timestamp field and select the correct canonical mode.

Wrong secret for the current environment

Webhook secrets are typically environment-specific. A staging or test secret will not match a production webhook. Verify you are using the correct secret from the provider dashboard for the environment that sent the webhook.

Trailing newline or whitespace in payload

HTTP frameworks often append a trailing newline when reading the request body as a string. If your payload has trailing whitespace, the canonical string will differ from what the provider signed. The tool shows a trailing whitespace warning when detected.

SHA-1 vs SHA-256 mismatch

GitHub previously used HMAC-SHA1 for X-Hub-Signature but now defaults to HMAC-SHA256 for X-Hub-Signature-256. Check which header your code is reading and match the algorithm accordingly.

Frequently asked questions

Why is my webhook signature verification failing?

Most webhook signature failures happen because the string your server is hashing is not identical to the string the provider signed. Common causes include:

  • Hashing parsed JSON instead of the raw body — use the raw request body bytes before any JSON parsing.
  • Wrong canonical format — Stripe prepends a timestamp, Slack prepends version and timestamp. Using raw body only for those providers will always produce a mismatch.
  • Encoding mismatch — Shopify uses Base64; Stripe and GitHub use lowercase hex. Comparing across encoding formats will never succeed.
  • Prefix not stripped — GitHub sends sha256=<hex>. Comparing the full header value against the raw HMAC output (without prefix) fails.
  • Wrong environment secret — test and production secrets are different in every provider dashboard.
  • Trailing whitespace — some frameworks add a trailing newline when reading the body as a string.

The safest debugging approach is to paste the exact raw body into the Canonical String tab, verify the format matches your provider's documented format, and compare the byte-level output before touching the secret or signature.

Webhook provider signature format cheat sheet

ProviderCanonical string formatEncoding / header shapeCommon pitfall
Stripetimestamp + "." + raw_body (t= from Stripe-Signature)Lowercase hex, prefixed with v1=Not extracting t= from the Stripe-Signature header; hashing parsed JSON
GitHubRaw body onlyLowercase hex, header: X-Hub-Signature-256: sha256=<hex>Not stripping sha256= prefix; using older X-Hub-Signature (SHA-1) header
Slack"v0:" + X-Slack-Request-Timestamp + ":" + raw_bodyLowercase hex, header: X-Slack-Signature: v0=<hex>Wrong canonical order; missing v0: prefix; not using the timestamp header value
ShopifyRaw body onlyBase64 HMAC-SHA256, header: X-Shopify-Hmac-Sha256Using hex instead of Base64; hashing query parameters instead of body
GenericRaw body (most common default)Hex or Base64, no standard prefixCheck provider docs for canonical format, encoding, and header name

Provider signature formats can change. Always verify against current official documentation.

Why use constant-time comparison for webhook verification?

Standard string equality (=== in JavaScript, == in Python) compares characters from left to right and returns as soon as a mismatch is found. This means the comparison takes slightly longer the more characters at the start match — a timing side-channel that allows an attacker to guess a valid signature one character at a time by measuring response times.

Constant-time comparison functions — such as crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python, and subtle.ConstantTimeCompare in Go — always take the same amount of time regardless of where the comparison fails. Use these in production webhook verification code. The generated snippets in this tool use constant-time comparison by default.