where predicate.
Collections are the folders. Every source belongs to exactly one. Naming a
collection that does not exist creates it.
Discover a source
POST /v1/feeds/search finds public feeds. It does not subscribe to anything.
Pass mode: "site" to ask what one URL publishes; omit mode (or pass
"feeds") to search by topic.
Response (abridged)
feedUrl values straight to POST /v1/feeds/follow. The response also
echoes the query and mode it ran.
Follow it
POST /v1/feeds/follow takes up to 20 URLs in one call. Each URL gets its own
outcome. Partial success is the normal result, not an error: one dead URL never
discards the nineteen that worked.
Follow two feeds into a collection
Response
outcomes on every call. followed counts only the successes.
Discovery failures report invalid_url, feed_not_found, feed_too_large, or
feed_unavailable in error.code — the first is worth correcting, the last is
worth retrying.
Keep source_id: it is what where.source.include takes.
A site URL is resolved to the feed it publishes, so you can pass
https://www.nasa.gov and https://www.nasa.gov/feed/ interchangeably.
For a topic with no publisher feed, POST /v1/feeds/inputs/news-query
creates a durable query input that behaves like any other source.Read what arrived
POST /v1/feeds/query reads the stream. Every argument is optional. “What’s
new?” is {"since": "1d"}.
Catch up on the last day
Response
Narrow with where
where fields AND together. Omit a field to not filter on it.
Use
since for “what’s new” and discovered_after for diffs. Backfill makes an
old post look newly discovered, so discovered_after is the wrong tool for a
human-facing “today” view.
Control the size of the answer
limitdefaults to 20 and caps at 100. Page withnext_cursoruntil it isnull. Never build a cursor yourself.per_source_limit(1–20) caps entries per source across the whole result, so one loud publisher cannot own the page. The response then also reportsomitted.by_per_source_limit.fieldsprojects entries down to what you use.["id"]is the cheapest diff.idis always included.format: "compact"returns token-lean text lines intextand leavesentriesempty. Each line carries every id a follow-up call needs.
Response with format
Ask about sources instead of entries
select: "sources" returns per-source health. It accepts only where.collection
and where.source; use having for health filters such as
{"silent_days": {"gte": 30}}. select: "aggregate" returns grouped counts,
with group_by of source, collection, or day.
Fetch status and publisher activity are separate facts. A source can be fetching
perfectly and still be stalled, because stalled means the publisher has gone
quiet. Do not unfollow on health alone.
Catch up on what is new
POST /v1/feeds/query answers “what is there”. POST /v1/feeds/changes answers
“what changed since I last looked”. It is the polling primitive, and it has two
checkpoint owners: you, or smry.
This section covers the caller-managed cursor, where your code stores the
position. If your agent is stateless — a cron job, a scheduled worker, a
function with no database — use a managed consumer instead.
Response
- A cursor is bound to its predicate. Store
next_cursortogether with the exactwhereyou sent. Changingwhereinvalidates the cursor and returns400 invalid_request; restart that stream withcursor: "latest". - Keep polling while
has_moreistrue.limitdefaults to 100 and caps at 500. - Branch on
event.type.entry.discoveredmeans smry saw the entry for the first time.entry.classifiedmeans a collection filter reached a decision about an entry you already have. Ignore event kinds you do not recognize rather than rejecting the batch. - Read the
attentionblock. It reports source health that unread counts never reveal:stalled(the publisher went quiet),erroring(fetching failed), andnever_produced. Each bucket lists five sources by default and the*_countfields always carry the full tally.attention_limitaccepts 0 (counts only) through 500.
Write back
POST /v1/feeds/mutate is the selector-based write path. It previews by
default, and that preview is the safety mechanism: you see what a selector
matches before it changes anything.
1
Dry run
dry_run defaults to true. Nothing changes. You get would_affect, a
sample of up to ten labels, and a commit_token.Response
2
Check would_affect
If the number is not what you expected, narrow the selector and run another
dry run. Do not commit a token you did not read the preview for.
3
Commit
Send the same
op and the commit_token. Omit where and set —
the plan is already recorded server-side.Response
4
Check partial and every outcome
partial: true means at least one target failed and the rest succeeded.
Read each outcomes[].ok and outcomes[].error. affected counts only
successes.409 commit_in_progress — retry shortly for the recorded
outcome, and verify with a query before starting a fresh dry run.
Set dry_run: false to apply immediately, in one call. Use it when your code
already knows the exact target ids; prefer preview-then-commit everywhere else.
The operations and what each one requires
sources.* reject entry-level filters (match, unread, dates,
classification) rather than silently ignoring them. An empty selector is
refused outright, because it would target every source. Deleting a collection
moves its sources to Unsorted; it does not unfollow them.
For plain collection CRUD, GET/POST /v1/feeds/collections and
DELETE /v1/feeds/collections/{id} are more direct than a selector mutation.
Unsorted is built in: deleting it returns 409 collection_protected.
Teach the feed
POST /v1/feeds/feedback records an explicit signal on one entry.
less_like_this hides an entry that the collection filter had included;
more_like_this records preference; null undoes either.
Response
filterChanged is always false. Feedback is separate context and never
silently rewrites a collection’s filter — change a filter through
POST /v1/feeds/filters/preview and POST /v1/feeds/filters/apply, which
require you to see the classified sample before saving it.
Export a slice
POST /v1/feeds/export returns everything matching a predicate — up to 10,000
rows — as newline-delimited JSON, with the row count in the X-Export-Rows
response header.
Each line of entries.ndjson
Next
Managed consumers
Let smry hold the read position so a scheduled agent stores nothing.
Watch a page for changes
Turn a page that publishes no feed into a source you can query.
Save what matters
Move an entry into the library, tag it, and search it later.