Skip to content
Security 7 min read

HMAC Signatures: API Authentication and Webhook Verification

Learn how HMAC-SHA256 works, how to sign and verify API requests, how webhook providers use HMAC for payload verification, and how to implement it in JavaScript and Python.

ToolsVito Team

What Is HMAC?

HMAC (Hash-based Message Authentication Code) combines a cryptographic hash function with a secret key to produce a message authentication code. It provides two guarantees:

  1. Integrity: The message hasn't been tampered with — any change to the payload changes the HMAC.
  2. Authentication: The sender knows the secret key — only parties with the key can produce a valid HMAC.
HMAC(key, message) = H((key XOR opad) || H((key XOR ipad) || message))
// H = hash function (SHA-256 in HMAC-SHA256)
// Two nested hash operations prevent length-extension attacks

HMAC in JavaScript

// Web Crypto API (browser + Node.js 15+)
async function hmacSha256(key, message) {
  const encoder = new TextEncoder();
  const cryptoKey = await crypto.subtle.importKey(
    "raw",
    encoder.encode(key),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"]
  );
  const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(message));
  return Array.from(new Uint8Array(signature))
    .map(b => b.toString(16).padStart(2, "0"))
    .join("");
}

// Node.js built-in crypto module
import { createHmac } from "crypto";
const hmac = createHmac("sha256", secret).update(payload).digest("hex");

HMAC in Python

import hmac
import hashlib

signature = hmac.new(
    key=secret.encode(),
    msg=payload.encode(),
    digestmod=hashlib.sha256
).hexdigest()

Verifying Webhook Payloads

Stripe, GitHub, Shopify, and most other platforms sign webhook payloads with HMAC-SHA256. Here is a generic verification pattern:

// Express webhook handler
import { createHmac, timingSafeEqual } from "crypto";

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-signature-sha256"];
  const expected = createHmac("sha256", process.env.WEBHOOK_SECRET)
    .update(req.body)  // raw body, not parsed JSON
    .digest("hex");

  // Use timingSafeEqual to prevent timing attacks
  const sigBuffer = Buffer.from(signature, "hex");
  const expBuffer = Buffer.from(expected, "hex");

  if (sigBuffer.length !== expBuffer.length ||
      !timingSafeEqual(sigBuffer, expBuffer)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body);
  // Process event...
  res.status(200).send("OK");
});

Platform-Specific Formats

// GitHub: "sha256=<hex>"
const [alg, sig] = req.headers["x-hub-signature-256"].split("=");
// alg === "sha256"

// Stripe: "t=timestamp,v1=<hex>"
// Verify: HMAC("sha256", secret, timestamp + "." + payload)
// Check timestamp to prevent replay attacks

// Shopify: base64-encoded HMAC in X-Shopify-Hmac-Sha256
const decoded = Buffer.from(req.headers["x-shopify-hmac-sha256"], "base64").toString("hex");

Timing-Safe Comparison

Never compare HMACs with === — JavaScript string comparison short-circuits as soon as it finds a mismatch, leaking timing information that helps attackers guess the correct signature byte by byte. Always use crypto.timingSafeEqual() (Node.js) or hmac.compare_digest() (Python).

Generate HMAC Signatures Instantly

Use ToolsVito's HMAC Generator to compute HMAC-SHA256 (and other variants) from a key and message in your browser — useful for testing webhook integrations.

Try it now — free, runs in your browser

HMAC Generator

Keyed message authentication