The keyed hash that authenticates most of the machine-to-machine web: what an HMAC proves that a checksum cannot, the raw-body mistake behind almost every failing webhook signature, and what keys need that salts do not.

What an HMAC proves

An HMAC answers a question a plain hash cannot: not "is this data intact" but "does this data come from someone holding the secret". Defined in RFC 2104, it runs the hash twice with the key mixed in both times, H(key XOR opad, H(key XOR ipad, message)), a shape chosen so that neither knowing hash internals nor extending messages helps a forger. The result behaves like a signature between two parties who share a secret: same key plus same bytes always reproduces it, anyone without the key cannot.

That construction has aged remarkably. HMAC's security does not lean on collision resistance, which is why HMAC-SHA1 still stands (TOTP two-factor codes run on it to this day) even though SHA-1 itself fell in 2017. New systems still pick HMAC-SHA256, but the reason is hygiene and headroom, not a known crack.

How to use this generator

  1. Pick the algorithm: SHA-256 is the default and what nearly every current API signs with; SHA-1 and SHA-512 cover the legacy and long-digest cases.
  2. Enter the key: in the field in the options bar, read as UTF-8 bytes. It is wired deliberately outside the tool's remembered options, so it never touches localStorage, and like everything here it never leaves the tab.
  3. Paste the message or drop the captured payload file, and the signature updates live. --base64 switches to the encoding your API publishes, and the verify field compares an expected signature, naming the first differing character on mismatch.

Debugging webhook signatures

The single most common reason a webhook signature check fails is that the receiver signs different bytes than the sender did. Stripe, GitHub, Shopify and the rest compute their HMAC over the raw request body, the exact bytes on the wire. The moment a framework parses that body into objects and you re-serialise it for signing, key order, whitespace or Unicode escaping shifts and the signature is dead, with nothing visibly wrong in the logs. Express's express.raw(), or the rawBody capture pattern, exists for this.

A tool like this page shortens the debugging loop: paste the captured raw body, type the endpoint secret, and compare against the header value. If they match here but not in your service, your service is not hashing the raw body. If they do not match here either, the secret is wrong or the provider signs a composite (Stripe signs timestamp.body, not the body alone). The failure FAQ below lists the full checklist.

Keys: length, storage, rotation

An HMAC key is a secret credential and wants the same treatment as an API token: generated from a cryptographic random source at hash-output length or more (32 bytes for SHA-256), stored in a secret manager rather than code, and rotated by running old and new keys in parallel during the switchover, which is why webhook providers let you have two active secrets at once. Our password generator produces suitable random material if you need a key by hand.

One byte-level trap deserves its own sentence: when a provider gives you a key as hex or Base64, the key is the decoded bytes. Feeding the encoded string into the HMAC, which every library happily accepts, produces consistently wrong signatures that look like a protocol bug.

A key is not a salt

The two get mixed up because both are "extra bytes next to the data". They solve opposite problems. A salt is public, unique per record, and exists to make identical inputs hash differently; it defeats precomputed tables in password storage and hides nothing. A key is secret, shared, and exists to make the hash uncomputable for outsiders; leak it and the authentication is void, while a leaked salt costs nothing. So an HMAC with a public "key" is as pointless as a password hash with a secret "salt" is fragile. Password storage, where the salt lives, has its own tooling on the bcrypt page; the guide to password hashing covers the wider picture, including the pepper, which really is a secret and is best applied as, fittingly, an HMAC over the password.

HMAC questions

What is the difference between a hash and an HMAC?

A hash proves integrity, an HMAC proves integrity plus origin. Anyone can compute sha256(message), so a bare hash only shows the data was not corrupted. An HMAC mixes a shared secret key into the hashing (H(key XOR opad, H(key XOR ipad, message)) per RFC 2104), so only a party holding the key can produce or check the value. That makes it a message authentication code: a webhook receiver that recomputes the HMAC and gets a match knows the payload came from the sender it shares the secret with, not from anyone else on the network.

Why is HMAC used instead of just hashing the secret with the message, like sha256(key + message)?

Because plain sha256(key + message) is forgeable through length extension: SHA-256 exposes its internal state as the digest, so an attacker holding one valid value can append data and compute a new valid value without ever learning the key. The nested HMAC construction from 1996 blocks this and comes with a security proof that only needs weak assumptions about the hash, which is also why HMAC-SHA1 and even HMAC-MD5 remain unforged despite both hashes having broken collision resistance. In short: composing a keyed hash yourself is a known trap, HMAC is the standard way out of it.

How do I verify a webhook signature from Stripe or GitHub?

Both compute an HMAC-SHA256 with your endpoint secret over the raw request body. GitHub sends hex in X-Hub-Signature-256 ("sha256=" prefix plus lowercase hex); Stripe sends its Stripe-Signature header as t=<timestamp>,v1=<hex>, and the signed message is <timestamp>.<raw body> rather than the body alone. Verification is: read the raw bytes (before any JSON parsing), compute the HMAC with the shared secret, and compare with a constant-time function like crypto.timingSafeEqual or hmac.compare_digest. Stripe additionally wants the timestamp checked against a tolerance window to block replays.

Why does my computed HMAC not match the webhook signature?

In nearly every case: you hashed different bytes than the sender signed. The classic causes are re-serialising the JSON (body-parser middleware reformats whitespace and key order, so sign the raw body, not JSON.stringify(req.body)), a charset or compression middleware altering the payload, comparing hex against Base64, a truncated secret with trailing whitespace or a missing/extra prefix like "sha256=", and for Stripe forgetting the "timestamp.body" concatenation. Debug order: verify the secret byte for byte, then hash the captured raw body in a tool like this one, then compare encodings.

How long should an HMAC key be?

At least as long as the hash output: 32 bytes for HMAC-SHA256, 64 for HMAC-SHA512, from a cryptographic random source. RFC 2104 sets that as the minimum sensible length; shorter keys shrink the effective security to guessing the key itself. Longer than the hash block size (64 bytes for SHA-256) adds nothing, because HMAC hashes such keys down to output length first. A human-memorable passphrase is a weak HMAC key for the same reason it is a weak password: its entropy is far below its length, so generate keys, do not invent them.

What is the difference between an HMAC and a digital signature (HS256 vs RS256)?

An HMAC is symmetric: one shared key both creates and verifies, so every verifier could also forge. A digital signature (RSA, ECDSA, Ed25519) is asymmetric: the private key signs, the public key only verifies. In JWT terms, HS256 means whoever validates tokens can also mint them, fine inside one service, dangerous across trust boundaries; RS256/ES256 lets an identity provider sign while dozens of services verify with the public key and can forge nothing. Rule of thumb: same trust domain, HMAC (simpler, faster); multiple parties or third-party verification, real signatures.

How do I compute an HMAC-SHA256 in Python, Node.js or with OpenSSL?

Python: hmac.new(key_bytes, message_bytes, hashlib.sha256).hexdigest(). Node.js: crypto.createHmac("sha256", key).update(message).digest("hex"). OpenSSL CLI: echo -n "message" | openssl dgst -sha256 -hmac "key". Browser: crypto.subtle.importKey("raw", keyBytes, {name:"HMAC", hash:"SHA-256"}, false, ["sign"]) followed by crypto.subtle.sign, the exact calls this page runs. Compare results with a constant-time function, not ==, and mind that all of these read the key as raw bytes: a hex- or Base64-encoded secret must be decoded first, or you are keying with the encoding.

Why must signature comparison be timing-safe?

Because == returns the moment it finds the first differing byte, and that time difference leaks. An attacker submitting guesses can measure that a signature starting with the right first byte takes fractionally longer to reject, then fix that byte and attack the next, recovering a valid MAC byte by byte instead of brute-forcing the whole thing. Real recoveries of this kind have been demonstrated over networks. Every crypto library ships the fix: crypto.timingSafeEqual in Node, hmac.compare_digest in Python, hash_equals in PHP; use those, never string equality, for anything an outsider can submit.