Generate and verify HMAC webhook signatures for Stripe, GitHub, Shopify, Slack, Meshes, and any custom provider. HMAC-SHA256 and HMAC-SHA512. Hex or base64. Everything runs in your browser.
Signs `{timestamp}.{body}` with HMAC-SHA256 in hex. Header is `Stripe-Signature: t={ts},v1={sig}`.
Included in the signed payload as `{t}.{body}` and sent as `t=` in the header.
Use the exact bytes the receiver will see. Parsing and re-serializing JSON often reorders keys and breaks signatures.
An HMAC is a keyed-hash message authentication code: a hash computed over a message using a shared secret, so the receiver can confirm both integrity and origin. This tool computes and verifies those signatures in your browser using the Web Crypto API, with provider presets for the transformations Stripe, GitHub, Shopify, Slack, and Meshes apply before signing. No payload, secret, or signature is ever sent to a server.
Two parties — the sender and the receiver — agree on a shared secret out of band, typically by creating a webhook endpoint in a dashboard and copying the generated signing secret into the receiver's configuration. Neither side ever transmits the secret over the wire after that point.
When the sender is ready to deliver a message, it computesHMAC(secret, signed_payload)and attaches the resulting digest to the request as a header value. The signed payload is usually the raw request body, but some providers wrap it first — Stripe signs {timestamp}.{body} and Slack signs v0:{timestamp}:{body} so a captured request cannot be replayed later.
On the receiving side, your server reads the raw bytes of the request body, applies the same pre-signing transformation, computes its own HMAC with its copy of the secret, and compares the two digests using a constant-time compare. If the digests match, the request is authentic and has not been altered. If they differ — by even one byte — the request is rejected.
Because HMAC is symmetric, it proves the message came from someone who holds the secret. It does not prove which party signed it, which is why HMAC secrets should be treated with the same care as API keys: rotate them on a schedule, store them in a secret manager, and never log them.
All three examples read the raw request body, recompute the HMAC-SHA256 digest, and compare it to the received signature in constant time. A non-timing-safe compare such as == or === leaks information about the expected signature through timing side channels — always use the platform's constant-time helper.
import crypto from 'node:crypto';
// Sign the raw bytes of the request body — never the parsed JSON.
export function verifyWebhookSignature(params: {
body: string;
secret: string;
receivedHex: string;
}): boolean {
const expected = crypto
.createHmac('sha256', params.secret)
.update(params.body, 'utf8')
.digest('hex');
const expectedBuf = Buffer.from(expected, 'utf8');
const receivedBuf = Buffer.from(params.receivedHex, 'utf8');
if (expectedBuf.length !== receivedBuf.length) {
return false;
}
// Constant-time compare. Never use `===` or `==` on signatures.
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}import hmac
import hashlib
def verify_webhook_signature(body: bytes, secret: str, received_hex: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
msg=body,
digestmod=hashlib.sha256,
).hexdigest()
# hmac.compare_digest is constant-time. Never use `==` on signatures.
return hmac.compare_digest(expected, received_hex)
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
// VerifyWebhookSignature recomputes an HMAC-SHA256 over body with secret and
// compares it to receivedHex in constant time.
func VerifyWebhookSignature(body []byte, secret string, receivedHex string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
// hmac.Equal is constant-time. Never use == on signatures.
return hmac.Equal([]byte(expected), []byte(receivedHex))
}
await request.body(); in Go, do the HMAC before json.Unmarshal.\n to \r\n. Either will change the digest. If you cannot find the difference, hash the body with SHA-256 on both sides and compare before you worry about HMAC.crypto.timingSafeEqual, hmac.compare_digest, or hmac.Equal.alg header or similar field — that lets an attacker pick a weaker algorithm if one is ever introduced.HMAC-SHA256 is a keyed-hash message authentication code built on the SHA-256 hash function and a shared secret. Given the same secret and message, two parties can independently compute the same 32-byte digest. The receiver recomputes the digest from the request body and its copy of the secret, then compares it to the signature in the request header. If the digests match, the request was produced by someone who knows the secret and the body has not been altered in transit.
No. HMAC is symmetric — sender and receiver share the same secret, and either side can produce a valid signature. A digital signature like RSA or Ed25519 is asymmetric: the private key signs and the public key verifies, so only the holder of the private key could have signed. HMAC is faster and simpler, which is why most webhook providers use it, but it cannot prove which side signed a given request.
At least 32 bytes of cryptographically random data. NIST SP 800-107 recommends a key length equal to the hash output size, so 32 bytes for SHA-256 and 64 bytes for SHA-512. Generate it from a CSPRNG, store it alongside your other secrets, and rotate it when an engineer with access leaves or when you suspect it has been exposed.
Not by itself. An HMAC proves the body has not changed, but nothing stops an attacker from capturing a signed request and resending it later. Providers address this by putting a timestamp inside the signed payload — Stripe signs `{timestamp}.{body}`, Slack signs `v0:{timestamp}:{body}` — and receivers reject signatures whose timestamp is more than a few minutes old. If you are designing your own signing scheme, include a timestamp and a short tolerance window.
Stripe includes the timestamp in the signed payload to give you built-in replay protection. GitHub signs only the raw body and relies on TLS plus a short delivery window. Shopify signs the raw body too but emits the digest in base64 instead of hex. The math is identical — the difference is the pre-signing transformation and the header format. That is why a generic HMAC calculator gives the right digest but the wrong answer: it does not know which bytes the provider actually signed.
Almost always a byte-level payload mismatch. Common causes: your framework parsed the JSON body and re-serialized it with different key ordering or whitespace; a proxy added or stripped a trailing newline; the body was decoded as UTF-16 instead of UTF-8; or the preset adds a timestamp prefix you are not including. Sign against the raw bytes you received on the wire, not a parsed object.
Always use a constant-time compare such as `crypto.timingSafeEqual` in Node.js, `hmac.compare_digest` in Python, or `hmac.Equal` in Go. A normal `==` compare can leak information about the expected signature through timing differences on each byte, which lets an attacker recover the signature one character at a time. The cost of a constant-time compare is trivial; the risk of skipping it is not.
No. Every HMAC computation runs in your browser through the Web Crypto API. There is no server action, no API call, no analytics event tied to the payload or secret, and nothing written to localStorage. You can verify this by opening your browser devtools and watching the network tab while you generate a signature — nothing leaves the page.
About Meshes
Meshes can sign outbound webhooks with HMAC-SHA256 when you enable it on the destination, and handles key rotation, per-workspace credentials, and delivery retries so your customers' endpoints can verify every request without you owning the plumbing.
Docs