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
2xxin milliseconds and hands the work to a queue instead of doing it inline. - Rotation-ready — the verifier accepts the
X-Orbit-Signature-Nextgrace header during a secret rotation, so rotation is a zero-downtime affair.
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, andporting.request.loa_signed, but the pattern is identical for any event.
Step 1 — Register the endpoint and capture the secret
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)
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 sharedseen_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: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:
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: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
- Webhook events reference — subscribe to additional events
- Event payloads — per-event schema reference
- Webhook security — signature details and IP allowlisting