Security Tools

HMAC Generator & Webhook Signature Verifier

Compute HMAC-SHA1, SHA-256, SHA-384 or SHA-512 signatures, and debug failing GitHub, Stripe, Shopify or Slack webhooks by checking a received signature against the raw body and your secret.

Private by design: anything you enter is processed in your browser only. It is never uploaded to or logged on our servers. Secret keys are never included in share links, only the payload, settings and received signature.

The signature is calculated over the raw request body, byte for byte. Paste it exactly as received: pretty-printed or re-serialized JSON, re-ordered keys, changed whitespace or a missing trailing newline all produce a different signature. Line breaks typed in the box are sent as \n.

Signature
Header
Signed Payload (exact string that is HMACed)

                                

0 bytes

About HMAC and webhook signatures

HMAC (Hash-based Message Authentication Code, RFC 2104) combines a secret key with a hash function such as SHA-256 to produce a tag that proves a message came from someone holding the key and was not modified in transit. Unlike a plain hash from the hash generator, an attacker who changes the message cannot compute a valid new tag without knowing the secret. JSON Web Tokens signed with HS256 use the same HMAC-SHA256 construction; inspect them with the JWT decoder.

Webhook providers use HMAC to sign the requests they send to your server. GitHub sends X-Hub-Signature-256: sha256=<hex> computed over the body. Stripe sends Stripe-Signature: t=<timestamp>,v1=<hex> where the signed payload is the timestamp, a dot and the body. Shopify sends the Base64 encoded HMAC-SHA256 of the body in X-Shopify-Hmac-Sha256. Slack signs v0:<timestamp>:<body> and sends X-Slack-Signature: v0=<hex> together with X-Slack-Request-Timestamp.

The most common reason a signature fails to verify is that the body was parsed and re-serialized by a framework before it was hashed. Always capture the raw bytes of the request body (for example $request->getContent() in Laravel or express.raw() in Node) and compare signatures with a constant-time function such as hash_equals or crypto.timingSafeEqual. Stripe and Slack also include a timestamp so you can reject requests older than a few minutes to prevent replay attacks; the Unix timestamp converter turns it into a readable date.

This tool uses the browser's Web Crypto API, so your payloads and secrets are never sent anywhere. Even so, prefer test-mode secrets when debugging and rotate any production secret that may have been exposed. When you create your own signing secret, use at least 32 random bytes, for example from the secret key generator. HMAC is for message authentication only; store user passwords with the bcrypt generator instead.

Reference Guide

How the major webhook providers sign requests (all use HMAC-SHA256):

ProviderHeaderSigned stringEncodingKey
GitHubX-Hub-Signature-256: sha256=<sig>Raw bodyHexWebhook secret you entered on GitHub
StripeStripe-Signature: t=<ts>,v1=<sig><ts>.<body>HexEndpoint secret whsec_..., used as-is
ShopifyX-Shopify-Hmac-Sha256: <sig>Raw bodyBase64App client secret
SlackX-Slack-Signature: v0=<sig>v0:<ts>:<body>, ts from X-Slack-Request-TimestampHexApp signing secret

Common reasons a signature does not match:

SymptomCause and fix
Fails for every requestA JSON middleware parsed the body and you hashed the re-serialized object. Hash the raw bytes (express.raw(), $request->getContent(), request.get_data()).
Fails after switching environmentWrong secret: test and live endpoints have different secrets, and the Stripe CLI (stripe listen) prints its own.
Fails with a correct-looking secretThe secret was hex or Base64 decoded when the provider uses it as plain text, or vice versa. Check the Key Encoding setting.
Stripe or Slack onlyThe signed string must use the timestamp from the request header, not the current time.
Equal values reported as differentThe sha256= or v0= prefix was not stripped (or added) before comparing, or hex case differs.
Fails only for some payloadsNon-ASCII characters: compute the HMAC over UTF-8 bytes, exactly as received.

Verify a GitHub signature with a constant-time comparison (never use ==):

// Node.js (rawBody is a Buffer)
const crypto = require('crypto');
const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'));
const received = Buffer.from(req.get('X-Hub-Signature-256') || '');
const valid = expected.length === received.length && crypto.timingSafeEqual(expected, received);

// PHP / Laravel
$expected = 'sha256=' . hash_hmac('sha256', $request->getContent(), $secret);
$valid = hash_equals($expected, (string) $request->header('X-Hub-Signature-256'));

# Python
import hmac, hashlib
expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, request.headers.get('X-Hub-Signature-256', ''))

crypto.timingSafeEqual throws when the lengths differ, hence the length check first.

Example input

Preset:  GitHub
Secret:  demo_webhook_secret
Body:    {"action":"opened","number":42,"pull_request":{"title":"Add webhook handler"},"repository":{"full_name":"octo-org/hello-world"}}
Header:  X-Hub-Signature-256: sha256=52901a8f8a869d3602c425619d5fc9221f099669b907895c807b06b73c761275

This sample loads automatically when you open the tool. Use Load Sample to bring it back.

Frequently Asked Questions

HMAC (Hash-based Message Authentication Code) combines a secret key with a hash function such as SHA-256 to prove that a message came from someone who knows the key and was not modified.

The most common cause is hashing a re-serialised body instead of the raw request bytes. Whitespace, key order or encoding changes produce a completely different signature. Also check the secret, the algorithm and whether the provider signs a timestamp prefix.

Yes. Signatures are computed with the browser Web Crypto API and the key never leaves your device.