Webhook Security Patterns for Async Proof Workflows: HMAC, Idempotency, and Replay Protection
A file gets hashed. The hash goes off to get anchored. Sometime later, a webhook arrives saying the anchor is done. That gap between "submitted" and "confirmed" is where most of the hard problems in an async proof pipeline actually live. Not the hashing. Not the anchoring. The webhook. I hit this building the anchoring flow behind ProofLedger, and it's a pattern that applies to any system where a client kicks off work and gets notified later: payment confirmations, video transcoding, background exports. If you're building or consuming webhooks for anything time-sensitive, these four problems show up in the same order every time. Verify the signature before you trust the payload A webhook endpoint is a URL on the open internet. Anyone can POST to it. If your handler reads status: "anchored" from the body and acts on it without checking where it came from, you've built an endpoint that lets a stranger fake completion events. The standard fix is HMAC-SHA256. The sender computes a signature over the raw request body using a shared secret, puts it in a header, and the receiver recomputes it and compares. import hashlib import hmac from flask import Flask , request , abort app = Flask ( __name__ ) WEBHOOK_SECRET = b " shared-secret-from-sender " def verify_signature ( payload_body : bytes , signature_header : str ) -> bool : expected = hmac . new ( WEBHOOK_SECRET , payload_body , hashlib . sha256 ). hexdigest () return hmac . compare_digest ( expected , signature_header ) @app.route ( " /webhooks/proof-status " , methods = [ " POST " ]) def handle_webhook (): signature = request . headers . get ( " X-Signature-256 " , "" ) if not verify_signature ( request . get_data (), signature ): abort ( 401 , " invalid signature " ) event = request . get_json () process_event ( event ) return "" , 204 Two details matter here and both are easy to get wrong. First, hmac.compare_digest instead of == . A regular string comparison short-circuits on the first mismatched byte, which leaks t