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 in the catalog 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
news.item.updatedfeednewsnews.item.updated
news.item.removedfeednewsnews.item.removed
event.item.addedfeedeventsevent.item.added
event.item.updatedfeedeventsevent.item.updated
event.item.removedfeedeventsevent.item.removed
factcheck.completedorganization—factcheck.completed
factcheck.failedorganization—factcheck.failed
factcheck.status.changedorganization—factcheck.status.changed

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.

Event feed changes​

An event in an event feed produces, over its life in that feed:

  • event.item.added once, when it enters the feed.

  • event.item.updated each time something you can see in the feed changes. data.changedFields lists which parts changed in this update, compared with the previous update, always at least one:

    • title and description (the description or short description);
    • when: dates or times changed;
    • where: the location changed;
    • links: the URLs of the event or its sources (who published what) changed;
    • status: the event was cancelled or restored;
    • media: the cover image or the gallery (no image links are sent, so fetch the item for the current ones).

    price is reserved and never sent. A change you cannot see in the feed, such as the same page crawled again with nothing new, sends nothing. One visible change is not announced either: an image's usage changing while the cover and gallery keep the same images in the same order. Read usage each time you fetch the item; you fetch it for the current image links anyway.

    Treat changedFields as a hint, not a complete record. Debounce can collapse several updates to one item into the newest one, and only that update's changedFields arrives. For example, a cancellation followed shortly by a rename can arrive as a single update with changedFields: ["title"]. On every event.item.updated, fetch the item again (GET /feeds/events/{feedId}/items/{id}) and read the current values rather than applying only the listed fields.

  • event.item.removed when it leaves the feed: it no longer passes the feed's filters (the completeness opt-in filter among them), or the safety floor excludes it. It is also sent for every item when the feed is deleted (see Feed deletion). Its subject and data.eventId are the event id, and it carries no feed item id: match removals on eventId.

A cancelled event stays in the feed. The cancellation is sent as an event.item.updated, usually with "status" in changedFields, but debounce may fold it into a later update. The item you fetch again always shows it: the item's top-level status is "cancelled". If you show events to people, read that status rather than assuming an item in the feed is going ahead.

An event that leaves a feed and later matches it again is a new item: it gets a new item id and a new event.item.added.

Removals shift offset-paginated item listings: if an item is removed while you page through GET /feeds/events/{feedId}/items with limit and offset, the later items move up one place, and you can skip one.

News feed changes​

An article or post in a news feed produces, over its life in that feed:

  • news.item.added once, when it enters the feed. It fires at most once per (feedId, news item) for each time the item enters the feed, 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.

  • news.item.updated each time something you can see in the feed changes. data.changedFields lists which parts changed in this update, compared with the previous update, always at least one: title, description, url, publishedAt, publisher, author, original, about (the places the item is about) and media (the images, their order, or an image's kind or credit). A publisher editing an article updates the same item; it does not arrive as a new one. A change you cannot see in the feed sends nothing. A re-signed image link, or an image leaving the feed's image window, is not a change. A place that we newly name on an item shows on the item at once and is announced as about with the item's next change.

    Treat changedFields as a hint, not a complete record. Debounce can collapse several updates to one item into the newest one. On every news.item.updated, fetch the item again (GET /feeds/news/{feedId}/items/{id}) and read the current values.

  • news.item.removed when it leaves the feed: it no longer passes the feed's filters, or the safety floor excludes it. It is also sent for every item when the feed is deleted (see Feed deletion). Its subject is the article id (newsPageId), and data.id is the feed item id you received in news.item.added: match removals on data.id.

An item you were never sent news.item.added for is never removed: you get no removal for something you never received.

An article that leaves a feed and later matches it again is a new item: it gets a new item id and a new news.item.added.

Removals shift offset-paginated item listings: if an item is removed while you page through GET /feeds/news/{feedId}/items with limit and offset, the later items move up one place, and you can skip one.

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 a feed also publishes a removal for every item that was in it: event.item.removed for each event in an event feed, and news.item.removed for each article in a news feed that you were sent news.item.added for.

These removals are not sent at the moment of deletion. They are recorded with the deletion and delivered by a background sweep, which runs every 15 minutes and picks up removals at least 10 minutes old: expect them about 10 to 30 minutes after the feed is deleted.

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 cannot be replayed (409): replay the delivery that superseded it.

Backfilled deliveries (see Reliability) skip debounce entirely — the events are already historical, so there is nothing to collapse.

Next steps​