HMAC Signature Mismatch in API Requests
💡An HMAC signature mismatch means the computed signature does not match the expected value. The most common causes are using parsed JSON instead of the raw request body, key encoding differences (treating a hex string as raw bytes), algorithm mismatch, or comparing hex output to Base64 output. Always compute HMAC over raw bytes and compare in the same encoding.
Quick Diagnosis
If you see “HMAC signature mismatch in webhook verification” → it means the computed signature does not match the header value sent by the provider → do this: confirm you are using the raw request body bytes, not a parsed JSON object, for HMAC input
If you see “signature matches locally but fails on server” → it means the message format or key encoding differs between environments → do this: log the exact message string and key bytes on both sides before computing
If you see “HMAC-SHA256 vs HMAC-SHA1 mismatch” → it means sender and receiver are using different hash algorithms → do this: check the API documentation for the required algorithm and match it exactly
If you see “hex vs Base64 output comparison failure” → it means both sides computed the same HMAC but encoded the output differently → do this: compare in the same format — convert both to hex or both to Base64 before comparing
Common Causes and Fixes
Webhook: parsing body before HMAC
❌ Wrong
// Wrong: computing HMAC on parsed JSON (re-serialized)
app.use(express.json());
app.post("/webhook", (req, res) => {
const sig = hmac(JSON.stringify(req.body), secret);
// → mismatch: key order and whitespace differ✅ Fixed
// Correct: use raw body bytes
app.use(express.raw({ type: "application/json" }));
app.post("/webhook", (req, res) => {
const sig = hmac(req.body, secret); // Buffer, not string
});Re-serializing parsed JSON changes whitespace and key order. Always compute HMAC on the raw request bytes.
Key encoding mismatch
❌ Wrong
// Sender uses hex-encoded key, receiver uses raw string
const sig = createHmac("sha256", "abcdef1234").update(msg).digest("hex");
// Receiver:
const sig2 = createHmac("sha256", Buffer.from("abcdef1234", "hex")).update(msg).digest("hex");
// sig !== sig2✅ Fixed
// Match key encoding on both sides
// If the secret is documented as hex, decode it:
const key = Buffer.from(process.env.HMAC_SECRET, "hex");
const sig = createHmac("sha256", key).update(msg).digest("hex");Using a hex string as a raw key feeds different bytes than decoding the same hex to a buffer.
Generate HMAC Signatures Online
Compute HMAC-SHA256, HMAC-SHA1, and other variants with your message and secret key to verify your implementation produces the expected signature.
Debugging Checklist
- ✓Use raw request body bytes — not parsed and re-serialized JSON
- ✓Confirm algorithm matches on both sides (SHA-1, SHA-256, SHA-512)
- ✓Check key encoding: raw string, hex-decoded bytes, or Base64-decoded bytes
- ✓Compare signatures in the same output format (both hex or both Base64)
- ✓Use constant-time comparison to prevent timing attacks
- ✓Log the exact message length and key length on both sides to catch invisible differences
Related Guides
Frequently Asked Questions
Why does my HMAC signature not match?
The most common causes are computing HMAC on parsed/re-serialized JSON instead of raw bytes, key encoding differences (hex string vs decoded bytes), and algorithm mismatch (SHA-1 vs SHA-256).
How do I verify a webhook HMAC signature?
Compute HMAC over the raw request body bytes (not parsed JSON) using the shared secret, then compare the result to the signature in the request header using a constant-time comparison.
Should I compare HMAC signatures with === ?
No. Use a constant-time comparison function (like crypto.timingSafeEqual in Node.js) to prevent timing attacks that could leak information about the expected signature.
All tools run in your browser. Your data never leaves your device.