Skip to main content

Verify webhook signatures in Python and Go

The Webhook security page covers the full signature protocol — header formats, the signed string, and timing-safe comparison. But its polished examples are mostly Node.js. If your backend is Python or Go with no Node in the request path, you need a canonical verifier you can paste directly into your codebase and run with just the standard library. This guide gives you exactly that: one complete, copy-pasteable HMAC verifier per language. Each handles the header trio (X-Orbit-Signature first, X-Orbit-Signature-Next during a rotation, X-Devotel-Signature as the legacy fallback), guards the replay window, and compares with a timing-safe function. Each also ships with a negative test that proves the verifier refuses a wrong secret. For the protocol details — what each header carries, the <t>.<raw_body> signed string, rotation grace windows — see Webhook security. For a full receiver walkthrough with a queue and retry logic, see Build a durable webhook consumer.

What you’re verifying

Every delivery carries the signature in up to three headers. Your verifier should read them in this order and accept if any matches:
  1. X-Orbit-Signature — canonical, signed with your current secret. Check this first.
  2. X-Orbit-Signature-Next — only present during a key-rotation grace window, signed with your previous secret. If you hold both secrets during a rotation, try this after the first fails.
  3. X-Devotel-Signature — legacy back-compat header that carries one or two v1= candidates in a single header (new then previous). Old deliveries queued before the canonical headers existed carry only this one.
Each header uses the same encoding: t=<unix_seconds>,v1=<hex> with an optional second v1= in the legacy header. The signed string is <t>.<raw_body> — the literal timestamp from the header, a dot, then the exact bytes of the request body.

Prerequisites

  • Your webhook signing secret (whsec_...), captured at endpoint creation via POST /api/v1/webhooks or after a rotation via POST /api/v1/webhooks/{id}/rotate-secret. If you only have a masked preview (whsec_********...<last4>), rotate to recover a usable secret — the preview never validates.
  • A webhook endpoint that receives the raw request body before any JSON parser or middleware touches it (see Step 1 below).

Step 1 — Preserve the raw body

HMAC is computed over the exact bytes Orbit POSTed. If your framework deserializes the body first (request.get_json(), json.NewDecoder(r.Body) on the same body stream, an auto-parsing middleware), whatever you re-serialize will differ from what was signed. Preserve the raw bytes and pass them to the verifier. Python (Flask): use request.data (bytes) — do not call request.get_json() before verification. Go (net/http): read r.Body into a []byte once, then use that buffer for both signature verification and JSON decoding.

Step 2 — Python verifier (stdlib hmac + hashlib)

This is the complete verifier. Drop it into any Python codebase — no external dependencies.
Flask handler:

Step 3 — Go verifier (crypto/hmac)

Standard library only. Works with net/http, Gin, Echo, or any framework.
net/http handler:

Step 4 — Negative test: prove reject-on-wrong-secret

A verifier that always returns true (or never actually compares) is worse than no verifier. Test the negative path once in unit tests and once against a live delivery with the wrong secret.

Python pytest

Go testing

Run these before deploying. If a “reject” test passes (the verifier returns true on garbage), you have a logic bug — fix it before shipping.

Rotation: handling X-Orbit-Signature-Next

During a secret rotation grace window (7 days), every delivery carries both the canonical header (signed with the new secret) and X-Orbit-Signature-Next (signed with the previous secret). Your verifier should keep both secrets configured and try both headers:
  • Try X-Orbit-Signature against the current secret.
  • If that fails and X-Orbit-Signature-Next is present, try it against the previous secret.
  • Accept if either matches.
The header-order code in the handlers above already falls back to X-Orbit-Signature-Next. To verify against both secrets, keep both in your config and try each:
After the grace window ends, Orbit stops sending X-Orbit-Signature-Next and the previous secret retires. Remove it from your config.

Troubleshooting

If a delivery you believe is valid fails:
  1. Raw body mutated — a body parser ran before verification (see Step 1). See Troubleshooting: signature failures section 1 and 5.
  2. Wrong secret — a masked whsec_********...<last4> preview copied in, or a rotation that didn’t reach your code. See Webhook security: signing secret.
  3. Stale timestamp — your server clock is skewed by more than 5 minutes. See Troubleshooting: signature failures section 3.
  4. Missing header — check the fallback order (canonical → next → legacy). Old queued deliveries may only have X-Devotel-Signature.
For the full failure catalog mapped to error codes, see Troubleshooting: signature verification failures.

Continue