Skip to main content

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:
The response includes the cleartext signing secret (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 — register express.raw() on this route before any global express.json()):
Python (Flask):
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 canonical X-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 envelope id (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 on event.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 2xx within 30 seconds — success, no retry.
  • Timeout or 5xx — always retries.
  • Retryable 4xx (400, 422, …) — retries until the schedule is exhausted.
  • 401, 403, 404, or 410 Gone — proven-dead: skips retries, moves to the dead-letter queue, and immediately disables the endpoint (the org admin is notified). 410 Gone is also how you permanently skip retries for a lone event type you have deprecated.
50 consecutive failures on one endpoint also auto-disables it. Re-enable from the dashboard once your receiver is healthy; deliveries to a disabled endpoint go straight to the dead-letter queue.

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:
For a single failed delivery from the event timeline, use Replay on the delivery row, or 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

  1. Run the receiver locally and expose it with a tunnel (ngrok http 3000).
  2. Register the tunnel URL, subscribe to one event type, and copy the cleartext secret.
  3. Trigger a real event (send a test SMS) and watch the signature verify, the handler run, and a 200 go back.
  4. Flip the receiver off and re-trigger — watch the retry schedule in Developer → Webhooks → Events, then bring the receiver back and replay the failure.
If signatures fail to verify, work through Troubleshooting: Signature Verification Failures — the common case is a JSON middleware consuming the raw body before verification.

See also