Generate an HMAC from a message and a secret key using SHA-256 or SHA-512. The usual reason to be here is a webhook that keeps failing verification: compute the expected signature and compare it with what your code produces. The Web Crypto API signs in this tab, so the key is never sent anywhere.
Start with HMAC-SHA256. Stripe and GitHub webhooks, AWS Signature V4 and the HS256 algorithm in JWT all use it.
The same construction over SHA-512. Choose it when the other side specifies it; it adds no practical safety over SHA-256.
HMAC-SHA256:
d93591bdf7a9d2d6a362547a0701c6010d293125211915995537905132z8787f Stripe, GitHub and most payment providers sign requests with HMAC-SHA256. Paste the raw request body and your signing secret here: if the result matches the header, your code is wrong; if it does not, check the secret.
Run the same message and key through this page and through your code, then compare the digests. A mismatch points at encoding, whitespace or the key, before you start debugging the network.
const { createHmac, timingSafeEqual } = require("node:crypto");
const secret = "my-secret-key";
const message = "hello world";
const hex = createHmac("sha256", secret).update(message).digest("hex");
console.log(hex);
// 90eb182d8396f16d4341d582047f45c0a97d73388c5377d9ced478a2212295ad
// Compare signatures in constant time, never with ===
function verify(received, expected) {
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
console.log(verify(hex, hex)); // true
// Browser: Web Crypto
async function hmacSha256(key, text) {
const enc = new TextEncoder();
const k = await crypto.subtle.importKey("raw", enc.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", k, enc.encode(text));
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
hmacSha256(secret, message).then(console.log); <?php
$secret = 'my-secret-key';
$message = 'hello world';
$hex = hash_hmac('sha256', $message, $secret);
echo $hex, PHP_EOL;
// Base64 output (what many webhook providers send)
echo base64_encode(hash_hmac('sha256', $message, $secret, true)), PHP_EOL;
// Verify a signature from a header in constant time
$received = $hex;
var_dump(hash_equals($hex, $received)); // bool(true) For webhooks, sign the raw request body from php://input, not the decoded array, or the bytes will not match.
import base64
import hashlib
import hmac
secret = b"my-secret-key"
message = b"hello world"
digest = hmac.new(secret, message, hashlib.sha256)
print(digest.hexdigest())
print(base64.b64encode(digest.digest()).decode())
# Verify a webhook signature in constant time
received = digest.hexdigest()
print(hmac.compare_digest(received, digest.hexdigest())) # True
# From the shell:
# printf 'hello world' | openssl dgst -sha256 -hmac 'my-secret-key' Do not name your script hmac.py; it would shadow the standard library module and fail with a circular import error.
HMAC mixes a secret key into a hash so that only someone holding the key can produce or check the tag. A bare hash cannot do that: with plain SHA-256, anyone who sees the message can compute the digest, and length-extension attacks let them extend it. HMAC closes both holes (RFC 2104 has the construction). The tag is the same size as the underlying hash, so 32 bytes for HMAC-SHA256, shown as 64 hex characters.
HMAC-SHA256 is the common choice: webhooks from GitHub, Stripe and Shopify, AWS Signature v4, JWT HS256. HMAC-SHA1 is still sound as a MAC, because HMAC does not rely on collision resistance. However, pick SHA-256 for anything new, because security scanners and auditors flag SHA-1 regardless of how it is used. Use a random key at least as long as the hash output.
This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.
A hash depends only on the message. An HMAC also depends on a secret key, so only someone who knows the key can produce or verify it. This is why webhooks and API request signing use HMAC.
HMAC with SHA-1, SHA-256, SHA-384 or SHA-512. HMAC-SHA256 is the most common choice.
Webhook signatures (Stripe, GitHub, Shopify, Slack), API request signing (AWS Signature v4), JWTs with HS256, and cookie or session integrity checks.
Yes. Paste the raw request body as the message and your signing secret as the key, then compare the output with the signature header the provider sent.
No. The Web Crypto API computes the HMAC in your browser tab, and the page sends nothing.
For a digest without a key, use the SHA generator. HMAC-SHA256 also signs most JWTs, so the JWT decoder and JWT generator cover the token side.