What is a webhook signature? How verification works

A webhook signature is a value, sent in a request header, that proves a webhook really came from the provider and that its body wasn't changed on the way. The provider computes it from the raw body (often plus a timestamp) and a shared webhook secret, almost always with HMAC-SHA256. Your endpoint recomputes it with the same secret and rejects the request if the values don't match.

POST /webhooks/github HTTP/1.1
X-GitHub-Event: push
X-Hub-Signature-256: sha256=3b1f6e4c0a5d8c3f0a3d5e2b1c4f9a7e8d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a
Content-Type: application/json

{"ref":"refs/heads/main", ...}

How webhook signature verification works

  1. You and the provider share a secret, shown when you create the webhook.
  2. For each delivery, the provider builds a signed string: the raw body, or a timestamp and the body joined together.
  3. It computes HMAC-SHA256(secret, signed_string) and sends the result, hex or base64 encoded, in a header.
  4. Your endpoint builds the same string from the bytes it received, computes the HMAC, and compares in constant time.
  5. If the provider includes a timestamp, you also reject requests older than a few minutes, which stops replay attacks.
import crypto from 'node:crypto'

function verifyGithub(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(expected), b = Buffer.from(header || '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Signature formats by provider

ProviderHeaderSigned stringEncoding
GitHubX-Hub-Signature-256: sha256=...raw bodyhex
StripeStripe-Signature: t=...,v1=...{t}.{body}hex
ShopifyX-Shopify-Hmac-Sha256raw bodybase64
SlackX-Slack-Signature: v0=... with X-Slack-Request-Timestampv0:{timestamp}:{body}hex
Standard Webhookswebhook-signature: v1,... with webhook-id, webhook-timestamp{id}.{timestamp}.{body}base64

Each has its own verifier page: Stripe, GitHub, Shopify, Slack and Standard Webhooks.

Common mistakes

  • Verifying a parsed body. JSON.stringify(JSON.parse(body)) doesn't give back the same bytes. Capture the raw body before any JSON middleware runs on the webhook route.
  • Comparing with ==. A normal string comparison exits at the first differing character, which can leak timing information. Use crypto.timingSafeEqual, hmac.compare_digest in Python, or hmac.Equal in Go.
  • Wrong secret. Test-mode and live-mode secrets differ, and each endpoint in Stripe has its own. Standard Webhooks secrets must be base64-decoded after removing the whsec_ prefix.
  • Mixing up encodings. Comparing a hex digest against a base64 header always fails.
  • Ignoring the timestamp. A valid signed request captured once can be resent forever unless you check its age.
  • A proxy that rewrites the body. Anything that re-encodes, decompresses or reformats the body between the provider and your code breaks the signature. If you forward webhooks, the forwarder must pass the body through byte for byte.
  • Returning 200 on failure. Return 401 for a missing or invalid signature so the failure shows up in the provider's logs. See 401 vs 403.

Signatures and webhook forwarding

When webhooks pass through a webhook gateway or forwarder, the provider's signature still covers the original body. Webhook Relay forwards the body and headers unchanged unless you attach a transform, so your endpoint can verify as usual. You can also verify HMAC signatures at the gateway with a function, as shown in the HMAC authentication docs. For webhooks you send to your own customers, the Outbound Gateway signs every delivery with Standard Webhooks headers.

Frequently asked questions

What is a webhook signature?

A webhook signature is a value the provider computes from the request body and a shared secret, usually with HMAC-SHA256, and sends in a request header. The receiver recomputes it with the same secret and compares. A match proves the webhook came from someone who knows the secret and that the body was not changed in transit.

Why does my webhook signature verification fail?

The most common cause is computing the HMAC over a parsed and re-serialized body instead of the exact raw bytes. Other causes are using the wrong secret, for example a test-mode secret against live events, forgetting to include the timestamp in the signed string, comparing hex against base64, or a proxy that modifies the body.

Where is the webhook signature sent?

In a request header whose name depends on the provider. Stripe uses Stripe-Signature, GitHub uses X-Hub-Signature-256, Shopify uses X-Shopify-Hmac-Sha256, Slack uses X-Slack-Signature, and providers that follow the Standard Webhooks spec use webhook-signature.

Is HTTPS enough without a webhook signature?

No. HTTPS encrypts the request in transit, but anyone who knows your webhook URL can send a request to it. The signature proves the sender knows the shared secret. Use both.