> ## 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 fan-out: one event, many destinations

> How one platform event becomes many deliveries — the events buffer, per-endpoint webhooks, and event sinks — each with its own filter, retry state, and dead-letter queue.

# Webhook fan-out: one event, many destinations

One thing happens in your workspace — a message is delivered, a call completes, a flow step fires — and Orbit emits **one event** for it. From that point the event fans out to **zero or more destinations**: the ephemeral events buffer, every registered webhook endpoint, and any configured event sink. Each destination is an independent subscription with its own routing filter, its own retry state, and its own dead-letter path. This page explains that topology — what the producer surfaces are, how a subscription declares what it wants, and what failure means per destination. The transport contract for any single webhook endpoint (payload envelope, retry schedule, DLQ, proven-dead statuses) is covered in [Webhook delivery semantics](/concepts/webhook-delivery-semantics); this page is the model one level up.

## Section 1 — Producer vs. consumer surfaces

Orbit emits platform events from one taxonomy — `message.*`, `call.*`, `flow.*`, and the rest of the catalog on the [events reference](/webhooks/events). Every event carries the same envelope (`id`, `type`, `created_at`, `data`), and three surface groups consume it:

* **Events buffer (ephemeral).** [Events API](/api-reference/events) — a bounded replay window (the most recent \~5000 events per tenant, expiring after \~15 minutes of inactivity) that you poll over REST or stream over SSE. It is for live dashboards and "what just happened" debugging; evicted ids are gone. Never treat it as delivery.
* **Webhooks (durable, retried, signed).** Each endpoint you register ([webhooks overview](/webhooks/overview)) is a push destination. Orbit delivers the event to it at least once, signs the request, retries on a bounded schedule, and dead-letters after the schedule exhausts.
* **Event sinks (to your own infrastructure).** A sink ([Event Sinks API](/api-reference/event-sinks)) points the same event stream at your own Kafka topic or batch HTTP collector instead of an HTTP receiver per event type. You get one destination carrying many event types; record shaping is byte-identical to the webhook envelope either way. Delivery on the sinks surface is still rolling out — until it lands, the configured config is stored but nothing flows through it (check the warning block on the [Event Sinks API](/api-reference/event-sinks) page before you plan around sinks).

The distinction that matters operationally: only webhooks and (once delivery ships) event sinks are durable consumer surfaces. The events buffer is a convenience — build nothing on it that has a retention requirement.

## Section 2 — The fan-out topology

One event becomes zero or many deliveries. Orbit does not "route" an event to a single destination; it fans the event out to every subscription whose filter matches:

```
one platform event
    ├── events buffer (ephemeral, ~5000-event window)
    ├── webhook endpoint A  (filter: message.*)
    ├── webhook endpoint B  (filter: message.delivered, call.completed)
    ├── webhook endpoint C  (filter: *)            ── each with separate
    ├── event sink: kafka   (filter: message.*)    ── retry state + DLQ
    └── event sink: http_batch (filter: *)
```

A subscription is just the pair `(filter, destination)`. Orbit keeps per-subscription delivery state: consecutive-failure counts, the retry position for each in-flight delivery, and the DLQ. No state is shared across subscriptions — endpoint A having a bad day does not slow, block, or starve endpoint B, and a sink that starts returning errors does not change the retry schedule of any webhook endpoint.

If no subscription matches an event's type, the event still lands on the events buffer and nowhere else — there is nothing to deliver.

## Section 3 — Routing filters: how a subscription declares what it wants

The customer-facing filter is an `events` array on the registration:

* **Webhook endpoint** — `events: ["message.*"]`, `events: ["message.delivered", "call.completed"]`, or `events: ["*"]` for everything (see [webhooks overview](/webhooks/overview)).
* **Event sink** — the `events` field at `PATCH /api/v1/developer/event-sinks/{kind}` accepts the same vocabulary: an exact type (`message.sent`), a `<prefix>.*` glob (`message.*`), or `*` (see [Event Sinks API](/api-reference/event-sinks)). An empty or `*` filter means every event.

The same filter vocabulary applies on both surfaces — name the prefix you want, or subscribe to all. Orbit evaluates the filter per event at fan-out time, and a subscription never sees a non-matching event type. **`(event id)` is duplicated across subscriptions** — every receiver deduplicates independently on the envelope `id` (`evt_...`), not just its own (see [delivery semantics](/concepts/webhook-delivery-semantics)).

## Section 4 — Ordering and at-least-once semantics

There is **no global ordering guarantee across subscriptions** — and there cannot be, because each surface has its own transport. "Event E1 then E2" holds per subscription, not across them:

* Webhook endpoints receive E1 before E2 (Orbit emits in event-time per endpoint), but network reordering can still invert them — see the ordering section of [delivery semantics](/concepts/webhook-delivery-semantics).
* A Kafka sink (once delivery ships) gets partition-level ordering **only** for a single key (your `partition_key_path` such as `data.contact_id`).
* The events buffer shows insertion order within the retained window.

Every delivery is **at-least-once**: a timed-out attempt that actually landed, and any DLQ replay, are legitimate re-deliveries of the same event. Corollary: the same `id` may arrive at the same endpoint more than once, and will arrive at different endpoints on independent schedules. Write each receiver idempotently — dedupe on the envelope `id`, and let the event with the largest `created_at` win for entity state.

## Section 5 — Failure modes

Because fan-out is per-subscription, failure is too. The table is the whole model:

| Failure class                                                         | What happens on the affected subscription                                                | Effect on the other subscriptions |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------- |
| Endpoint returns timeout / 5xx / retryable 4xx (`400`, `422`)         | Retried on that endpoint's schedule (1 initial + 9 retries, \~4.3 h at nominal delays)   | Zero                              |
| Endpoint returns a proven-dead status (`401` / `403` / `404` / `410`) | Bounces straight to that endpoint's DLQ and the endpoint auto-disables                   | Zero                              |
| Retries exhausted                                                     | Deliveries land in that subscription's DLQ — replayable for 7 days for webhook endpoints | Zero                              |
| \~50 consecutive failures on one webhook endpoint                     | That endpoint auto-disables (deliveries go straight to its DLQ until you re-enable)      | Zero                              |

Two safety properties follow. **Replay is safe by construction**: a DLQ requeue re-enters the retry pipeline as a fresh attempt, which is exactly why the dedup window (the `evt_...` buffer, held for at least 7 days) must span the replay window, not just the initial retries. And **signature verification gates the receiver**: a failed HMAC check drops the delivery before your handler runs, so spoofed payloads never enter your dedup set — see [troubleshooting signature failures](/webhooks/troubleshooting-signature-failures).

For event sinks (once delivery ships) the same philosophy applies — retries back off per sink, and a sink that exhausts its attempts files the record to a dead-letter list — but the retry knobs and run-status fields live on the sink itself (`last_run_status`, `consecutive_failures`) rather than on a per-endpoint delivery schedule. Where a sink's failure surfaces is its own run status; your webhook endpoints are unaffected either way.

## Section 6 — Where to watch delivery health

Three operational surfaces, one per class of question:

* **One specific delivery** — [Inspecting deliveries](/webhooks/inspecting-deliveries) shows every delivery attempt per endpoint and the response it got, and the DLQ surfaces (**Developer → Webhooks → Dead-letter queue** in the dashboard, or `GET /api/v1/webhooks/dlq`) hold the terminal failures.
* **A signature that fails on your side** — [Troubleshooting signature failures](/webhooks/troubleshooting-signature-failures) covers rotation windows and verification gotchas.
* **A sink that is unhealthy (once delivery ships)** — read its run status (`last_run_status`, `last_error`, `consecutive_failures`) via `GET /api/v1/developer/event-sinks/{kind}`; the same fields are the ones the dashboard shows.

Alerting on webhook health is on you: the [operator-observability map](/concepts/operator-observability-map) puts it on the short list of places to hook thresholds — its durable-feed discussion is also where the trade-off between webhooks and the ephemeral events buffer is framed in full.

## Where this fits

This page is the one level up from the transport contract — the sibling pages carry the details:

* [Webhook delivery semantics](/concepts/webhook-delivery-semantics) — per-endpoint retries, ordering, DLQ, proven-dead statuses, idempotency windows
* [Webhooks overview](/webhooks/overview) — endpoint setup, envelope shape, signature headers
* [Event Sinks API](/api-reference/event-sinks) — sink configuration, run status, credential handling
* [Events API](/api-reference/events) — the ephemeral buffer (not a system of record)
* [Operator observability map](/concepts/operator-observability-map) — which surface answers which question
