python 98 lines · 3 tabs

Verify Stripe-Style Webhook HMAC Signatures Before Processing in Flask

Shared by codesnips Jul 2026
3 tabs
import hashlib
import hmac
import time


class SignatureError(Exception):
    pass


def parse_signature_header(header):
    parts = {}
    for item in header.split(","):
        key, _, value = item.partition("=")
        parts.setdefault(key.strip(), value.strip())
    if "t" not in parts or "v1" not in parts:
        raise SignatureError("malformed signature header")
    return parts["t"], parts["v1"]


def verify_signature(payload, header, secret, tolerance=300):
    timestamp, provided = parse_signature_header(header)

    try:
        age = time.time() - int(timestamp)
    except ValueError:
        raise SignatureError("invalid timestamp")
    if age > tolerance:
        raise SignatureError("timestamp outside tolerance window")

    signed = b"%s.%s" % (timestamp.encode("utf-8"), payload)
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, provided):
        raise SignatureError("signature mismatch")
    return True
3 files · python Explain with highlit

A webhook endpoint is a public HTTP handler that acts on events sent by a third party, so it must never trust the request body until the sender is proven authentic. This snippet shows the Stripe-style approach: the sender signs the raw payload with a shared secret and includes a timestamp, and the receiver recomputes the HMAC to confirm both authenticity and freshness before doing any work.

In signing.py, parse_signature_header splits the Stripe-Signature header into its t (timestamp) and v1 (signature) components. verify_signature reconstructs the signed payload as "{timestamp}.{body}", computes an HMAC-SHA256 over it with the endpoint secret, and compares it against the header value using hmac.compare_digest. That constant-time comparison matters: a naive == can leak how many leading bytes matched through timing, which is enough to forge a signature over many attempts. The function also enforces a tolerance window so a captured request cannot be replayed hours later — an old timestamp is rejected even if the signature itself is valid.

A critical detail is that verification runs against the exact bytes received. In webhook.py the handler reads request.get_data() rather than request.json, because re-serializing parsed JSON would reorder keys or change whitespace and break the HMAC. Parsing happens only after the signature checks out.

The webhook.py blueprint wires this together: it pulls the secret from config, calls verify_signature, and returns 400 on a bad or stale signature so the sender knows to retry or stop. Only then does it parse the event and dispatch on event['type']. It also guards against duplicate delivery via already_processed(event['id']), since webhook providers guarantee at-least-once delivery and will resend on timeouts. Making handlers idempotent by event id is essential to avoid double-charging or duplicate fulfillment.

app.py shows the minimal factory that registers the blueprint and loads the secret from the environment. Together the files separate pure crypto (signing.py) from transport concerns (webhook.py), which keeps the verification logic unit-testable without spinning up Flask. The pattern generalizes to any signed-webhook provider that uses a timestamped HMAC scheme.


Related snips

Share this code

Here's the card — post it anywhere.

Verify Stripe-Style Webhook HMAC Signatures Before Processing in Flask — share card
Link copied