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 in the catalog 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 |
news.item.updated | feed | news | news.item.updated |
news.item.removed | feed | news | news.item.removed |
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 |
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.addedonce, when it enters the feed. -
event.item.updatedeach time something you can see in the feed changes.data.changedFieldslists which parts changed in this update, compared with the previous update, always at least one:titleanddescription(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).
priceis 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'susagechanging while the cover and gallery keep the same images in the same order. Readusageeach time you fetch the item; you fetch it for the current image links anyway.Treat
changedFieldsas a hint, not a complete record. Debounce can collapse several updates to one item into the newest one, and only that update'schangedFieldsarrives. For example, a cancellation followed shortly by a rename can arrive as a single update withchangedFields: ["title"]. On everyevent.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.removedwhen it leaves the feed: it no longer passes the feed's filters (thecompletenessopt-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). Itssubjectanddata.eventIdare the event id, and it carries no feed itemid: match removals oneventId.
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.addedonce, 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.updatedeach time something you can see in the feed changes.data.changedFieldslists 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) andmedia(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 asaboutwith the item's next change.Treat
changedFieldsas a hint, not a complete record. Debounce can collapse several updates to one item into the newest one. On everynews.item.updated, fetch the item again (GET /feeds/news/{feedId}/items/{id}) and read the current values. -
news.item.removedwhen 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). Itssubjectis the article id (newsPageId), anddata.idis the feed itemidyou received innews.item.added: match removals ondata.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:
| 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 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
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
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.