Production-grade webhook consumer: end-to-end walkthrough
This guide ties together every piece you need to build a production-grade Orbit webhook consumer — from picking events to surviving retries — into one end-to-end narrative. The individual guides cover each piece in depth; this page is the orchestrated walkthrough that connects them.1. Why signature verification is non-negotiable
Every Orbit webhook delivery carries an HMAC-SHA256 signature in theX-Orbit-Signature header. Without verifying it, your receiver treats every POST as genuine — which means it will process forged events, replay old events an attacker captured, and accept bodies mutated by a proxy between Orbit and your server.
The signature prevents two classes of attack:
- Replay — an attacker replays a captured delivery. The signature’s embedded Unix timestamp (
t=) plus your server’s 5-minute clock skew check reject anything stale. - Forgery — an attacker invents an event envelope. Without your
whsec_...signing secret — which never leaves your control — the HMAC does not match.
Node.js (Express)
Python (stdlib hmac + hashlib)
Go (crypto/hmac)
The header fallback order
Read headers in this priority order — canonical first, then the rotation grace header, then the legacy back-compat header:X-Orbit-Signature— signed with your current secret.X-Orbit-Signature-Next— only present during a secret rotation grace window (7 days), signed with your previous secret.X-Devotel-Signature— legacy header that carries one or twov1=candidates for older in-flight deliveries.
2. Event catalog: what the envelope always carries
Every event Orbit dispatches shares a common envelope. Before you write a single handler, know the fields that every event type guarantees:
These fields are the contract the event catalog publishes. Every entry in the catalog carries a machine-readable
json_schema (JSON Schema Draft-07) that asserts required: ["id", "type", "created_at", "data"] and pins type with a const, plus an example_payload you can copy into a fixture, and a fingerprint (sha256:<16 hex>) you pin in CI to catch schema drift. The catalog answers “what events exist” and “what shape is each payload” before any receiver code runs. Browse it in the dashboard under Developer → Webhooks → Event catalog or fetch it over the API:
3. The consumer loop: receive, verify, route, enqueue, ack
A production webhook consumer follows one fixed order. Skipping or reordering any step creates a defect that ships silently.Complete handler (Node.js + Express)
4. Retry semantics and acknowledgment rules
Orbit delivers at-least-once. If your endpoint returns a non-2xx status or times out, the dispatcher retries with exponential backoff — 30s, 60s, 120s, and so on, doubling each round with up to 20% jitter — for one initial attempt plus up to nine retries. After the ladder is exhausted (roughly 4.3 hours from the first attempt), the event lands in the dead-letter queue.What to return per outcome
Never return 2xx for a signature mismatch. A 200 on an unauthenticated request tells Orbit the event was accepted — the retry loop stops and you lose the event. Always 401.
Delivery timeout
Per-endpoint delivery timeout is configurable from 1 to 30 seconds (default 30). Set it in the endpoint’s Edit dialog under Developer → Webhooks or through the API ontimeout_seconds. If your handler does heavy work inline instead of enqueueing, a default 30s timeout masks the problem until traffic grows — keep handler work under one second and let the queue absorb the rest.
Dead-letter replay
From Developer → Webhooks → Failed Deliveries you can replay dead-lettered events one at a time or in bulk for up to 7 days. After 7 days, dead-lettered events are removed. The endpoint operations guide walks bulk replay, dry-run preview, and polling the replay job to completion. Keep your dedupe table rows for at least 7 days to cover this window.5. Failure paths
Signature mismatch — 400 with a log, never 2xx
A signature check that fails is either a misconfigured secret in your verifier, a mutated raw body (a JSON middleware ran before verification), or a genuine forgery. Return401 and log the failure with enough detail to diagnose:
Delivery timeout
If your handler does not respond within the configured timeout window, Orbit counts it as a failure and retries. The common cause is business logic running inline — every millisecond of DB work or API call in the handler is time your ack is not arriving. Move all business logic to a queue worker.Duplicate delivery (replay)
The same eventid can arrive more than once — the retry loop re-POSTs identical payloads. Your dedupe table (section 6) is the only defense. Without it, a message.delivered retried 9 times updates the delivery state 10 times, and one of those might overwrite a later status with a stale one.
For idempotency patterns that survive beyond the 7-day dedupe window — including safe retries you initiate from your own queue — see the idempotency and safe retries concept page.
6. Deduplication and the 7-day window
The dedupe key is the envelopeid. It is stable across retries of the same event, and it is what Orbit retries by. Persist seen ids in your database — not in memory — so deduplication survives process restarts:
DELETE ... WHERE first_seen < now() - interval '7 days' so the table does not grow unbounded.
7. Testing with the Webhook Tester and the inbound debugger
The Webhook Tester (dashboard at Developer → Webhook Tester) fires synthetic events at any HTTPS URL and shows your server’s real status, headers, and response body. Use it during development to iterate on payload parsing before you register a real endpoint. Two facts to know:- Test sends are unsigned. The tester has no endpoint secret, so no
X-Orbit-Signatureheader is attached. A verification-first receiver correctly returns 401. To exercise the signature path, use the signed Test-fire action on a registered endpoint under Settings → Webhooks. - The tester delivers through the real dispatcher. The POST reaching your server is genuine — your URL must be publicly reachable over HTTPS.
Local test with a signed payload
Once you have registered an endpoint and captured thewhsec_... secret, sign a payload locally to confirm your verifier works:
duplicate: true.
8. Worked example: a campaign.delivered consumer
Here is a complete consumer wired for campaign.delivered. It reads the raw body, verifies the signature, dedupes on event id, enqueues the parsed payload, and acks in under a millisecond.
The envelope
Queue worker
The handler acks in microseconds. The queue worker processes the event when it is ready:campaign.delivered handler records the delivery against the campaign’s analytics and the contact’s timeline. If the DB write fails, your queue retries the job — the webhook ack already returned 200, so Orbit does not re-deliver.
9. Secure storage of the signing secret and rotation
Thewhsec_... secret controls who can forge a valid signature. Treat it with the same care as an API key:
- Store it in a secrets manager (environment variable, vault, SSM parameter) — never in code, config files, or logs.
- The cleartext secret is returned exactly once: at endpoint creation and at rotation initiation/completion. Every later
GET /api/v1/webhooks/{id}returns a maskedwhsec_********...<last4>preview. - If you lose the cleartext secret, run a rotation to mint a new one — you cannot recover a lost secret.
Rotation in the console
Open the endpoint in Settings → Webhooks, then open Rotate secret from the detail page or overflow menu. The wizard mints a new candidate secret while the old one keeps verifying deliveries. During the 7-day grace window every delivery carries bothX-Orbit-Signature (signed with the new secret) and X-Orbit-Signature-Next (signed with the previous one). Copy the new secret into your verifier alongside the current one, confirm, and when the status panel shows most deliveries verify with the new secret, click Complete rotation to retire the old one.
The endpoint operations guide and the edit console guide walk each rotation step with screenshots, force-complete, and the POST /api/v1/webhooks/{id}/rotation/initiate API workflow.
Next steps
- Build a durable webhook consumer — Node.js and Python handlers with dedup, retry, and rotation in detail
- Explore webhook event schemas from the catalog — machine-readable schemas, CI fingerprint pins, and the TypeScript codegen path
- Verify webhook signatures without the SDK — standard-library verifiers for Python, Go, Ruby, and PHP with negative tests
- Webhook payload cookbook — one copy-pasteable JSON payload per event family
- Test webhooks with the Webhook Tester — unsigned test sends and the signed Test-fire path
- Operate a webhook endpoint — health, replay, bulk replay, pause, and the DLQ
- Debug inbound carrier webhooks — carrier-side signature verification and the inbound debug log
- Quickstart: receive your first webhook — tunnel, register, verify, replay end to end
- Webhook security — HMAC format specification and the rotation grace header