Your webhook endpoint is on the public internet, and anyone who finds the URL can POST to it. If the handler creates orders, moves money, or changes account state, an unverified endpoint is a remote-control button for attackers. Security here is about proving each request genuinely came from the provider — and refusing everything that cannot.
Verify the signature
Reputable providers sign each webhook: they compute an HMAC of the raw request body with a shared secret and put it in a header. You recompute the HMAC on the exact bytes you received and compare. If it does not match, reject with a 4xx and do nothing else. This is the single most important control — without it, nothing else matters.
import hmac, hashlib
def verify(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature) # constant-time compare
Two details people get wrong: verify against the raw body, not a re-serialised version — reparsing and re-encoding the JSON changes the bytes and breaks the signature. And use a constant-time comparison (hmac.compare_digest), because a normal string compare leaks timing information an attacker can exploit.
Block replays with a timestamp
A captured valid request can be re-sent by an attacker — a replay. Providers include a timestamp in the signed payload; reject requests whose timestamp is outside a small tolerance (say five minutes), so an old captured request is stale by the time it is replayed. Combine this with event-id dedupe and a replayed request is both stale and already-seen.
Harden the rest
- HTTPS only — a signature over a plaintext connection can still leak the payload; require TLS.
- Never trust the payload's contents blindly — validate and bound it; treat any URLs inside as untrusted to avoid SSRF, and cap the body size to blunt abuse.
- Keep secrets in a secret manager and rotate them; support two valid secrets briefly during rotation so you do not drop events.
- Return quickly and process async — a slow handler is a denial-of-service target.
Every other control is defence in depth around this one check. An endpoint that skips signature verification is trusting the entire internet to only send legitimate events. If a provider does not sign its webhooks, put them behind a shared-secret path token at minimum, and push the provider to add signing.
Secure webhook handling is a short, strict pipeline: TLS in, raw-body HMAC verification, timestamp and dedupe checks against replay, then bounded validation of the contents. Everything that fails any step is rejected before it touches your business logic — which is exactly where you want the line drawn.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.