Skip to main content

Concepts

Deliveries are signals, not source of truth

caution

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:

  1. 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.
  2. 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.
  3. 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:

FieldDescription
specversionAlways "1.0".
typeThe event type, e.g. news.item.added. See Event type taxonomy.
sourceThe 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.
idThe delivery's unique identifier. Stable across retry attempts — use it as your idempotency key.
timeWhen the underlying change happened, ISO 8601. Not the delivery time.
datacontenttypeAlways "application/json".
dataThe 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"
}
}
Idempotency

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:

TypeScopeFeed typePage
news.item.addedfeednewsnews.item.added
event.item.addedfeedeventsevent.item.added
event.item.updatedfeedeventsevent.item.updated
event.item.removedfeedeventsevent.item.removed
factcheck.completedorganizationfactcheck.completed
factcheck.failedorganizationfactcheck.failed
factcheck.status.changedorganizationfactcheck.status.changed
Not currently emitted

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:

PatternMatches
*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.addedExact 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:

scopeKindMatches
allEvery feed-scoped event the organization can see, of any feed type, plus every organization-scoped event.
feed_typeEvery event from feeds of one feedType (news or events).
feedsOnly 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 debounceWindowSeconds afterwards 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.

Next steps