Skip to content
apikey.tools
HMACSignatures

Webhook Payload Verification Signature Calculator

Reproduce Stripe, GitHub, Shopify, Slack and Svix signatures, and check one you received.

HMACRuns in this tabInput not transmitted

Input

Paste the whole header. It is compared in constant time.

sec

Output

computing

Every provider uses HMAC-SHA256. They differ in what they feed into it, and that difference is why a verification that works for GitHub fails for Stripe.

What each provider signs

ProviderHeaderSigned bytesEncoding
StripeStripe-Signaturetimestamp.bodyhex, as t=…,v1=…
GitHubX-Hub-Signature-256raw bodyhex, prefixed sha256=
ShopifyX-Shopify-Hmac-Sha256raw bodyBase64
SlackX-Slack-Signaturev0:timestamp:bodyhex, prefixed v0=
Svixwebhook-signaturemsg_id.timestamp.bodyBase64

The "bytes actually signed" panel shows the constructed string for whichever provider you select, which is usually enough to spot the mismatch immediately.

The raw body problem

Overwhelmingly the most common cause of a failed verification. Your framework parses JSON before your handler runs, and you sign JSON.stringify(req.body) — which is not the bytes that arrived. Key order, whitespace and Unicode escaping all differ.

Every provider's documentation says to use the raw body. Every framework makes that inconvenient. Configure it explicitly:

javascript
// Express — capture the buffer during parsing
app.use("/webhooks", express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; },
}));

// Next.js App Router — read text first, parse second
export async function POST(request: Request) {
  const raw = await request.text();
  if (!verify(raw, request.headers.get("stripe-signature"))) {
    return new Response("Invalid signature", { status: 400 });
  }
  const event = JSON.parse(raw);
}

The timestamp is not decoration

Stripe, Slack and Svix include a timestamp in the signed payload so you can reject old requests. Without that check, a captured request is replayable forever — and a payment_intent.succeeded replayed a thousand times is a thousand fulfilments.

Five minutes is the standard tolerance. This tool flags a signature that is cryptographically valid but outside the window, because that is exactly the case a correct implementation must reject and a naive one accepts.

Verification order

  1. Read the raw body.
  2. Check the timestamp is within tolerance. Reject early if not.
  3. Compute the expected signature over the provider's constructed string.
  4. Compare in constant time.
  5. Only then parse the JSON and act on it.

Parsing before verifying means you are running your JSON parser on unauthenticated attacker input.

Frequently asked questions

Why constant-time comparison?

A comparison that returns on the first differing byte leaks the correct signature through response timing. It is a real, demonstrated attack against webhook endpoints, and the fix is one function call.

Should I return 400 or 401 for a bad signature?

Can I skip verification if the endpoint URL is secret?

No. A secret URL appears in your logs, your proxy's logs and any Referer header. It cannot be rotated without reconfiguring the sender, and it says nothing about whether the payload was modified in transit.

How do I test that my rejection path works?

Use the webhook sandbox with tampering enabled. It produces a request whose body was changed after signing. If your endpoint accepts it, you are not verifying the raw body.

More signatures tools

Compute and verify request signatures