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:X-Orbit-Signature— canonical, signed with your current secret. Check this first.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.X-Devotel-Signature— legacy back-compat header that carries one or twov1=candidates in a single header (new then previous). Old deliveries queued before the canonical headers existed carry only this one.
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 viaPOST /api/v1/webhooksor after a rotation viaPOST /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.
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 returnstrue (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
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-Signatureagainst the current secret. - If that fails and
X-Orbit-Signature-Nextis present, try it against the previous secret. - Accept if either matches.
X-Orbit-Signature-Next. To verify against both secrets, keep both in your config and try each:
X-Orbit-Signature-Next and the previous secret retires. Remove it from your config.
Troubleshooting
If a delivery you believe is valid fails:- Raw body mutated — a body parser ran before verification (see Step 1). See Troubleshooting: signature failures section 1 and 5.
- Wrong secret — a masked
whsec_********...<last4>preview copied in, or a rotation that didn’t reach your code. See Webhook security: signing secret. - Stale timestamp — your server clock is skewed by more than 5 minutes. See Troubleshooting: signature failures section 3.
- Missing header — check the fallback order (canonical → next → legacy). Old queued deliveries may only have
X-Devotel-Signature.
Continue
- Webhook security — full protocol spec and all header details
- Build a durable webhook consumer — queue, dedupe, ack-fast pattern
- Troubleshooting: signature failures — complete failure catalog
- Webhook events — event catalog