Security6 min read

HMAC Explained: How to Sign API Requests and Verify Webhook Signatures

Learn what HMAC is, how HMAC-SHA256 works, how to sign HTTP requests with a secret key, and how to verify webhook signatures from services like Stripe and GitHub.

Try the free online tool mentioned in this guide:HMAC Generator

What is HMAC?

HMAC (Hash-based Message Authentication Code) is a mechanism for verifying both the integrity and authenticity of a message using a shared secret key and a hash function.

Unlike a plain hash (SHA-256 of the message), an HMAC binds the hash to a secret key. Only someone who knows the secret can produce a valid HMAC for a given message. This makes it suitable for API request signing, webhook verification, and session tokens.

How HMAC works

HMAC is defined as:

HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message))

Where H is the hash function (SHA-256, SHA-512, etc.), opad and ipad are fixed padding constants, and ⊕ is XOR. In practice you never implement this yourself — use a standard crypto library.

typescript
// Node.js
import { createHmac } from "crypto"

const secret = "my-webhook-secret"
const payload = JSON.stringify({ event: "payment.completed", amount: 4999 })

const signature = createHmac("sha256", secret)
  .update(payload)
  .digest("hex")

console.log(signature)
// "a3b4c5d6..." — 64 hex chars (256 bits)

// Python
import hmac, hashlib

signature = hmac.new(
    key=b"my-webhook-secret",
    msg=payload.encode("utf-8"),
    digestmod=hashlib.sha256
).hexdigest()

Verifying webhook signatures

Major platforms (Stripe, GitHub, Shopify, Twilio) sign webhook payloads with HMAC-SHA256 so you can verify the request came from them and was not tampered with.

The general pattern: 1. Receive the webhook request. 2. Read the raw body (do not parse JSON yet — parsing may reorder keys). 3. Compute HMAC of the raw body using your webhook secret. 4. Compare against the signature in the request header using a constant-time comparison.

typescript
// Stripe webhook verification (Node.js/Express)
import Stripe from "stripe"

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["stripe-signature"]

  try {
    const event = stripe.webhooks.constructEvent(
      req.body,           // raw Buffer — not parsed JSON
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    )
    // Handle event.type
    res.json({ received: true })
  } catch (err) {
    res.status(400).send("Webhook signature verification failed")
  }
})

// GitHub webhook verification
import { timingSafeEqual } from "crypto"

function verifyGitHubSignature(payload: Buffer, sigHeader: string, secret: string) {
  const expected = "sha256=" + createHmac("sha256", secret).update(payload).digest("hex")
  return timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader))
}

Why constant-time comparison matters

Never use === to compare HMAC signatures. String comparison in most languages short-circuits — it returns false as soon as the first mismatched byte is found. This timing difference leaks information about how much of the signature is correct, enabling a timing attack.

Always use a constant-time comparison function: crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python, or equivalent.

typescript
// ❌ Vulnerable to timing attack
if (computedSignature === receivedSignature) { ... }

// ✅ Constant-time comparison (Node.js)
import { timingSafeEqual } from "crypto"
const a = Buffer.from(computedSignature, "hex")
const b = Buffer.from(receivedSignature, "hex")
if (a.length === b.length && timingSafeEqual(a, b)) { ... }

# Python
import hmac
if hmac.compare_digest(computed_sig, received_sig):
    ...

HMAC vs plain hash vs digital signature

Plain hash (SHA-256 of message): anyone can compute it — no authentication, only integrity.

HMAC (SHA-256 with shared secret): proves the signer knows the secret — integrity + authenticity. Requires sharing the secret with all verifiers.

Digital signature (RSA, ECDSA): asymmetric — private key signs, public key verifies. No secret sharing needed. Better for public verification (JWT RS256, code signing, TLS certificates). More computationally expensive than HMAC.

Frequently asked questions

What is the difference between HMAC-SHA256 and SHA-256?

SHA-256 is a hash function — it produces a digest of the message but anyone can compute it. HMAC-SHA256 mixes a secret key into the hash using a specific construction, so only holders of the secret can produce a valid HMAC.

Can I use HMAC for API key authentication?

Yes, and many APIs do. The client signs each request body or canonical string with a shared secret (API secret key). The server recomputes the HMAC and compares. AWS Signature Version 4 uses HMAC-SHA256 in this way.

Is HMAC-SHA1 still secure?

SHA-1 is broken as a standalone hash for collision resistance. However, HMAC-SHA1 is still considered secure for message authentication — the HMAC construction prevents the collision attacks that break raw SHA-1. That said, prefer HMAC-SHA256 for new implementations.

How do I test HMAC signatures during development?

Use MyDevTools HMAC Generator to compute HMAC-SHA256 (or other variants) for any key and message directly in the browser, without writing a script. Useful for verifying your implementation produces the expected output.

Try HMAC Generator for free

Compute HMAC-SHA1, HMAC-SHA256, HMAC-SHA384, and HMAC-SHA512 signatures in hex or Base64 for webhook signing and API integration testing. Runs entirely in your browser. No install, no account required to try it.