Tutorials

How to Verify a Webhook Signature with HMAC-SHA256 (GitHub, Stripe, Shopify, Slack)

How webhook HMAC signatures work, how GitHub, Stripe, Shopify and Slack sign requests, and Node.js and Python code to verify them safely, plus why signatures fail to match.

By Muhammad Saqlain
·Published 2026-11-03·Updated 2026-11-03·8 min read

Use this guide with

3 ToolsWebPro tools

Open the free tool(s) below and follow the steps in this guide.

The short answer

To verify a webhook signature, compute HMAC-SHA256 over the raw request body (plus a timestamp, if the provider signs one) with your webhook secret. Then compare the result with the signature header using a constant-time comparison. If they match, the request came from someone who knows the secret and the body was not changed.

To test by hand, paste the body and secret into the HMAC generator and compare its output with the header value.

How each provider signs webhooks

↔ Scroll table horizontally to view full data
ProviderHeaderSigned dataFormat
GitHubX-Hub-Signature-256Raw bodysha256= followed by hex
StripeStripe-Signaturetimestamp + "." + raw bodyt=…,v1=hex
ShopifyX-Shopify-Hmac-Sha256Raw bodyBase64
SlackX-Slack-Signature"v0:" + timestamp + ":" + raw bodyv0= followed by hex

Providers change details over time, so confirm the current scheme in their documentation. Stripe and Slack sign a timestamp so you can reject old requests; check that it is within a few minutes of the current time.

Node.js example

Read the body as raw bytes before any JSON middleware parses it. In Express, use express.raw({ type: 'application/json' }) on the webhook route. Then compute the HMAC with crypto.createHmac('sha256', secret).update(rawBody).digest('hex'), build a buffer from the received hex signature (after removing a sha256= prefix), check the two buffers have the same length, and compare them with crypto.timingSafeEqual. Only parse the JSON after the signature is valid.

Python example

In Flask use request.get_data() for the raw body. Compute hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() and compare it with the received value using hmac.compare_digest(expected, received). For Shopify, use base64.b64encode(...digest()) instead of hexdigest(), because Shopify sends Base64.

Why signatures fail to match

The HMAC generator shows hex and Base64 at once and ignores a sha256= prefix when you paste a signature to verify, so you can quickly rule out format problems.

  • The body was re-serialised. JSON.stringify(req.body) changes spacing and key order. Always sign the raw bytes you received.
  • Wrong key format. Most secrets are used as plain text, including Stripe's whsec_ secret. Decode from hex or Base64 only when the provider says so.
  • Missing timestamp. Stripe and Slack sign the timestamp together with the body, not the body alone.
  • Hex versus Base64. GitHub, Stripe and Slack send hex; Shopify sends Base64. The same HMAC looks completely different in each.
  • Wrong secret. Test and live modes, or different endpoints, often have different secrets.
  • Encoding or trailing newline. An extra newline changes the HMAC. Copy payloads from your logs exactly when testing.

HMAC is not encryption

An HMAC proves authenticity and integrity; it does not hide the payload. Keep webhooks on HTTPS. A plain SHA-256 hash is not a substitute either: anyone can compute it, whereas only someone with the secret can produce a valid HMAC. If a signature arrives Base64-encoded and you want to inspect its bytes, the Base64 decoder helps, but compare signatures in code, not by eye.

Frequently asked questions

Why does my computed HMAC not match the webhook signature?

In most cases you signed different bytes. Use the raw request body before any JSON parsing, include the timestamp when the provider signs one, and decode the secret only if the provider says it is hex or Base64.

Should I compare signatures with ==?

No. Use a constant-time comparison such as crypto.timingSafeEqual in Node.js or hmac.compare_digest in Python, so the comparison time does not leak how many characters matched.

Do I still need HTTPS if webhooks are signed?

Yes. The HMAC proves who sent the request and that it was not changed, but it does not encrypt the body. HTTPS keeps the payload private in transit.

M

Muhammad Saqlain

Cybersecurity Practitioner & Lead Engineer

Builds and tests ToolsWebPro's file converters, invoice viewers and browser-side security tools.

Read full author bio & credentials →