Overview
Treat every inbound webhook as untrusted input until its HMAC signature is verified; a public endpoint that processes unsigned payloads is a remote command interface for anyone who finds the URL. This page is the house standard for receiving webhooks from any provider. Provider-specific details (Stripe’s Stripe-Signature header and SDK helpers) live on payments-stripe.
Verify the HMAC signature over the raw request body
Compute the HMAC over the exact bytes you received, before any parsing. Providers sign the raw payload, so re-serializing JSON changes whitespace, key order, or Unicode and breaks verification (Stripe). Capture the raw body in your framework before middleware deserializes it, compare with a constant-time function, and reject on mismatch. Non-constant comparison leaks the correct signature byte by byte and turns the endpoint into a signing oracle (Standard Webhooks).
import hashlib
import hmac
import time
def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
# Reject events outside a 5-minute window to blunt replay attacks.
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
# Constant-time compare; never use ==.
return hmac.compare_digest(expected, signature)Python’s hmac.compare_digest is the constant-time primitive; in Node use crypto.timingSafeEqual over equal-length buffers. See python-security for the broader rules.
Enforce a timestamp tolerance window
Bind the signature to a signed timestamp and reject events older than the tolerance. Stripe libraries default to a 5-minute window (Stripe), and Standard Webhooks requires the same check against the webhook-timestamp header. Because the timestamp is inside the signed payload, an attacker cannot adjust it without invalidating the signature, so a captured-and-replayed request expires.
Dedup by event id
Providers deliver at least once, so the same event id arrives more than once after retries (GitHub). Store each processed id (webhook-id, X-GitHub-Delivery, or the event id) with a unique constraint or in Redis, and ignore repeats. Standard Webhooks recommends the webhook-id header as the idempotency key. Record the id in the same transaction as the side effect so a crash between processing and recording does not double-charge or double-send.
Respond 2xx fast, process asynchronously
Acknowledge with a 2xx immediately, then do work in a background job. Providers time out quickly (GitHub at 10 seconds) and a slow handler triggers retries, duplicate deliveries, and eventual endpoint disabling. Verify the signature synchronously, enqueue the validated payload, and return. See fastapi for background-task patterns.
Make handlers safe to re-run
Providers retry with exponential backoff on timeouts and 5xx responses, so assume any handler runs more than once. Combine the idempotency key with upserts and conditional writes instead of blind inserts. Return 5xx only for transient failures you want retried; return 2xx for events you have accepted but cannot act on, so the provider stops resending.
Support multiple signing secrets during rotation
Keep two or more active secrets during a rollover and accept a payload if it verifies against any of them. In-flight deliveries continue using the previous secret until the provider picks up the new one. Verify against the new secret first, fall back to the old, and retire the old secret only after deliveries stop matching it. Store secrets in your secret manager, never in code; see secrets-and-env. Log verification failures to error-tracking to catch a botched rotation early.
Related
- payments-stripe - Stripe-specific signature header and SDK verification.
- secrets-and-env - Where to store and rotate signing secrets.
- auth-sessions - Authenticating the rest of your API surface.
- fastapi - Background tasks for async webhook processing.
- error-tracking - Alerting on verification and processing failures.
- python-security - Constant-time compares and input handling.