Skip to main content

Build a durable webhook consumer

What you’ll build

A server that receives Orbit webhook events — message status (message.delivered, message.failed), call lifecycle (call.completed), and number-porting updates (porting.request.*) — and processes them durably:
  • Signature-verified — every request is authenticated with HMAC-SHA256 before anything else happens.
  • Replayable — seen event IDs are persisted, so the automatic retry loop can’t double-process an event.
  • Fast to ack — the handler returns 2xx in milliseconds and hands the work to a queue instead of doing it inline.
  • Rotation-ready — the verifier accepts the X-Orbit-Signature-Next grace header during a secret rotation, so rotation is a zero-downtime affair.
For endpoint concepts and the delivery contract, start at Webhooks overview. For the signature math itself, see Webhook security. This guide puts those pieces together into a working consumer.

Prerequisites

  • A publicly reachable HTTPS URL you control, e.g. https://yourapp.com/webhooks/orbit (you’ll tunnel to it for local testing in step 8).
  • An Orbit API key (dv_live_sk_...) with webhook management permission — created in the dashboard under Settings > API Keys.
  • The event names you care about from the event catalog — this guide uses message.delivered, message.failed, call.completed, and porting.request.loa_signed, but the pattern is identical for any event.

Step 1 — Register the endpoint and capture the secret

The response includes your endpoint id (wh_...) and the full cleartext whsec_... signing secret. Copy the secret now — it is returned exactly once. Omitting secret in the body lets Orbit generate it, which is what this example does. A later GET /api/v1/webhooks/{id} returns only a masked whsec_********...<last4> preview; feed that masked value to your verifier and every signature check silently fails.

Step 2 — Build the receiver

The receiver must read the raw request body (before any JSON parser touches it), check the signature timestamp (reject anything older than 5 minutes), and verify the HMAC before it does any work.

Node.js (Express)

Read the canonical X-Orbit-Signature header and fall back to the legacy X-Devotel-Signature so older in-flight deliveries still validate. During a secret rotation Orbit additionally signs with your previous secret and sends it as the X-Orbit-Signature-Next header — to accept either key, parse the rotation state from GET /api/v1/webhooks/{id}/rotation/status and check X-Orbit-Signature-Next against the previous secret when that header is present. Queue jobs run the parsed payload against your business logic and can throw freely — a failed job just stays in your own queue for its own retry. The webhook handler itself never fails as long as parsing and enqueueing succeed.

Python (Flask)

Step 3 — Persist and dedupe

Dedupe must survive process restarts, so keep the seen IDs in your database, not in memory — the examples above use a shared seen_events store whose implementation is one table:
INSERT ... ON CONFLICT (event_id) DO NOTHING returning zero rows means “already processed, ack as duplicate.” Because Orbit retries a failing endpoint for about 4–5 hours and a dead-lettered event stays replayable for 7 days, keep these rows around for at least 7 days — prune them with a nightly DELETE against first_seen < now() - interval '7 days' (or your database’s equivalent TTL mechanism). Your worker then reads its own queue (Sidekiq, Celery, BullMQ, a plain table — whatever you already run) and hands the parsed event to the right handler:

Step 4 — Understand retries and the dead letter queue

Orbit gives you at-least-once delivery: an event can arrive more than once, which is why the dedupe table is non-negotiable. If your endpoint returns a non-2xx or times out, Orbit retries with exponential backoff (30s → 60s → … doubling, up to 20% jitter) for one initial attempt plus nine retries, then moves the event to the dead letter queue — roughly 4.3 hours after the first attempt. From the dashboard under Webhooks > Failed Deliveries you can replay dead-lettered events for 7 days. Net effect: a failed endpoint has hours to catch up, and a fixed endpoint can replay everything it missed.

Step 5 — Rotate signing secrets

Two rotation paths exist; use them in the order below. The productised lifecycle is the preferred path. Initiate a rotation to mint a new candidate secret:
Copy the returned cleartext secret into your verifier alongside the current one (the response is the only place it is ever visible). During the 7-day grace window Orbit dual-signs: X-Orbit-Signature uses the new secret and X-Orbit-Signature-Next uses the previous one, so old and new verifiers both pass. Poll GET /api/v1/webhooks/wh_abc123/rotation/status until it reports the recent deliveries all verified with the new secret, then:
Completion swaps the candidate in as the canonical secret. POST .../rotation/cancel discards the candidate if you need to abort. If you call initiate again within 24 hours you get the existing rotation back rather than a fresh secret, which keeps two dashboard tabs from invalidating each other. If you lose the cleartext secret entirely, the legacy fallback still works:

Step 6 — Test locally

Point a tunnel at your local server and register that URL as a temporary endpoint:
Then push a synthetic event at it with your signing secret:
The same one-liner signs a replayed payload against either the canonical or the grace header; send the same body twice to confirm the second call acks as duplicate: true.

Production checklist

  • Endpoint URL is HTTPS (required for delivery).
  • Handler acks within the 30-second timeout window — heavy work happens in a queue, past-30s responses count as failures.
  • Signature verification happens before any other request handling, with timing-safe comparison and the 5-minute timestamp window.
  • Dedupe IDs persist for at least 7 days to cover retry + dead-letter replay windows.
  • If your firewall restricts inbound HTTPS, allowlist every IP returned by GET /api/v1/webhooks/egress-ips — and treat that as a second factor, never a replacement for signature verification.
  • Monitor failed deliveries in Webhooks > Failed Deliveries; alert on any event reaching the dead letter queue.
  • Store the signing secret in a secrets manager, never in code or logs.

Next steps