How to Secure a Webhook Endpoint (HMAC + Replay Protection)

An open webhook accepts anything. Secure it like Stripe and GitHub: verify an HMAC signature over the raw body, reject old timestamps to block replays, and compare signatures in constant time. Here's how.

Security Engineersecuritywebhookshmac

Why webhooks need this

A webhook URL is public, so anyone can POST to it. Without verification, an attacker can forge events (fake a payment, trigger actions). The standard defense (Stripe, GitHub, Shopify all do it): the sender signs each request with a shared secret; you verify the signature before trusting the payload.

The signature header

Senders include a header like:

X-Webhook-Signature: t=1700000000,v1=5257a869e7 ...

t is the timestamp; v1 is HMAC_SHA256(secret, "<t>.<raw_body>"). You recompute it and compare.

Verify it

import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts["t"], parts["v1"]

    # 1. Replay protection: reject stamps outside the tolerance window
    if abs(time.time() - int(t)) > tolerance:
        return False

    # 2. Recompute the HMAC over "<t>.<raw_body>"
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()

    # 3. Constant-time compare (no early-exit timing leak)
    return hmac.compare_digest(expected, sig)

The three things that make it secure

  1. Sign the raw body, not the parsed JSON. Re-serializing changes bytes (key order, spacing) and breaks the signature - verify against the exact bytes received.
  2. Timestamp + tolerance = replay protection. Without checking t, an attacker can resend a valid captured request forever. Reject anything older than ~5 minutes.
  3. Constant-time comparison. hmac.compare_digest (not ==) so an attacker can't learn the signature byte-by-byte from response timing.

Operational notes

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

How do I verify a webhook signature?

Recompute HMAC-SHA256 over the signed payload (usually timestamp.raw_body) with the shared secret and compare it to the signature header using a constant-time comparison. Reject if they don't match.

How do I stop webhook replay attacks?

Include and verify a timestamp in the signed payload, and reject requests whose timestamp is outside a tolerance window (e.g. older than 5 minutes), so a captured valid request can't be resent indefinitely.

Why verify the raw body instead of parsed JSON?

Parsing and re-serializing JSON changes the bytes (key order, whitespace), which breaks the HMAC. You must compute the signature over the exact raw bytes the sender signed.

What is HMAC authentication for a webhook?

HMAC webhook authentication has the sender sign each request body with a shared secret and send the signature in a header. The receiver recomputes the HMAC over the raw body and compares - if they match, the payload is authentic and untampered.

Keep learning

Close a SQL Injection in a Search EndpointSecurity projectNeutralize a Stored XSS in CommentsSecurity projectRemove Hardcoded Credentials From Source CodeSecurity projectSecurity roadmapStep by step to hiredSecurity interview questionsSTAR answersAll Security projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →