Skip to main content

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; 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. Every event carries the same envelope (id, type, created_at, data), and three surface groups consume it:
  • Events buffer (ephemeral). Events API — 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) 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) 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 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:
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 endpointevents: ["message.*"], events: ["message.delivered", "call.completed"], or events: ["*"] for everything (see 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). 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).

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.
  • 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: 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. 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 deliveryInspecting 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 sideTroubleshooting 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 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: