Skip to main content

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 canonical X-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.
During a secret-rotation grace window, Orbit additionally emits 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:
It carries the same 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:
A verifier reading 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.
  1. Parse the signature header into a timestamp t and one or more v1 candidate signatures.
  2. Reject the request if t is older than 5 minutes (replay protection).
  3. Compute expected = HMAC_SHA256(secret, "<t>.<raw_body>") as hex.
  4. Compare expected against each v1 candidate using a timing-safe comparison.

Verification Examples

These examples read the canonical X-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.timingSafeEqual in Node.js, hmac.compare_digest in 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 id field to deduplicate, since Orbit guarantees at-least-once delivery
  • Respond quickly — return a 2xx within 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-ips returns 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 with whsec_) 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:
You can also view (and rotate) the secret in the dashboard under Webhooks > [Your Webhook] > Signing Secret. Store it securely — never expose it in client-side code or logs.