How to Verify a Webhook Signature (HMAC SHA256)
Verify webhook signatures so you only trust authentic requests. How HMAC SHA256 signing works, how GitHub, Stripe and Shopify do it, a Node.js example, and a free verifier.
Your webhook endpoint is a public URL. That means anyone who discovers it can send it requests — including fake ones. Signature verification is how you make sure a webhook genuinely came from the provider and wasn't tampered with in transit. Skip it, and an attacker could forge a payment.succeeded or subscription.created event.
This guide explains how HMAC signing works, how the major providers implement it, and how to verify it correctly.
How webhook signing works
When a provider sends a webhook, it computes an HMAC — a hash of the request body mixed with a secret only you and the provider know — and puts the result in a header. You recompute the same HMAC on your side and compare. If they match, the body is authentic and unmodified, because an attacker can't produce a valid hash without the secret.
The steps are always the same:
- Read the raw request body (the exact bytes, not re-serialized JSON).
- Compute
HMAC-SHA256(secret, rawBody). - Compare it to the signature header using a constant-time comparison.
How the major providers do it
| Provider | Header | Algorithm | Notes |
|---|---|---|---|
| GitHub | X-Hub-Signature-256 | HMAC-SHA256, hex, prefixed sha256= | Strip the sha256= prefix before comparing |
| Stripe | Stripe-Signature | HMAC-SHA256 over timestamp.body | Header contains t= and v1=; check the timestamp to prevent replays |
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256, base64 | Encode your digest as base64, not hex |
| Slack | X-Slack-Signature | HMAC-SHA256 over v0:timestamp:body | Versioned signing string |
| Square | x-square-hmacsha256-signature | HMAC-SHA256, base64 | Signs the notification URL + raw body — the URL must match exactly |
| LINE | x-line-signature | HMAC-SHA256, base64 | Channel secret over the raw body; never deserialize first |
| Airwallex | x-signature | HMAC-SHA256, hex | Signs x-timestamp (milliseconds) + body; prepend the timestamp |
| MyFatoorah | MyFatoorah-Signature | HMAC-SHA256, base64 | Signs a canonical Key=Value string per event, not the raw body |
The last four are worth a closer look because each breaks the "just HMAC the raw body" assumption: Square prepends your endpoint URL, Airwallex prepends a millisecond timestamp, and MyFatoorah signs a hand-built canonical string instead of the body at all. Get those details wrong and verification silently fails.
A Node.js example
const crypto = require('crypto')
// rawBody must be the exact bytes you received (Buffer or string), not JSON.parse'd output.
function verifyGitHub(rawBody, signatureHeader, secret) {
const digest = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
// constant-time comparison avoids timing attacks
const a = Buffer.from(digest)
const b = Buffer.from(signatureHeader || '')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
For Shopify, swap .digest('hex') for .digest('base64') and drop the sha256= prefix. For Stripe, build the signed payload as ${timestamp}.${rawBody} and compare against the v1 value.
Test your verification with a free tool
Before wiring this into production, confirm your inputs line up. Paste the raw body, the secret and the expected signature into the free HMAC generator & signature verifier — it computes the HMAC for SHA256, SHA512, SHA1 or MD5 and tells you whether it matches the provider's value. It's the fastest way to rule out "is my secret right?" before debugging code.
Provider-specific verifiers
The generic verifier works for any plain-HMAC provider, but for the ones below we built dedicated tools that bake in that provider's exact scheme — the right header, encoding, timestamp/URL handling and secret format — plus copy-paste Node.js and Python:
- Verify Stripe webhook signatures —
Stripe-Signature, timestamp + body - Verify GitHub webhook signatures —
X-Hub-Signature-256, hex - Verify Shopify webhook signatures —
X-Shopify-Hmac-Sha256, base64 - Verify Slack webhook signatures —
v0:timestamp:body - Verify Square webhook signatures —
x-square-hmacsha256-signature, signs URL + body - Verify LINE webhook signatures —
x-line-signature, channel secret - Verify Airwallex webhook signatures —
x-signature+x-timestamp - Verify MyFatoorah webhook signatures — canonical
Key=Valuestring - Verify Twilio webhook signatures — URL + sorted params, HMAC-SHA1
- Verify Standard Webhooks signatures — Svix/OpenAI/Anthropic
- All providers →
Common mistakes
- Verifying the parsed body. Frameworks often parse JSON before you see it; re-serializing changes the bytes and breaks the signature. Capture the raw body.
- Using
==to compare. Use a constant-time comparison (crypto.timingSafeEqual) to avoid timing attacks. - Wrong encoding. GitHub is hex, Shopify is base64 — mixing them up fails silently.
- Ignoring the timestamp. For Stripe/Slack, reject signatures with an old timestamp to prevent replay attacks.
- Forgetting the secret rotates. Support more than one valid secret during rotation.
Where this fits
Signature verification is one item on a longer list — see webhook security best practices for the full checklist, and how to test webhooks to capture real signed payloads to test against.
Need to verify a signature right now? Open the HMAC verifier, or inspect a live webhook to grab a real signature header.
