Concepts
Deliveries are signals, not source of truth
A delivery tells you something changed. It does not carry the full record — only enough to identify what to re-fetch. Always query the relevant feed or fact-check API for current state.
Why payloads stay minimal:
- Stale-data prevention. Between the source change and your handler running, the underlying record may have changed again. Re-fetching guarantees you act on current state.
- Out-of-order safety. Network conditions can deliver requests out of order. The API has the authoritative state, so re-fetching makes delivery order irrelevant.
- Debouncing. Multiple rapid changes to the same subject collapse into a single delivery. The delivery says "sync me," not "here's the diff."
The envelope
Every delivery is a CloudEvents v1.0
structured-mode JSON object, sent with content-type: application/cloudevents+json:
| Field | Description |
|---|---|
specversion | Always "1.0". |
type | The event type, e.g. news.item.added. See Event type taxonomy. |
source | The container that produced the event, e.g. /feeds/news/{feedId}. |
subject | (optional) What changed within that source. Every event type published today sets it — see the per-type pages for what each one names. |
id | The delivery's unique identifier. Stable across retry attempts — use it as your idempotency key. |
time | When the underlying change happened, ISO 8601. Not the delivery time. |
datacontenttype | Always "application/json". |
data | The event-specific payload. See the per-type pages under Event Types for schemas. |
{
"specversion": "1.0",
"type": "news.item.added",
"source": "/feeds/news/{feedId}",
"subject": "123e4567-e89b-12d3-a456-426614174000",
"id": "456e7890-a12b-34c5-d678-901234567890",
"time": "2024-01-15T10:30:00.000Z",
"datacontenttype": "application/json",
"data": {
"feedId": "123e4567-e89b-12d3-a456-426614174000",
"id": "123e4567-e89b-12d3-a456-426614174000",
"newsPageId": "123e4567-e89b-12d3-a456-426614174000"
}
}
Use the envelope id, not any field inside data, as your dedupe key. It is
the delivery identifier and is identical across every retry of the same
delivery. See Reliability.
Event type taxonomy
Event types are lowercase, dot-delimited, with every segment meaningful:
news.item.added, event.item.updated, factcheck.status.changed.
Underscores are forbidden — they would collapse a level and make a prefix
like news.item.* inexpressible.
Fetch the full catalog, including JSON Schema for each type's data shape,
from GET /events/types:
| Type | Scope | Feed type | Page |
|---|---|---|---|
news.item.added | feed | news | news.item.added |
event.item.added | feed | events | event.item.added |
event.item.updated | feed | events | event.item.updated |
event.item.removed | feed | events | event.item.removed |
factcheck.completed | organization | — | factcheck.completed |
factcheck.failed | organization | — | factcheck.failed |
factcheck.status.changed | organization | — | factcheck.status.changed |
event.item.updated is reserved: it is accepted in a subscription and
returned by GET /events/types, but no producer in the platform publishes
it, so a subscription to it will never deliver anything. The remaining six
types, including factcheck.status.changed, fire today.
Every type identifies exactly one subject via the envelope's subject field.
Nothing batches: removing several events from a feed produces one delivery per
removed event, each with its own subject and its own data.eventId.
news.item.added fires at most once per (feedId, news item), no matter how
many sources or pages surface that item — idempotency is keyed on the item's
identity, not on how many times the underlying discovery work ran.
Wildcards
A subscription's eventTypes array accepts exact types or trailing
wildcards:
| Pattern | Matches |
|---|---|
* | Everything. |
news.* | Every type under the news prefix (news.item.added, and any future news.* type). |
news.item.* | Wildcards work at any depth. |
news.item.added | Exact match only. |
Only a trailing wildcard is supported — news.*.added is rejected at
subscription-write time, not silently ignored. Mid-pattern wildcards
multiply matching cost for a case nobody has needed.
Feed scoping
Feed-scoped event types (news.*, event.*) carry a feedType and feedId
that a subscription's scope must match:
scopeKind | Matches |
|---|---|
all | Every feed-scoped event the organization can see, of any feed type, plus every organization-scoped event. |
feed_type | Every event from feeds of one feedType (news or events). |
feeds | Only the pinned (feedType, feedId) pairs. |
Organization-scoped events (factcheck.*) never reach a subscription whose
scope names a feed, and never cross an organization boundary — a
subscription only receives factcheck events for the organization that owns
its destination.
A destination's subscriptions must name feeds the destination's organization can actually read; pinning a feed you cannot access is rejected at subscription-write time, not silently dropped.
Feed access is re-checked at send time
Access is not just checked when you write a subscription — it is re-checked
again for every event at the moment it would be sent. A delivery whose
subscription has gone inactive, or whose destination organization has since
lost access to the event's feed, is not sent: it is marked discarded.
Replaying a delivery discarded for
either reason is refused with 409, and the error names the reason.
Losing access to a feed (for example, an organization's subscription to a shared feed is revoked) deactivates every pin for that feed across every subscription that had it pinned, regardless of which subscription matched an event first.
URL safety is re-checked at send time
The same pattern applies to the destination's own URL: it isn't validated only once, when you create or update the destination — it is re-resolved and re-checked before every single delivery attempt, specifically to catch a DNS record that starts resolving into a private or reserved address sometime after the destination was validated safely (DNS rebinding). If that happens, every further attempt fails immediately and non-retryably with a generic "Invalid destination URL" message — the resolved address itself is never included in the response — counting toward auto-disable exactly like any other non-retryable failure.
Feed deletion
Deleting a feed removes it from every subscription's pins. A feeds-scoped
subscription left with zero pins as a result becomes inactive with
inactiveReason: "feed deleted". Deleting an event feed additionally
publishes
event.item.removed
for every event item that was in that feed. News feeds have no
news.item.removed type, so deleting a news feed does not publish anything
for its items — only the pin cleanup applies.
Those removals are delivered to the organizations that could read the feed at the moment it was deleted: the feed's owner, plus any organization subscribed to it. Access is otherwise re-checked live against the feed, but a deleted feed has no live answer to give, so the deletion records one — which is why the removals still arrive even though the feed they name is gone. A destination whose organization never had the feed receives nothing.
Debounce
Rapid, repeated changes to the same subject collapse into a single delivery rather than one per change:
- Window: configurable per destination, 0–900 seconds (default 120s).
- Captured at publish time: the window applied to a given event is the
value configured at the moment that event was published. Changing a
destination's
debounceWindowSecondsafterwards does not retroactively change the window for events already collapsing under the old value. - Scope: debounced independently per subject — updating item A does not delay a delivery about item B.
- Implication: during high activity you receive one delivery per
(subject, debounce window), not one per underlying change. Re-fetch from the API to see the final state, per Deliveries are signals, not source of truth above. - A freshly created destination may see nothing at first. The first matching change after you subscribe still waits out the debounce window before it's sent — don't expect a delivery within seconds of the source change.
The delivery that gets collapsed away by debounce is not deleted — it is
finalized with status superseded, with no attempts of its own. If you see
superseded rows in GET /events/destinations/{ref}/deliveries, that's
debounce working as intended: a newer delivery for the same subject carried
the update instead, not a sign that anything broke. A superseded delivery
can still be replayed if you need
the exact one re-sent.
Backfilled deliveries (see Reliability) skip debounce entirely — the events are already historical, so there is nothing to collapse.