> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook delivery semantics: retries, ordering, and the dead-letter queue

> The delivery model behind Orbit webhooks — at-least-once semantics, the exponential retry schedule, proven-dead responses, the dead-letter queue, and endpoint auto-disable — and what each implies for your receiver.

# 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](/webhooks/overview); for signature verification, see [Webhook security](/webhooks/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:

| Attempt | Nominal wait before attempt | Cumulative elapsed (no jitter) |
| ------- | --------------------------- | ------------------------------ |
| 1       | — (initial)                 | 0                              |
| 2       | 30s                         | 30s                            |
| 3       | 60s                         | 90s                            |
| 4       | 120s                        | 3.5 min                        |
| 5       | 240s                        | 7.5 min                        |
| 6       | 480s                        | 15.5 min                       |
| 7       | 960s                        | 31.5 min                       |
| 8       | 1920s                       | 63.5 min                       |
| 9       | 3840s                       | 2.1 h                          |
| 10      | 7680s                       | 4.25 h → dead-letter queue     |

## 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:

* **Dashboard** — **Developer → Webhooks → Dead-letter queue**, with a one-click requeue per delivery.
* **API** — `GET /api/v1/webhooks/dlq` to list, `POST /api/v1/webhooks/dlq/<delivery_id>/requeue` to put one delivery back into the retry pipeline.

```bash theme={null}
# List dead-lettered deliveries
curl https://api.orbit.devotel.io/api/v1/webhooks/dlq \
  -H "X-API-Key: dv_live_sk_..."

# Requeue one delivery
curl -X POST https://api.orbit.devotel.io/api/v1/webhooks/dlq/dlv_abc123/requeue \
  -H "X-API-Key: dv_live_sk_..."
```

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:

* [Webhooks overview](/webhooks/overview) — endpoint setup (dashboard and API), the payload envelope, and signature headers
* [Webhook security](/webhooks/security) — HMAC signature verification with copy-paste verifiers
* [Webhook events reference](/reference/webhook-events) — the catalog of event types and per-event payload shapes
