HMAC and Webhook Signatures

Calculate an HMAC or check a GitHub, Stripe or Slack webhook signature. The secret stays on this device, and this page shows no ads.

Runs in your browser. Your input is not sent anywhere.

For a webhook: the raw request body, exactly as received.

Never stored, not even with “Remember my input on this device” on.

An HMAC in hexadecimal or Base64; compared in constant time.

Off by default. When on, the fields of this tool are kept in the local storage of this browser, unencrypted, until you turn it off.

No HMAC yet

Your result will appear here.

Keyboard shortcuts

Ctrl + Enter, ⌘ + Enter
Run the tool with the current input
Alt + Shift + C
Copy the result
Alt + Shift + X
Clear all fields

Shortcuts work while the focus is inside the tool. Every action is also available as a button.

How to use it

  1. Paste the message. For a webhook, this is the raw request body exactly as it arrived: log it before your framework parses the JSON, because re-serialized JSON rarely has the same bytes.
  2. Enter the secret key. For keys given in hexadecimal or Base64, choose the matching input format so that the tool uses the bytes, not the text.
  3. For a plain HMAC, choose SHA-256, SHA-384 or SHA-512 and read the result in hexadecimal and Base64. Paste a value into “Compare with” to check it.
  4. For a webhook, choose GitHub, Stripe or Slack and paste the signature header. The tool builds the string the provider signs, calculates the signature and says whether it matches. Leave the header empty to create a valid one for testing your own endpoint.

Worked examples

RFC 4231, test case 2: the key is the text Jefe and the message is “what do ya want for nothing?”.

HMAC of RFC 4231 test case 2
HMAC-SHA256 (hex)     5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
HMAC-SHA256 (Base64)  W9zBRr9gdU5qBCQmCJV1x1oAPwidJzmDnexYuWTsOEM=
HMAC-SHA512 (hex)     164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7
                      ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b
                      636e070a38bce737

GitHub’s documentation gives a test delivery: the secret “It’s a Secret to Everybody” (with a straight apostrophe) and the body “Hello, World!” must produce the header below. The tool reports it as valid.

GitHub test values
Secret   It's a Secret to Everybody
Body     Hello, World!
Header   X-Hub-Signature-256: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

Slack’s guide works through a slash command request with the signing secret 8f742231b10e8888abcd99yyyzzz85a5 and the timestamp 1531420618; the tool reproduces its signature v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503 from the body given there.

What each provider signs

All three use HMAC with SHA-256 and the secret as UTF-8 text, and send the result as lowercase hexadecimal. They differ in what goes into the signed string and where the timestamp comes from.

Webhook signature schemes
Provider and headerSigned stringHeader value
GitHub, X-Hub-Signature-256the raw bodysha256=<hex>
Stripe, Stripe-Signature<t> + “.” + the raw bodyt=<unix time>,v1=<hex>; several v1 values while a secret is being rolled
Slack, X-Slack-Signature“v0:” + X-Slack-Request-Timestamp + “:” + the raw bodyv0=<hex>

Stripe’s key is the whole endpoint secret, including its whsec_ prefix, as Stripe’s own libraries use it. Stripe says to ignore schemes other than v1; test events also carry a v0 value, which the tool skips. GitHub still sends an older X-Hub-Signature header with HMAC-SHA1 for compatibility and recommends the SHA-256 header.

The timestamp protects against replayed requests, because it is signed together with the body. Stripe’s libraries reject timestamps older than 5 minutes by default, and Slack’s guide rejects requests more than 5 minutes off in either direction. The tool warns when a timestamp is more than 5 minutes old or in the future, measured with this device’s clock.

The comparison takes the same time wherever the first difference is. In a browser that detail does not matter, but on your server it does: comparing with == can leak how many leading characters of a forged signature were right. All three providers ask for a constant-time comparison.

Verifying a GitHub delivery in Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyGitHub(rawBody, header, secret) {
  const expected = Buffer.from(
    'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'),
  );
  const received = Buffer.from(header ?? '');
  return (
    received.length === expected.length && timingSafeEqual(received, expected)
  );
}

On the command line, OpenSSL calculates the same value for a text key. OpenSSL 3 prints the output below; older versions and LibreSSL label it differently.

OpenSSL 3
printf '%s' 'Hello, World!' | openssl dgst -sha256 -hmac "It's a Secret to Everybody"
SHA2-256(stdin)= 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

Common mistakes

  • Signing parsed and re-serialized JSON instead of the raw body. Whitespace, key order and escapes change, and the signature no longer matches.
  • A trailing line break. Copying a body from a terminal or a file often adds one; echo adds one too, which is why the OpenSSL example uses printf.
  • Using the wrong secret: the Stripe secret of another endpoint or of test mode, or a Slack verification token instead of the signing secret.
  • Treating a hexadecimal key as text. The text 0b0b… and the bytes 0x0b 0x0b … give different HMACs.
  • Comparing signatures with == or comparing the hexadecimal strings in different letter case.

Sources

Provider documentation was read on 2026-09-29; providers can change their schemes, so check their pages if a signature that should match does not.

Frequently asked questions

Is my secret sent anywhere or saved?
No. The HMAC is calculated by your browser’s Web Crypto interface, nothing is sent, and the key field is never written to storage, even when “Remember my input on this device” is on. This page loads no ads or other third-party scripts. Still, prefer test secrets, or rotate a production secret after pasting it into any web page.
Why does my webhook signature not match?
Almost always because the body is not byte for byte what the provider sent: it was parsed and serialized again, re-encoded, or trimmed. Other causes are the wrong secret, a missing timestamp in the signed string for Stripe or Slack, and a stray line break at the end.
Is HMAC-SHA256 the same as SHA-256 of the key and message?
No. HMAC hashes twice with the key mixed in through inner and outer padding (RFC 2104). Simply hashing the key followed by the message is open to length-extension attacks with SHA-256, which HMAC prevents.
Hex or Base64: which output do I need?
They are the same bytes written differently. GitHub, Stripe and Slack send lowercase hexadecimal; other APIs use Base64, and a JWT signed with HS256 carries the bytes in Base64url. Match what the receiving side expects.
How long should an HMAC key be?
RFC 2104 discourages keys shorter than the hash output, so use at least 32 random bytes for SHA-256, 48 for SHA-384 and 64 for SHA-512. Keys longer than the block size, 64 bytes for SHA-256 and 128 bytes for SHA-384 and SHA-512, are hashed first.

Last updated: September 29, 2026