Skip to content

HTTP API

All endpoints are JSON under /api/v1. Errors return {"error": {"code": "...", "message": "..."}} with a 4xx/5xx status. The server binds 127.0.0.1 by default; see Hosting.

EndpointDescription
GET /healthLiveness probe, returns {"ok": true}
EndpointDescription
GET /feedsAll subscriptions with unread counts and fetch status
POST /feedsSubscribe. Body {"url": "…"}. Returns 201 with the feed, or 200 with a feeds[] choice list when the URL hosts several feeds. With "credentialId", discovery is skipped: the URL must be the feed itself, on the credential’s origin
POST /feeds/discoverPreview discoverable feeds at a URL without subscribing
POST /feeds/importOPML import. Body {"opml": "<xml…>"}. Returns 201 with added[] and skipped[]; first fetches run in the background. Capped at 500 feeds
POST /feeds/import/youtubeYouTube import. Body {"csv": "…"} — Google Takeout’s subscriptions.csv. Each channel becomes its feeds/videos.xml?channel_id= feed. Same response and cap as OPML import
DELETE /feeds/:idUnsubscribe
POST /feeds/:id/refreshPoll now. Returns {"newArticles": n}
POST /feeds/:id/mark-all-readMark every article in the feed read
EndpointDescription
GET /articlesCursor-paginated list. Query: feed_id, list_id, category, before, before_id, limit. Returns {articles, nextCursor}
GET /articles/:idFull record, including listIds and snoozedUntil
POST /articles/:id/readBody {"read": true | false}
POST /articles/:id/snoozeBody {"until": ISO8601 | null} — null unsnoozes
GET /articles/:id/embeddableWhether the article’s page can be shown in an iframe: {"embeddable": true | false | null, "reason"}. Reads the page’s X-Frame-Options and CSP frame-ancestors; null when the page could not be checked. Cached per URL
GET /articles/:id/transcriptPodcast transcript, fetched from the publisher and normalized: {"kind": "cues", "cues": [{start, text, speaker}]}, {"kind": "html", html} or {"kind": "text", text}. 404 when the episode has none, 502 transcript_unavailable when the file cannot be fetched or read
GET /articles/:id/chaptersPodcast chapters: {"chapters": [{start, title, url, img}]}. 404 / 502 chapters_unavailable as above

Articles carry media ({url, type} of a playable enclosure) in lists too; transcript and chaptersUrl come with the full record.

EndpointDescription
GET /realtimeOutbound stream state: each socket’s name, redacted URL, state (connecting | open | waiting | closed), message and reconnect counts, last error; plus Podping, Jetstream and Mastodon summaries
EndpointDescription
GET /categoriesSubject counts. Query: feed_id to scope to one feed
EndpointDescription
GET /listsAll saved lists
POST /listsCreate. Body {"title", "visibility": "public" | "private"}
DELETE /lists/:idDelete the list (articles are untouched)
POST /lists/:id/itemsBody {"articleId"} — save an article
DELETE /lists/:id/items/:articleIdRemove an article
GET /lists/:token.xmlRSS for a public list — the sharing URL
EndpointDescription
GET /ingestorsAll ingestors with status and pending digest counts
POST /ingestorsCreate. Body: kind (mastodon | bluesky | reddit | composite), config, optional fetchIntervalMin, digestMode (realtime | hourly | daily), filterThreshold, llmEnabled
PATCH /ingestors/:idUpdate interval, digest mode, threshold, LLM flag. credentialId attaches a connected account of the ingestor’s platform; null detaches it
DELETE /ingestors/:idRemove the ingestor
POST /ingestors/testRun one fetch cycle; returns kept and dropped statuses with scores

Credential fields in ingestor configs are accepted but redacted from every response. Mastodon and Reddit configs take a credentialId instead of secrets; Mastodon also takes "timeline": "home", which requires one.

EndpointDescription
GET /credentialsConnected accounts and feed sign-ins: provider, kind (basic | bearer | oauth2), label, origin, usedBy. Never the secret
POST /credentialsBasic or bearer sign-in for a feed. Body {"kind": "basic", "url", "username", "password"} or {"kind": "bearer", "url", "token"}. Bound to the URL’s origin
DELETE /credentials/:idRemove. 409 credential_in_use while a feed or ingestor uses it
POST /oauth/startBegin an OAuth sign-in. Body {"provider": "mastodon", "instance"}, {"provider": "reddit", "clientId", "clientSecret"}, or {"provider": "generic", "feedUrl", "authorizeUrl", "tokenUrl", "clientId", "clientSecret"?, "scope"?}. Returns {"authorizeUrl"}. Must come from the browser: the redirect URI is built from the Origin header
GET /oauth/callbackWhere the provider sends the browser back. Stores the credential and posts the result on the reader-oauth BroadcastChannel