How to Use HMAC for API Authentication and Webhook Verification
A practical guide to HMAC-based authentication — how it works, implementing webhook signature verification, and securing API requests with HMAC-SHA256.
What is HMAC?
HMAC (Hash-based Message Authentication Code) combines a cryptographic hash function with a secret key to provide both data integrity and authentication. Unlike a plain hash, HMAC requires knowledge of the secret key to generate or verify the signature.
HMAC(key, message) = Hash((key ⊕ opad) || Hash((key ⊕ ipad) || message))
The result is a fixed-size signature that proves:
- The message has not been tampered with (integrity)
- The sender knows the secret key (authentication)
How Webhook Verification Works
When a service (Stripe, GitHub, Slack) sends a webhook to your server, it signs the payload with HMAC:
Service → Your Server:
POST /webhooks/stripe
Body: {"event": "payment.completed", "amount": 9999}
Header: Stripe-Signature: sha256=a1b2c3d4e5f6...
Your server verifies the signature:
const crypto = require("crypto");
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post("/webhooks/stripe", (req, res) => {
const signature = req.headers["stripe-signature"];
const payload = req.body; // raw body string
if (!verifyWebhook(payload, signature, process.env.STRIPE_WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
// Process the webhook...
res.json({ received: true });
});
Implementing HMAC in Different Languages
JavaScript (Node.js)
const crypto = require("crypto");
const hmac = crypto
.createHmac("sha256", "your-secret-key")
.update("message to sign")
.digest("hex");
Python
import hmac
import hashlib
signature = hmac.new(
b"your-secret-key",
b"message to sign",
hashlib.sha256
).hexdigest()
Browser (Web Crypto API)
async function generateHmac(message, secret) {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign(
"HMAC",
key,
encoder.encode(message)
);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
}
Security Best Practices
-
Use timing-safe comparison — Never use
===to compare signatures. Usecrypto.timingSafeEqual()to prevent timing attacks. -
Include a timestamp — Add a timestamp to the signed payload and reject requests older than 5 minutes to prevent replay attacks.
-
Use SHA-256 or higher — HMAC-MD5 and HMAC-SHA1 are deprecated. Use HMAC-SHA256 at minimum.
-
Rotate secrets regularly — Change webhook secrets periodically and support multiple active secrets during rotation.
-
Verify the raw body — Parse the request body BEFORE any middleware modifies it. JSON parsing can change whitespace.
Generate and verify HMAC signatures with our HMAC Generator tool.
Related Tools
- HMAC Generator — Generate HMAC-SHA256/384/512 signatures
- Hash Generator — Generate SHA-256, SHA-512 hashes
- JWT Decoder — JWTs use HMAC for HS256 signing
- Base64 Encoder — Encode signatures for transmission
- API Authentication Methods — Compare auth methods