Verify webhook signatures with the SDK, per language
Verifying a webhook signature is one SDK call in every language. What trips teams up is the wiring around it: reading the raw body before a JSON parser mutates it, reading the right header, and returning401 (not 200) on a bad signature so retries don’t ack forged traffic. This page gives you a complete, working handler per language — paste it, point it at your secret, and it runs.
The protocol itself (header formats, the <t>.<raw_body> signed string, rotation grace windows, timing-safe comparison) is specified on Webhook security. If you have a Python or Go backend and don’t want to pull in the SDK, the no-SDK verifiers do the same check with the standard library.
What every handler does
- Read the raw body — before any JSON middleware or parser runs. HMAC is over the exact bytes Orbit POSTed; a parse-and-reserialize changes them.
- Read the signature header — canonical
X-Orbit-Signaturefirst, legacyX-Devotel-Signatureas fallback (old queued deliveries may carry only the legacy one). Each carriest=<unix_ts>,v1=<hex>. - Verify — one SDK call: returns the decoded event, throws on any failure (malformed header, replay-window breach, signature mismatch, invalid JSON).
- Answer
401on failure — a forged or stale request must not get200.
Node.js (Express)
SDK:Orbit.webhooks.constructEvent(payload, signature, secret) — throws OrbitWebhookSignatureError on failure. Mount Express’s raw body parser on the webhook route only; keep your JSON parser elsewhere.
Python (Flask)
SDK:verify_webhook(payload=..., signature=..., secret=...) — keyword-only, returns the decoded event dict, raises OrbitWebhookSignatureError.
Go (net/http)
SDK: orbit.VerifyWebhook(payload, signatureHeader, secret, 0, time.Time{}) — the 0 tolerance picks the default 5-minute window; the zero time.Time{} means “now”. Returns the decoded event or a *orbit.WebhookSignatureError.
PHP
SDK:Webhooks::verify(payload: ..., signature: ..., secret: ...) — named arguments, returns the decoded event array, throws OrbitWebhookSignatureError.
Ruby (Sinatra)
SDK:OrbitSdk::Webhooks.verify(payload:, signature:, secret:) — keyword arguments, returns the decoded event hash, raises OrbitSdk::OrbitWebhookSignatureError.
Java
SDK:Webhooks.verify(payload, signature, secret) — returns the decoded event Map, throws OrbitWebhookSignatureError. The example below runs on the JDK’s built-in HttpServer; with Spring or another framework, capture the body as raw byte[]/String before any deserializer touches it.
C# (ASP.NET minimal API)
SDK:Webhooks.Verify(payload, signature, secret) — returns the decoded event JsonElement, throws OrbitWebhookSignatureError.
Rotation and troubleshooting
During a secret rotation grace window, verifyX-Orbit-Signature against the current secret and X-Orbit-Signature-Next against the previous secret — both headers arrive on every delivery until the grace window ends (7 days). The pattern per language mirrors the rotation section of the no-SDK guide.
If a valid delivery still fails verification, the cause is almost always one of: a body parser ran before verification (fix per Step 1 above), a masked secret preview (whsec_********...<last4>) copied in instead of a live secret, or server clock skew past the replay window. See Troubleshooting: signature verification failures.
Continue
- Webhook security — full signature protocol
- Verify without the SDK (Python and Go) — stdlib verifiers
- Webhook events — event catalog
- Build a durable webhook consumer — queue, dedupe, ack-fast pattern