---
title: "How to Verify a Webhook Signature (HMAC SHA256) | WebhookRelay"
meta:
  "og:description": "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."
  "og:title": "How to Verify a Webhook Signature (HMAC SHA256)"
  description: "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."
---

![Stripes](https://webhookrelay.com/blog/verify-webhook-signature/images/stripes.svg)

# **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](#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:

1. Read the **raw** request body (the exact bytes, not re-serialized JSON).
2. Compute `HMAC-SHA256(secret, rawBody)`.
3. Compare it to the signature header using a **constant-time** comparison.

## [How the major providers do it](#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](#a-nodejs-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](#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](https://webhookrelay.com/blog/verify-webhook-signature/hmac-verification/) — 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](#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](https://webhookrelay.com/blog/verify-webhook-signature/verify-stripe-webhook-signature/) — `Stripe-Signature`, timestamp + body
- [Verify GitHub webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-github-webhook-signature/) — `X-Hub-Signature-256`, hex
- [Verify Shopify webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-shopify-webhook-signature/) — `X-Shopify-Hmac-Sha256`, base64
- [Verify Slack webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-slack-webhook-signature/) — `v0:timestamp:body`
- [Verify Square webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-square-webhook-signature/) — `x-square-hmacsha256-signature`, signs URL + body
- [Verify LINE webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-line-webhook-signature/) — `x-line-signature`, channel secret
- [Verify Airwallex webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-airwallex-webhook-signature/) — `x-signature` + `x-timestamp`
- [Verify MyFatoorah webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-myfatoorah-webhook-signature/) — canonical `Key=Value` string
- [Verify Twilio webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-twilio-webhook-signature/) — URL + sorted params, HMAC-SHA1
- [Verify Standard Webhooks signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-standard-webhooks-signature/) — Svix/OpenAI/Anthropic
- [All providers →](https://webhookrelay.com/blog/verify-webhook-signature/verify-webhook-signature/)

## [Common mistakes](#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](#where-this-fits)

Signature verification is one item on a longer list — see [webhook security best practices](https://webhookrelay.com/blog/verify-webhook-signature/blog/webhook-security/) for the full checklist, and [how to test webhooks](https://webhookrelay.com/blog/verify-webhook-signature/blog/how-to-test-webhooks/) to capture real signed payloads to test against.

Need to verify a signature right now? [Open the HMAC verifier](https://webhookrelay.com/blog/verify-webhook-signature/hmac-verification/), or [inspect a live webhook](https://webhookrelay.com/blog/verify-webhook-signature/webhook-bin/) to grab a real signature header.