Webhook Security
Every webhook delivery from Orbit is signed with HMAC-SHA256 so you can verify that the request genuinely came from Orbit and hasn’t been tampered with. Each delivery carries the signature in up to three headers:
All three are valid HMAC-SHA256 signatures over the string
<t>.<raw_body> and share the identical t=<unix>,v1=<hex> encoding. Always verify at least one of them before processing a delivery.
Canonical Headers
New integrations should verify the canonicalX-Orbit-Signature header, which is sent on every current delivery and carries a single v1=<hex> signed with your current signing secret (a small number of older deliveries queued before this header existed arrive with only the legacy X-Devotel-Signature, so keep a fallback to it — the examples below do this automatically):
t=<unix>— Unix timestamp (seconds) when Orbit signed the payload. Use this for replay protection.v1=<hex>— HMAC-SHA256 of the string<t>.<raw_body>keyed by your webhook signing secret. Hex-encoded.
X-Orbit-Signature-Next — the same payload signed with your previous signing secret:
X-Orbit-Signature-Next is absent outside the grace window. Because each canonical header carries exactly one v1, a verifier that holds both your new and previous secrets can tell which key matched cleanly (no multi-candidate parsing): check X-Orbit-Signature against your current secret, and — if present — X-Orbit-Signature-Next against your previous secret.
Legacy X-Devotel-Signature
For backwards compatibility, every delivery also includes the Stripe-style X-Devotel-Signature header, which is not a bare hex string:
t=<unix> timestamp and v1=<hex> encoding as the canonical headers. During a secret-rotation grace window, Orbit emits two v1=<hex> values in this single header (new secret first, previous secret second) — the combined form of X-Orbit-Signature and X-Orbit-Signature-Next — so a verifier configured with either secret continues to validate:
X-Devotel-Signature should split the header by ,, parse each key=value pair, and accept the request if any v1 matches your computed HMAC.
How Signature Verification Works
The steps below work for any of the signature headers —X-Orbit-Signature (recommended), X-Orbit-Signature-Next, or the legacy X-Devotel-Signature. The same t=...,v1=... parser handles all three; the canonical headers simply carry a single v1.
- Parse the signature header into a timestamp
tand one or morev1candidate signatures. - Reject the request if
tis older than 5 minutes (replay protection). - Compute
expected = HMAC_SHA256(secret, "<t>.<raw_body>")as hex. - Compare
expectedagainst eachv1candidate using a timing-safe comparison.
Verification Examples
These examples read the canonicalX-Orbit-Signature header and fall back to the legacy X-Devotel-Signature header so they work against any delivery. The shared parser accepts one or more v1 candidates, so the same code also validates X-Orbit-Signature-Next if you choose to verify it explicitly during a rotation.
Node.js
Python
Go
Security Best Practices
- Always verify signatures — never process webhooks without checking the signature
- Use timing-safe comparison — prevents timing attacks (
crypto.timingSafeEqualin Node.js,hmac.compare_digestin Python) - Use HTTPS — your webhook endpoint must use HTTPS to protect payloads in transit
- Rotate secrets — rotate your webhook signing secret periodically in Settings > Webhooks
- Idempotency — use the event
idfield to deduplicate, since Orbit guarantees at-least-once delivery - Respond quickly — return a
2xxwithin 30 seconds; process heavy work asynchronously - Verify signatures first, allowlist IPs second — always authenticate every delivery with the HMAC signature above; a source IP can be shared or spoofed upstream, so it is never sufficient on its own. If your webhook endpoint sits behind a firewall that only accepts inbound HTTPS from known addresses,
GET /api/v1/webhooks/egress-ipsreturns the static source IP(s) Orbit sends deliveries from so you can allowlist them. Allowlist every entry the endpoint returns and don’t assume a fixed count. (The separate IP allowlist under Settings > Security restricts who can reach the Orbit dashboard and API by IP; it does not affect webhook delivery.)
Signing Secret
Your webhook signing secret (prefixed withwhsec_) can be supplied on creation — pass it as the optional secret body field (minimum 16 characters) — or auto-generated by Orbit when you omit it. The cleartext secret is returned only once — in the response to POST /api/v1/webhooks (endpoint creation) or POST /api/v1/webhooks/{id}/rotate-secret (rotation). Store it securely at that moment.
GET /api/v1/webhooks/{id} does not return a usable secret — it returns a masked preview (whsec_********...<last4>) for identification only. Do not copy that masked value into your verification logic; the HMAC will never match and signature checks will silently fail.
If you lose the secret, rotate to obtain a new one: