Build your first webhook receiver
This walkthrough takes you from zero to your first verified Orbit delivery. By the end you have a receiver that verifies the signature, deduplicates retries, and routes each event type to a handler — plus the retry semantics to reason about it in production. The same pieces exist deeper in the reference docs; this page puts them in build order. Security is the canonical signature reference with verifiers in seven languages, Event payloads documents the envelope and headers, and Inspecting deliveries covers replay tooling.Step 1 — Register an endpoint and copy the secret
Register a webhook subscription in the dashboard under Settings → Webhooks → Add endpoint, or over the API:whsec_...). This is the only time it is shown — copy it immediately and store it as an environment variable, never in code. Subscribe to a narrow event list while you develop; broad * subscriptions generate far more traffic than a new integration needs.
Step 2 — Serve a POST endpoint with a raw body
Verification signs the raw request body, so your receiver must read the body before any JSON middleware parses it. Keep the raw bytes around. Node (Express — registerexpress.raw() on this route before any global express.json()):
The Python SDK ships the same verifier as
orbit_sdk.verify_webhook(payload, signature, secret) — one call that raises OrbitWebhookSignatureError on failure and returns the decoded event dict. The Node verifier above is the same logic with no dependency. Verifiers in Go, Ruby, PHP, Java, and C# are on Webhook security.Step 3 — Verify the signature (shown above)
Both receivers read the canonicalX-Orbit-Signature header and fall back to the legacy X-Devotel-Signature, parse the t=<unix>,v1=<hex> format, reject timestamps older than 5 minutes, and compare HMAC_SHA256(secret, "<t>.<raw_body>") with a timing-safe comparison. During a secret-rotation grace window the legacy header carries two v1 candidates — the “accept if any matches” logic above handles that without changes.
Return 401 on failure and stop — never process an unverified payload.
Step 4 — Deduplicate by event id
Delivery is at-least-once: a timeout or retry can deliver the same event twice. The envelopeid (evt_...) is stable across every retry of one event, and the Idempotency-Key header mirrors it. Persist every id you process and return 200 immediately for a duplicate — a fast duplicate ack is better than re-firing billable work downstream.
Keep ids for at least 7 days, which covers the full retry window plus the dead-letter replay window.
Step 5 — Map event types to handlers
Route onevent.type and acknowledge types you do not handle. New event types appear over time; a receiver that returns 4xx on an unknown type burns its retry budget on events it never wanted. The full catalog of type values is on Webhook events.
Retry semantics — the concrete timings
If your endpoint returns a non-2xx response or does not answer within 30 seconds, Orbit retries the delivery on an exponential backoff schedule: one initial delivery plus 9 retries (10 attempts total). The backoff starts at 30 seconds and doubles each retry:
Up to 20% extra jitter is applied on top, so a burst of failures against one endpoint does not retry in lockstep. The whole schedule spans roughly 4.3 hours — after the 10th attempt fails, the event moves to the dead-letter queue.
Response codes mean specific things:
- Any
2xxwithin 30 seconds — success, no retry. - Timeout or
5xx— always retries. - Retryable
4xx(400, 422, …) — retries until the schedule is exhausted. 401,403,404, or410 Gone— proven-dead: skips retries, moves to the dead-letter queue, and immediately disables the endpoint (the org admin is notified).410 Goneis also how you permanently skip retries for a lone event type you have deprecated.
Dead-letter queue and replay
After retries exhaust, events sit in the dead-letter queue for 7 days, replayable from the dashboard under Developer → Webhooks → Dead-letter queue or over the API:POST /api/v1/webhooks/{endpoint_id}/deliveries/{delivery_id}/replay — failed-only, same endpoint, within 7 days. The dashboard flow and bulk paths are on Inspecting deliveries.
Test the loop
- Run the receiver locally and expose it with a tunnel (
ngrok http 3000). - Register the tunnel URL, subscribe to one event type, and copy the cleartext secret.
- Trigger a real event (send a test SMS) and watch the signature verify, the handler run, and a
200go back. - Flip the receiver off and re-trigger — watch the retry schedule in Developer → Webhooks → Events, then bring the receiver back and replay the failure.
See also
- Webhooks overview — delivery guarantees, retry schedule, managing endpoints
- Webhook security — canonical HMAC reference, seven-language verifiers, key rotation
- Webhook event payloads — envelope schema and every request header
- Inspecting deliveries and replaying failures — events explorer, delivery inspector, bulk replay
- Webhook events catalog — every
typevalue you can subscribe to