API Security

Verify Webhook Signatures Locally Before You Trust the Payload

Anyone can POST to your webhook URL. Signatures are how you tell a real event from a forged one. Here’s a local habit for checking HMAC headers.

Part of the Developer Privacy Workflows series.

Your webhook endpoint is a public door with a weird doorbell. Vendors promise “we sign every request.” Attackers can still knock. If your handler trusts JSON because it arrived on the right path, you’re one forged POST away from bad state.

Signature verification is the boring fix. Doing the first few checks locally - with real headers and a sample body - stops you from debugging production with guesswork, and keeps signing secrets out of disposable cloud sandboxes.

What the signature actually proves

Most providers compute an HMAC over the raw body (sometimes plus a timestamp) using a shared secret, then send the digest in a header. If you recompute the same digest and it matches, the body wasn’t altered in transit by someone who lacked the secret. It does not prove the event is fresh forever - that’s why timestamps and replay windows matter.

A local verify loop

  1. Grab a sample payload and the signature header from docs or a test event (never from a production incident Slack dump if it still has live secrets).
  2. Open Webhook HMAC and pick the vendor preset when one exists, or mirror the documented algorithm manually.
  3. Confirm the raw body bytes match what the vendor signed - pretty-printed JSON will fail even when the secret is right.
  4. Cross-check digests with HMAC Generator if you need a second opinion on the math.
  5. Fire synthetic events via Webhook Payload Simulator against a local listener before you point production traffic at a new handler.

Gotchas that waste afternoons

  • Parsed then re-serialized bodies. Sign the raw request buffer, not JSON.stringify(req.body) after Express parsed it.
  • Charset and line endings. Copy-paste from email can change bytes.
  • Multiple signatures. Some providers rotate secrets and send several digests. Accept if any valid secret matches.
  • Ignoring timestamps. Without a skew window, a captured request can be replayed later.

Handler checklist before go-live

  • Reject when the signature header is missing.
  • Use a constant-time compare for digests.
  • Enforce a timestamp tolerance (often five minutes - follow the vendor).
  • Log failures with request ids, not with full payloads that contain PII.
  • Rotate secrets on a schedule; keep the previous secret briefly during rollout.

Why “local” shows up in a security article

Signing secrets and production payloads don’t belong in anonymous online HMAC toys. Same rule as JWT debugging and HAR inspection: keep sensitive material on your machine. If you need a receipt for a client demo of local verification, pair the workflow with the ideas in privacy receipts.

Vendor differences to respect

Stripe-style signatures often include a timestamp and one or more v1 digests. GitHub uses a sha256 HMAC header with a secret you configure on the webhook. Slack signs basestring with a version prefix. Copying one vendor’s code onto another’s traffic fails in confusing ways - always match the doc for that provider.

Build a small matrix in your repo: vendor → header name → canonical string → secret location. Onboarding engineers should read that before touching handlers.

Test doubles that don’t leak

Commit fixture payloads with fake secrets used only in tests. Never record production signatures in VCS. CI should run verification unit tests against those fixtures so regressions show up before deploy.

For manual exploration, local simulators beat sharing secrets into SaaS request bins. Rotate any secret that ever appeared in a screenshot.

Ops after verify succeeds

Verification is necessary, not sufficient. Still enforce idempotency keys where vendors send them. Still validate schema. Still rate-limit abusive clients who present garbage signatures - failed verify should be cheap and boring, not a CPU sink.

Alert on sudden spikes in signature failures; that can mean a secret rotation gone wrong or an attacker probing your endpoint.

Field notes from teams who shipped this

The pattern that keeps showing up: write the constraint first, then the steps, then the failure modes. Teams that only publish happy-path screenshots create tickets. Teams that document the ugly path create trust.

Schedule a short review ninety days after publishing. Check whether product UI names still match, whether linked tools still exist, and whether support still hears the same questions. Update the page or merge it. Standing still is how useful posts become interchangeable again.

If you adapt this article for internal wikis, keep the examples tied to your stack names. The moment you generalize back to “best practices for organizations,” you’ve started erasing the specificity that made the piece worth saving.

Rotation day playbook

Secret rotation is when verification bugs appear. Run this sequence:

  1. Generate the new secret in the vendor dashboard without disabling the old one if dual-verify is supported.
  2. Deploy handler code that accepts either secret.
  3. Flip the vendor to the new secret.
  4. Watch failure metrics for fifteen minutes.
  5. Remove the old secret from code and config.

Document who can trigger rotation. Shared Slack messages with the new secret are how you end up rotating twice.

Keep a local verify script in the repo that reads fixtures and exits non-zero on mismatch. Engineers should run it when touching canonical string construction - the usual breakage point.

Docs debt on webhooks

Update your runbook the same day you change canonical string construction. Future you will not remember whether the timestamp had a prefix. Link the vendor doc section you followed and the date you read it - vendors revise quietly.

Add a synthetic canary event in staging that fires hourly and must verify cleanly. Canaries catch rotation mistakes before customers do.

FAQ

Is HTTPS enough without signatures?

HTTPS protects the pipe. It doesn’t prove the sender knew your webhook secret. You want both.

Do GET callbacks need this?

Usually you’re verifying POSTs with bodies. Challenge/response OAuth redirects are a different pattern - don’t force HMAC where the provider uses another proof.

← All posts Browse tools