Hashes and HMACs
A hash such as SHA-256 turns any input into a fixed-length fingerprint. The same input always gives the same output, and a one-character change gives a completely different one. Use it to check that a file or payload hasn’t changed.
An HMAC mixes a secret key into the hash. Only someone with the key can produce the right value, so it proves who sent a message as well as that it wasn’t changed. Stripe, GitHub, Shopify and Slack all sign webhooks this way.
Debugging a webhook signature
- Turn on HMAC and paste the signing secret from the provider’s dashboard.
- Paste the raw request body, exactly as received. Parsing the JSON and serialising it again changes spacing and key order, and the signature will no longer match. This is the most common cause of failed verification.
- Pick the encoding the provider uses (GitHub and Stripe use hex, Shopify uses Base64).
- Paste the signature from the header into the compare box. A
sha256=prefix is ignored.
Some providers sign more than the body. Stripe signs {timestamp}.{body}, and Slack signs v0:{timestamp}:{body}. Build that exact string before hashing.
Which algorithm?
Use SHA-256 unless something else requires otherwise. SHA-1 is broken for collision resistance and should only be used to talk to legacy systems (HMAC-SHA1 itself is still considered safe). MD5 isn’t offered because browsers don’t include it, and it shouldn’t be used for anything security-related.
Don’t hash passwords with SHA-256. It is designed to be fast, which makes brute force cheap. Use Argon2id, scrypt or bcrypt.
In code
// Node.js
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
createHash("sha256").update(body).digest("hex");
const sig = createHmac("sha256", secret).update(rawBody).digest("hex");
timingSafeEqual(Buffer.from(sig), Buffer.from(received)); // compare in constant time
// Java
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(UTF_8), "HmacSHA256"));
HexFormat.of().formatHex(mac.doFinal(body.getBytes(UTF_8)));
// Python
import hmac, hashlib
hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
# Shell
printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" Note the printf '%s': echo adds a trailing newline, which changes the hash.