Skip to main content

Webhook delivery semantics

Every Orbit webhook is delivered under one contract: at-least-once, with bounded retries, and a dead-letter queue for the events that exhausted them. This page explains that contract as a model — what Orbit guarantees, what it deliberately does not guarantee, and how to build a receiver that is correct under both. For endpoint setup and the payload envelope, see Webhooks overview; for signature verification, see Webhook security.

1. At-least-once delivery

Orbit guarantees every event is delivered at least once — never exactly once. A retry after a timeout (where your endpoint may actually have received the event) and any dead-letter replay are legitimate re-deliveries of the same event. The implication for your receiver: deduplicate on the envelope id field (evt_...). Persist every event id you have processed and drop deliveries whose id you have already seen. Keep seen ids for at least 7 days — long enough to span the full retry schedule plus the dead-letter replay window (both below). At-least-once is the safe direction to err. An exactly-once claim would mean a lost event when a retry was suppressed — a duplicate you discard beats a delivery you never process.

2. Event-time ordering vs. network reordering

Orbit emits events for a given endpoint in event-time order, but HTTP delivery across the network can reorder them — a slow first attempt can land after a later event’s first attempt, and retries arrive interleaved with newer events by construction. The implication: design your receiver to tolerate out-of-order delivery. Sort or supersede by the envelope created_at timestamp, not by arrival order. If you mirror entity state (message status, call state) into your own datastore, apply events for the same entity idempotently and let the event with the latest created_at win — does not matter whether they arrived in sequence.

3. The retry schedule

A delivery counts as successful when your endpoint returns any 2xx within 30 seconds. Anything else — a timeout, a 5xx, or a retryable 4xx such as 400 or 422 — schedules a retry. Each event gets one initial attempt plus 9 retries — 10 attempts total — on an exponential backoff that starts at 30 seconds and doubles each round, with +0–20% jitter (delays run up to 20% longer than nominal, never shorter): 30s → 60s → 120s → 240s → 480s → 960s → 1920s → 3840s → 7680s If all 10 attempts fail, the event moves to the dead-letter queue — roughly 4.3 hours after the first attempt at nominal delays (up to ~5.1 hours if jitter stretches every delay). Sample failed-delivery timeline, assuming your endpoint returns 503 on every attempt:

4. Responses that skip retries

Four statuses are treated as proven-dead: 401, 403, 404, and 410 Gone. Any of them tells Orbit the endpoint will never succeed on this URL — authorization is wrong, the path is gone, or the resource was removed. Retrying is pointless, so a proven-dead response skips the retry schedule entirely: the event moves straight to the dead-letter queue and the endpoint is immediately disabled (Orbit notifies the org admin). 410 Gone is also the deliberate way to permanently skip retries for a single event — for example when you have deprecated handling for that event type. Return 2xx even when you choose not to process an event, unless you genuinely want it dead-lettered. Every other failure — timeouts, 5xx, retryable 4xx — follows the schedule in the previous section.

5. The dead-letter queue

Events that exhaust the retry schedule (or hit a proven-dead response) land in the dead-letter queue, where they remain replayable for 7 days. Two surfaces have parity — the dashboard and the API expose the same inbox:
  • DashboardDeveloper → Webhooks → Dead-letter queue, with a one-click requeue per delivery.
  • APIGET /api/v1/webhooks/dlq to list, POST /api/v1/webhooks/dlq/<delivery_id>/requeue to put one delivery back into the retry pipeline.
A requeued delivery re-enters the retry pipeline as a fresh attempt — which is exactly why your dedup buffer (section 1) must cover the replay window, not just the initial retry schedule.

6. Auto-disable and re-enable

Orbit tracks consecutive failures per endpoint. At 50 consecutive failures, the endpoint is auto-disabled and the org admin is emailed — the same mechanism that fires instantly on a proven-dead response. While an endpoint is disabled, deliveries do not keep retrying in the background; they go straight to the dead-letter queue, so nothing is lost. Once your endpoint is healthy again, re-enable it from the dashboard, then requeue the accumulated dead-letter backlog. Fix the cause before re-enabling — a re-enabled endpoint that immediately fails again starts a new streak.

7. Two directions of idempotency

Idempotency on Orbit runs in both directions, with a different key on each side:
  • Outbound (your request → Orbit). Send an Idempotency-Key header to make a retried API call safe — a retry returns the original result instead of executing twice. The key’s dedup window is 24 hours.
  • Inbound (Orbit webhook → your receiver). Deduplicate on the envelope id (evt_...), persisting seen ids for at least 7 days to cover retries plus dead-letter replays.
The distinction matters when you build both ends: the 24-hour Idempotency-Key window governs your client retries into Orbit; the much longer evt_... buffer governs Orbit’s deliveries into you. Do not assume one window covers the other.

Where this fits

Delivery semantics describe what the wire protocol guarantees. The sibling pages carry the rest of the model: