{"openapi":"3.1.0","info":{"title":"Newsmind API","description":"RSS feeds for your AI assistant. Read articles, search across feeds, manage subscriptions, and resume where you left off across conversations.\n\nRetention: tools that discover articles (unread queue, keyword and semantic search, what's-new cursor, aggregate counts) and account-level counts are filtered to the caller's plan retention window — Pro 30 days, Max 90 days, based on each article's published_at timestamp. Articles past the window are invisible to discovery; storage prunes them at 90 days. Articles published before the user subscribed to a feed (or before a subscription reactivation after a lapse) are also filtered out. Lookup by article ID is bound the same way as of v0.28.0: get_article, set_read_state and set_starred accept an ID only if it is in a feed you subscribe to AND inside your window. Removing a read or a star is always allowed. Stars do not bypass retention.","version":"1.0.0"},"servers":[{"url":"https://newsmind.app/api/v1"}],"components":{"securitySchemes":{"oauth2":{"type":"oauth2","flows":{"authorizationCode":{"authorizationUrl":"https://newsmind.app/oauth/authorize","tokenUrl":"https://newsmind.app/oauth/token","scopes":{"mcp":"Read and manage your RSS feeds and articles."}}}}},"schemas":{"Feed":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string","nullable":true},"url":{"type":"string"},"site_url":{"type":"string","nullable":true},"folder":{"type":"string","nullable":true},"last_polled_at":{"type":"string","format":"date-time","nullable":true},"error_count":{"type":"integer"},"last_error":{"type":"string","nullable":true},"host_errors":{"type":"object","nullable":true,"description":"Present when this feed's last poll ended in an error and the user holds other feeds on the same host: {host, feeds_on_host, erroring_now, shared_error?, note} — how many of them ended their last poll in an error too, and whether they share the message. Every feed on a host erroring the same way is the host, not the feed."}}},"Article":{"type":"object","properties":{"id":{"type":"integer"},"feed_id":{"type":"integer","description":"ID of the feed this article belongs to. Useful for chaining into list_feeds or unsubscribe."},"feed_title":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"url":{"type":"string","nullable":true},"image_url":{"type":"string","format":"uri","nullable":true,"description":"Thumbnail URL for the article, when available. HTTPS-only; tracking pixels filtered out. Sourced from <enclosure>, <media:thumbnail>, <media:content>, or first <img> in content. Forward-only — articles ingested before 0.3.0 have null."},"image_width":{"type":"integer","nullable":true,"description":"Pixel width of image_url, when the source feed exposed it (MRSS attrs or HTML width=). Lets clients lay out before fetching the bytes. Null when unknown."},"image_height":{"type":"integer","nullable":true,"description":"Pixel height of image_url, when known. Null otherwise."},"image_mime":{"type":"string","nullable":true,"description":"MIME type of image_url (image/jpeg, image/png, image/webp, image/gif, image/avif, image/svg+xml). Sourced from <enclosure type=>, <media:content type=>, or inferred from URL extension. Null when neither available."},"summary":{"type":"string","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true,"description":"When the source published the article (from the RSS feed)."},"added_at":{"type":"string","format":"date-time","nullable":true,"description":"When Newsmind ingested the article into the user's feed. Use this when the user asks about 'when did this show up for me' or 'what's new since I last checked'; use published_at for 'how old is the story'."},"matched_watches":{"type":"array","description":"Saved watches this article matched. Present on unread/new-article responses when the user has active watches.","items":{"type":"object","properties":{"watch_id":{"type":"integer"},"label":{"type":"string","nullable":true}}}},"likely_archival":{"type":"object","description":"Present when the title states a date a year or more before published_at — an aggregator re-indexed an old page under today's date. Don't present these as new.","properties":{"title_year":{"type":"integer"},"matched_text":{"type":"string"},"pattern":{"type":"string","enum":["slash","written","paren"]}}},"also_in":{"type":"array","description":"Other feeds that carried this same story. The duplicate was collapsed to one match (issue #59).","items":{"type":"object","properties":{"feed_id":{"type":"integer"},"feed_title":{"type":"string","nullable":true}}}},"user_excluded":{"type":"object","description":"This row matched one of the watch's exclude_terms. Demoted, never dropped, and the term is named so an over-broad guess can be corrected (issue #23).","properties":{"term":{"type":"string"}}},"low_signal":{"type":"object","description":"Present when a Google News item's source is an encyclopedia, film database, statistics page or obituary listing rather than reporting. `rule: \"obituary\"` comes from a headline rule and may name a legitimate newsroom — describe the item, not the outlet.","properties":{"source":{"type":"string"},"rule":{"type":"string","enum":["reference","entertainment_db","sports_stats","obituary","archive_page","scraped_video"]}}}}},"Watch":{"type":"object","description":"A saved-search definition. kind is keyword (phrase matching) or semantic (Max only, matching by meaning). delivery routes matches in-session, to email digests, or to a webhook; email/webhook take a cadence and optional delivery slot (hour + weekday, in the account's timezone). Stale keyword-only shape corrected in v0.42.0 — it had lagged reality since v0.22.","properties":{"watch_id":{"type":"integer"},"query":{"type":"string"},"label":{"type":"string","nullable":true,"description":"Optional human-readable name. Returned as-is and null when unset; only the email digest substitutes the query."},"feed_id":{"type":"integer","nullable":true,"description":"Optional scope to one feed."},"folder":{"type":"string","nullable":true,"description":"Optional scope to a subscription folder."},"kind":{"type":"string","enum":["keyword","semantic"]},"threshold":{"type":"number","nullable":true,"description":"Semantic only: cosine similarity cutoff."},"active":{"type":"boolean"},"delivery":{"type":"string","enum":["in_session","email","webhook"]},"cadence":{"type":"string","nullable":true,"enum":["realtime","weekdays","daily","weekly"]},"require_terms":{"type":"array","nullable":true,"items":{"type":"string"},"description":"issue #23: OR-list an article must also carry. null = no filter."},"exclude_terms":{"type":"array","nullable":true,"items":{"type":"string"},"description":"issue #23: OR-list that demotes a match at read time. Never deletes."},"delivery_hour":{"type":"integer","nullable":true,"minimum":0,"maximum":23,"description":"Digest delivery hour in the account's timezone; null = default 08:00."},"delivery_day":{"type":"string","nullable":true,"description":"Weekly digests: weekday name; null = default monday."},"webhook_url":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"backfilled_matches":{"type":"integer","description":"create: in-window articles that already matched."},"match_sources":{"type":"array","items":{"type":"string"},"description":"create: publishers this watch already matched. Unrelated publishers suggest the query is ambiguous."},"threshold_preview":{"type":"object","additionalProperties":{"type":"integer"},"description":"Semantic only: cutoff -> surviving match count, for picking a stricter threshold from data."},"rebackfilled_matches":{"type":"integer","description":"update: match count after an edit that rebuilt the set."},"rebackfilled_because":{"type":"string","enum":["query","threshold","scope","require_terms"],"description":"update: which edit rebuilt the match set."},"zeroed_by":{"type":"string","enum":["query","threshold","scope","require_terms"],"description":"update: present when the edit emptied a watch that previously had matches - usually an over-narrow edit."},"cadence_inert":{"type":"boolean","description":"update: a cadence was set on an in_session watch, where it schedules nothing."},"settled_backlog":{"type":"integer","description":"update: pending matches settled rather than emailed when delivery is sped up (in_session to email, or to a faster cadence), so the first digest is not the slower setting's backlog. Still readable via get_watch_matches."},"settled_backlog_message":{"type":"string","description":"update: accompanies settled_backlog - what was dropped from email and where it is still readable."},"ok":{"type":"boolean"},"message":{"type":"string"},"error":{"type":"string","description":"Set instead of a watch when the call was refused: max_required (semantic on a non-Max plan), invalid_webhook_url (not https, or a loopback/private/link-local host), watch_limit_reached, embedding_failed."}},"required":["watch_id","query","kind","active","delivery","created_at"]},"WatchWithCount":{"type":"object","description":"A Watch row plus a count of unseen matches right now.","properties":{"watch_id":{"type":"integer"},"query":{"type":"string"},"label":{"type":"string","nullable":true},"feed_id":{"type":"integer","nullable":true},"folder":{"type":"string","nullable":true},"kind":{"type":"string","enum":["keyword","semantic"]},"threshold":{"type":"number","nullable":true},"active":{"type":"boolean"},"delivery":{"type":"string","enum":["in_session","email","webhook"]},"cadence":{"type":"string","nullable":true,"enum":["realtime","weekdays","daily","weekly"]},"require_terms":{"type":"array","nullable":true,"items":{"type":"string"}},"exclude_terms":{"type":"array","nullable":true,"items":{"type":"string"}},"delivery_hour":{"type":"integer","nullable":true,"minimum":0,"maximum":23},"delivery_day":{"type":"string","nullable":true},"webhook_url":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"new_match_count":{"type":"integer","nullable":true,"description":"Unseen matches, counted as stories exactly as get_watch_matches groups them. Null when the count could not be read on this call (the response's unavailable says so) — unknown, never a guessed 0."}},"required":["watch_id","query","kind","active","delivery","created_at","new_match_count"]},"WatchSummary":{"type":"object","description":"Compact form returned in the watch_summaries field on get_new_articles. No articles — assistant calls get_watch_matches for detail.","properties":{"watch_id":{"type":"integer"},"label":{"type":"string","nullable":true},"query":{"type":"string"},"new_match_count":{"type":"integer"}},"required":["watch_id","query","new_match_count"]},"FeedWarnings":{"type":"object","description":"Failing feeds — 3+ consecutive fetch failures AND nothing delivered in 48h. The delivery clause keeps a host that merely flaps out of this list. Returned on get_account_info and get_new_articles, only when there is something to report. failing_samples is capped at 5; get_feed_health has the full picture.","properties":{"failing_count":{"type":"integer","description":"Total subscribed feeds that have failed 3+ fetches in a row and returned no items for 48 hours. They are still retried (#368)."},"note":{"type":"string","description":"Plain-language statement of what the count means: these feeds keep failing and have returned no items for 48 hours, and they are still retried. v0.55.0; #368."},"failing_hosts":{"type":"array","maxItems":10,"description":"Every host failing 2+ of the user's feeds, biggest group first. Turns a count into a shape: 42 feeds on one host that answers 451 is one breakage, not 42. Absent when no host is failing more than one feed. v1.259.0 (#327).","items":{"type":"object","properties":{"host":{"type":"string","description":"Hostname, spelled as the feeds' last_error text spells it."},"feed_count":{"type":"integer","description":"How many of this user's failing feeds are on this host."},"shared_error":{"type":"string","description":"The error text every failing feed on this host reports. DESCRIPTIVE, not causal — it does not claim one upstream cause, which matters on shared feed hosts where each feed has a different owner. Absent when the feeds report different errors."}},"required":["host","feed_count"]}},"failing_samples":{"type":"array","maxItems":5,"description":"Up to 5 sample failing feeds, round-robined across hosts with the biggest group first — so the samples span causes rather than repeating one dead host. v1.259.0 (#327); previously the 5 highest error_counts.","items":{"type":"object","properties":{"feed_id":{"type":"integer"},"title":{"type":"string","nullable":true},"url":{"type":"string","description":"The address that stopped working — so a client can search for the publication's current feed without a second get_feed call. v1.165.0 (#199)."},"diagnosis":{"type":"string","description":"Facts about the publisher, established once for this feed and identical for every subscriber: whether the site still answers and whether it advertises a feed elsewhere. States what is true, never what to subscribe to. Absent until diagnosed. v1.166.0 (#199)."},"last_error":{"type":"string","nullable":true,"description":"Most recent error text from the poller (HTTP status, parser error, etc.)."}},"required":["feed_id","url"]}}},"required":["failing_count","failing_samples"]},"ImportFailures":{"type":"object","description":"Import-failure notice — feeds the user asked to import that failed in the last window_days days. Returned on the discovery tools and get_account_info only when non-zero. Two states: `import_failures` (permanent, relay them and offer next steps) and `import_retrying` (a retry is still scheduled, nothing to do — do not raise it unprompted). Call get_import_status for failed URLs and reasons. v0.33.0, retry state documented v1.150.0 (#173).","properties":{"state":{"type":"string","enum":["import_failures","import_retrying"],"description":"`import_retrying` means every recent failure still has a retry scheduled — nothing is lost and there is nothing for the user to do. Emitted since v0.54.0 and undocumented until v1.150.0 (#173)."},"failed_imports":{"type":"integer","description":"Permanently-failed imports inside the window. Zero on an `import_retrying` notice."},"retrying_imports":{"type":"integer","description":"Failures with a retry still scheduled. Omitted when zero. v1.150.0 (#173)."},"window_days":{"type":"integer","description":"Recency window in days (currently 7)."},"message":{"type":"string","description":"Assistant-directed instruction: relay the failures and offer next steps."}},"required":["state","failed_imports","window_days","message"]},"ImportRecovered":{"type":"object","description":"Import-recovery notice — feeds the user asked to import that failed, were re-validated after a discovery fix shipped (#161), and have now been added automatically. Returned alongside the other notices only when non-zero. Worth mentioning ONCE: the user was previously told these feeds could not be added, so a feed appearing on its own is otherwise confusing. Nothing is required from them. v1.151.0 (#162).","properties":{"state":{"type":"string","enum":["import_recovered"]},"recovered_imports":{"type":"integer","description":"Distinct URLs recovered inside the window."},"urls":{"type":"array","items":{"type":"string"},"description":"Up to 10 of them, so the assistant can name names. May be fewer than recovered_imports."},"window_days":{"type":"integer","description":"Recency window in days (currently 7)."},"message":{"type":"string","description":"Assistant-directed instruction."}},"required":["state","recovered_imports","urls","window_days","message"]},"SearchScope":{"type":"object","description":"Present only when the caller's feeds hold more in-window articles than one search can cover (#303). Everything published since `since` was searched; older articles were not. Surface the note when present.","properties":{"since":{"type":"string","format":"date-time","description":"ISO timestamp; articles published before it were not searched."},"bounded":{"type":"boolean","description":"Always true when the field is present."},"articles_searched":{"type":"integer","nullable":true,"description":"Semantic search: how many of the caller's articles the scan read (the newest ones)."},"matches_ranked":{"type":"integer","nullable":true,"description":"Keyword search: how many caller-visible matches the rank was chosen from."},"note":{"type":"string","description":"One sentence for the user, ready to relay."}},"required":["since","bounded","note"]},"RetentionMeta":{"type":"object","description":"Tier-based retention window for the caller. Articles older than cutoff_at are invisible to discovery (search, unread, counts) regardless of query. cutoff_hit is set when retention likely truncated the result set — surface to the user when present.","properties":{"window_days":{"type":"integer","description":"Retention window in days. Pro = 30, Max = 90."},"cutoff_at":{"type":"string","format":"date-time","description":"ISO timestamp; articles with published_at < cutoff_at are filtered out."},"cutoff_hit":{"type":"boolean","nullable":true,"description":"Present (and set to true) only when retention likely bounded the result set: user asked for older data than retention covers, query came back empty for a multi-term ask, or the oldest returned article is right at the wall. Absent on queries where retention wasn't the limiter."}},"required":["window_days","cutoff_at"]}}},"security":[{"oauth2":["mcp"]}],"paths":{"/get_feeds":{"post":{"operationId":"get_feeds","summary":"Get feeds","description":"Returns every RSS feed the user is subscribed to, with title, URL, last poll time, and any error status. A feed whose last poll errored may carry host_errors: how many of the user's feeds on that host errored too, and whether they share the message.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"feeds":{"type":"array","items":{"$ref":"#/components/schemas/Feed"}}},"required":["feeds"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_account_info":{"post":{"operationId":"get_account_info","summary":"Get account info","description":"Caller introspection: email, tier (pro/max/expired), subscription status, billing period end, trial end, feed limit, current feed count. Use whenever the user asks about their account, plan, billing, trial, or feed quotas. Read-only.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","nullable":true},"tier":{"type":"string","description":"pro, max, or expired"},"status":{"type":"string","nullable":true,"description":"Stripe subscription status: pending, trialing, active, past_due, canceled, unpaid, incomplete, incomplete_expired"},"selected_plan":{"type":"string","nullable":true},"selected_cycle":{"type":"string","nullable":true},"current_period_end":{"type":"string","format":"date-time","nullable":true},"renewal_date":{"type":"string","format":"date-time","nullable":true,"description":"Friendlier alias for current_period_end. Same value."},"trial_ends_at":{"type":"string","format":"date-time","nullable":true},"cancel_at_period_end":{"type":"boolean"},"feed_limit":{"type":"integer","nullable":true},"feed_count":{"type":"integer","description":"Number of feeds the user is subscribed to."},"article_count":{"type":"integer","description":"Articles in the user's subscribed feeds that are within their tier retention window (Pro 30d / Max 90d). Reflects what's reachable via discovery tools — not raw storage. Updated in 0.3.5 to respect retention."},"star_count":{"type":"integer","description":"Articles the user has starred that are still within their tier retention window. Stars on articles past retention are not counted (those articles are invisible to discovery anyway). Updated in 0.3.5 to respect retention."},"import_pending":{"type":"integer","description":"Pending feeds in the user's OPML-import queue, drained in the background by the poll-batch worker. Added in v0.14.0."},"feed_warnings":{"$ref":"#/components/schemas/FeedWarnings","nullable":true,"description":"Present only when one or more feeds have failed 3+ fetches in a row AND delivered nothing for 48h; absent on healthy accounts. v0.21.0, delivery clause v1.117.1."},"import_failures":{"$ref":"#/components/schemas/ImportFailures","nullable":true,"description":"Present only when feed imports failed permanently in the last 7 days; absent when there's nothing to report. v0.33.0."},"import_recovered":{"$ref":"#/components/schemas/ImportRecovered","nullable":true,"description":"Present only when a previously-failed import has recovered and been added in the last 7 days. v1.151.0 (#162)."},"manage_url":{"type":"string","format":"uri"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_import_status":{"post":{"operationId":"get_import_status","summary":"Get import status","description":"Returns the user's pending / succeeded / failed import counts (import_opml, or a subscribe_to_feed that answered queued) plus the most recent failures, and the addresses subscribe_to_feed refused this week: rechecking, added or refused. Report counts to the user, not a completion time.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"pending":{"type":"integer","description":"Feeds still waiting for the worker to validate + insert."},"succeeded":{"type":"integer","description":"Feeds successfully imported in the last 24 hours."},"failed":{"type":"integer","description":"Feeds the worker rejected, all-time (validation error — dead URL, non-feed content, etc.)."},"failed_recent":{"type":"integer","description":"Failed imports inside the last 7 days — the recency window the import_failures notice uses. v0.33.0."},"retrying":{"type":"integer","description":"Failures that still have an automatic retry scheduled. NOT losses — the worker will validate them again, so do not offer to re-add them by hand. Undocumented here since v0.54.0; added v1.150.0 (#173)."},"next_retry_at":{"type":"string","format":"date-time","nullable":true,"description":"When the earliest scheduled retry runs. Omitted when nothing is scheduled. v1.150.0 (#173)."},"eta":{"type":"string","description":"Queue STATE, not a time: \"queue is empty\" or \"still importing\". Deliberately carries no duration — the queue is shared across customers and most of the wall-clock is publishers' servers responding. Report the pending/succeeded/failed counts to the user, never a completion time."},"advice":{"type":"string","nullable":true,"description":"Present when failed_recent > 0: assistant-directed instruction to relay the failed URLs and reasons and offer next steps. v0.33.0."},"failure_summary":{"type":"object","nullable":true,"description":"Present only when there are failures. Counts failures by cause rather than the 10 in recent_failures, over at most 500 rows — if failure_summary_truncated is present these buckets are a sample of failed, not a census. publisher_blocks, domain_gone and malformed_feed cannot be fixed by anyone; address_not_found and no_feed_at_site can still be rescued by a different address. Zero buckets are absent, not 0. v1.144.0 (#150), capped disclosure v1.149.0 (#174).","properties":{"publisher_blocks":{"type":"integer","description":"The publisher refuses automated readers (401/403/406, or a bot check that needs a browser). Nothing to fix on either side. A rate limit is not a block and is counted under temporary. v1.159.0 (#191), reworded v1.235.0 (#268)."},"address_not_found":{"type":"integer","description":"404/410 — the address is gone. The server has already tried the site's homepage."},"no_feed_at_site":{"type":"integer","description":"The page loaded but advertises no feed anywhere, after full discovery."},"domain_gone":{"type":"integer","description":"The domain no longer resolves."},"malformed_feed":{"type":"integer","description":"The address serves a feed, but its XML is broken. The address is right; the publisher's file is not. Nothing a different URL fixes. v1.147.0 (#177)."},"temporary":{"type":"integer","description":"Timeout, network failure, or a rate limit (HTTP 429 — retried, never treated as a block, since #228). The retry ladder re-drives these WHILE RETRIES REMAIN; a row whose message says we have stopped retrying has exhausted them and will not be re-driven until a validator change reopens it. v1.159.0 (#191)."},"other":{"type":"integer","description":"Unclassified."}}},"refused_subscribes":{"type":"array","description":"Present only when subscribe_to_feed refused an address in the last 7 days. One entry per address, its latest refusal, newest first, at most 20. v1.281.7 (#367).","items":{"type":"object","properties":{"url":{"type":"string"},"refused_at":{"type":"string","format":"date-time"},"state":{"type":"string","enum":["rechecking","added","refused"],"description":"rechecking: a further check is scheduled, daily this week if needed, and the feed is added on its own if it answers — nothing for the user to do. added: it was added after a re-check. refused: no further check is scheduled."},"added_at":{"type":"string","format":"date-time","description":"When the feed was added. Present only when state is added."},"reason":{"type":"string","description":"The first refusal's reason, as subscribe_to_feed reported it."},"http_status":{"type":"integer","description":"Present when the first refusal carried one."},"message":{"type":"string","description":"The first refusal's human-readable message."},"last_checked_at":{"type":"string","format":"date-time","description":"When the address was last checked again, if it has been. Absent until then."},"recheck_reason":{"type":"string","description":"What the last re-check found, when one ran and was refused: its reason. Absent otherwise."},"recheck_status":{"type":"integer","description":"That re-check's HTTP status, when it had one."},"recheck_message":{"type":"string","description":"That re-check's human-readable message."}}}},"refused_subscribes_note":{"type":"string","description":"Present with refused_subscribes: what each state means and what to tell the user. v1.281.7 (#367)."},"refused_subscribes_unavailable":{"type":"boolean","description":"Present, true, only when the refused-subscribes read failed: an absent refused_subscribes then means nothing, not that nothing was refused. v1.281.7 (#367)."},"failure_summary_truncated":{"type":"boolean","description":"Present only when failure_summary covered fewer rows than `failed`. Its buckets are then a sample, not a census — do not quote them as totals. v1.149.0 (#174)."},"failure_summary_counted":{"type":"integer","description":"How many failed rows failure_summary actually read, when that is fewer than `failed`. v1.149.0 (#174)."},"recent_failures":{"type":"array","description":"Up to 10 most recent failures with reasons, so the user can see what didn't import. Capped — use failure_summary for the totals.","items":{"type":"object","properties":{"url":{"type":"string"},"last_error":{"type":"string","nullable":true},"attempts":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"next_retry_at":{"type":"string","format":"date-time","nullable":true,"description":"When the retry ladder will re-drive this row; null if nothing is scheduled and it is permanently failed. The per-row companion to the top-level `retrying` count, which says how many are scheduled but not which. Undocumented since v0.54.0. Unlike the top-level next_retry_at, this key is always present."}}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/export_opml":{"post":{"operationId":"export_opml","summary":"Export OPML","description":"Returns the caller's RSS subscriptions as an OPML 2.0 document — the portable format every RSS reader imports. Subscriptions are grouped by folder. Works even on a lapsed subscription — export is never paywalled.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"opml":{"type":"string","description":"OPML 2.0 XML document"},"count":{"type":"integer","description":"Number of subscriptions exported"},"filename":{"type":"string","description":"Suggested filename (newsmind-feeds-YYYY-MM-DD.opml)"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/search_articles_by_keyword":{"post":{"operationId":"search_articles_by_keyword","summary":"Search articles by keyword","description":"Full-text keyword search. Ranked by relevance with a 14-day recency half-life; an AND search returning 0 auto-retries with OR (fallback='or'). The `retention` field carries the tier window + a cutoff_hit flag — surface when set. Paraphrased queries: use search_articles_by_meaning (Max).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","description":"Search terms — natural language works (websearch syntax)."},"unread_only":{"type":"boolean","default":false,"description":"Exclude articles the user has marked read."},"since_days":{"type":"integer","description":"Only articles published in the last N days. Whole days only - the ranking function rejects a fractional value."},"limit":{"type":"integer","default":50,"minimum":1,"maximum":1000,"description":"Max results. Default 50; values outside 1-1000 are clamped."}},"required":["query"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"articles":{"type":"array","items":{"$ref":"#/components/schemas/Article"}},"fallback":{"type":"string","enum":["or"],"nullable":true,"description":"Set to 'or' when the AND search returned 0 articles and the tool retried with OR semantics. Indicates the results match ANY term, not all terms — narrate this to the user as reduced precision."},"hint":{"type":"string","nullable":true,"description":"Human-readable note about empty results or fallback. Surfaces actionable suggestions (narrow the query, broaden time range, use semantic_search)."},"search_scope":{"$ref":"#/components/schemas/SearchScope"},"retention":{"$ref":"#/components/schemas/RetentionMeta"}},"required":["articles","retention"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_unread_count":{"post":{"operationId":"get_unread_count","summary":"Get unread count","description":"Total unread count within tier retention. Cheap — no article rows. Pass by_feed=true to also get per-feed breakdown (sorted desc, zero-count feeds omitted).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"by_feed":{"type":"boolean","default":false,"description":"If true, response also includes a `feeds` array with per-feed unread counts (sorted descending, zero-count feeds omitted)."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"unread_count":{"type":"integer"},"feeds":{"type":"array","description":"Present only when by_feed=true.","items":{"type":"object","properties":{"feed_id":{"type":"integer"},"title":{"type":"string","nullable":true},"unread_count":{"type":"integer"}},"required":["feed_id","unread_count"]}}},"required":["unread_count"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_unread_articles":{"post":{"operationId":"get_unread_articles","summary":"Get unread articles","description":"Unread articles, newest first. Slots are shared across feeds so one high-volume source cannot fill the page; balance_feeds=false restores strict recency. since_hours narrows the window; feed_id scopes to one source; lean=true (limit up to 1000) fits the whole set in one call.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","default":50,"maximum":1000,"description":"Max articles to return (1–1000). Default 50; raise toward 1000 when clustering the whole unread set."},"since_hours":{"type":"number","description":"Only articles published in the last N hours."},"feed_id":{"type":"integer","description":"Limit to a specific feed."},"exclude_feed_ids":{"type":"array","items":{"type":"integer"},"description":"Exclude articles from these feeds. Useful when one or two high-volume feeds (HN, WIRED) drown out the rest."},"lean":{"type":"boolean","default":false,"description":"Return a slim payload — id, feed_id, feed_title, title, url, published_at only — dropping summary, image_url/width/height/mime, and added_at. ~5x smaller per row, lets a single call carry hundreds of articles for AI-side topic clustering."},"boost_engaged_feeds":{"type":"boolean","default":false,"description":"Re-rank the recent unread pool by per-feed engagement (how often the user reads/stars that feed) instead of pure recency. Feeds the user values rise; skipped feeds sink; recency breaks ties. Use for 'what should I read first' / 'the good stuff'. Default false (strict newest-first)."},"balance_feeds":{"type":"boolean","default":true,"description":"Share the page out across feeds that published recently, so one high-volume source cannot fill it. Default true. Set false for strict newest-first, e.g. when the user explicitly wants the raw timeline or is chasing a fast-moving story."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"articles":{"type":"array","items":{"$ref":"#/components/schemas/Article"}},"balanced":{"type":"object","description":"Present only when balancing changed the result."},"unavailable":{"type":"object","description":"Present only when the engagement ranking could not be computed on this call (#390); the articles are then newest first.","properties":{"boost_engaged_feeds":{"type":"string"}}}},"required":["articles"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_article":{"post":{"operationId":"get_article","summary":"Get article","description":"Returns the full body of an article. If the feed only had a summary, fetches and extracts the body via Mozilla Readability the first time.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"article_id":{"type":"integer","description":"The `id` field on any article a read tool returned (get_unread_articles, search_articles_by_keyword, get_articles_by_feed, get_watch_matches, ...) - not a feed_id and not a URL. An id outside the user's subscriptions or outside tier retention (Pro 30d / Max 90d) returns the same 'Article <id> is not available' error as an id that does not exist."}},"required":["article_id"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Article"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/set_read_state":{"post":{"operationId":"set_read_state","summary":"Set read state","description":"Mark articles read or unread. read=true marks read, read=false restores to unread. Scope is EITHER article_ids OR through_published_at (exactly one). through_published_at is a high-water mark — pass current UTC time for 'mark all read'; add feed_id to scope to one source. Never touches stars.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"read":{"type":"boolean","description":"true = mark read; false = restore to unread."},"article_ids":{"type":"array","items":{"type":"integer"},"description":"Specific article IDs to flip. Mutually exclusive with through_published_at."},"through_published_at":{"type":"string","format":"date-time","description":"High-water mark — every subscribed article published at or before this timestamp flips. For 'mark all read', pass the current UTC time. Mutually exclusive with article_ids."},"feed_id":{"type":"integer","description":"Optional feed scope, high-water-mark mode only — flips only matching articles from this feed."}},"required":["read"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"read":{"type":"boolean"},"updated":{"type":"integer"},"scope":{"type":"string","enum":["ids","through"]},"through_published_at":{"type":"string","nullable":true},"feed_id":{"type":"integer","nullable":true},"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/set_starred":{"post":{"operationId":"set_starred","summary":"Set starred","description":"Star or unstar articles. starred=true adds the star, starred=false removes it. Operates on the stars table only; read state is preserved. Re-starring an already-starred article is a no-op.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"starred":{"type":"boolean","description":"true = star; false = unstar."},"article_ids":{"type":"array","items":{"type":"integer"},"description":"Article IDs to star or unstar."}},"required":["starred","article_ids"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"starred":{"type":"boolean"},"updated":{"type":"integer"},"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/subscribe_to_feed":{"post":{"operationId":"subscribe_to_feed","summary":"Subscribe to feed","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Feed URL or the site's homepage (HTML pages are auto-discovered). Must be absolute with a scheme - http:// and https:// are both accepted; a bare domain like example.com returns reason not_a_url. Pass it as the user gave it: tracking params (utm_source, fbclid, ref, ...) and #fragments are stripped server-side."},"folder":{"type":"string","description":"Folder to file the new subscription under. Omit for top-level - there is no default folder. Created on demand, so reuse an existing folder's spelling exactly as get_feeds returns it (matching is case-sensitive). Ignored if already subscribed (status already_subscribed); refile with move_feed_to_folder."}},"required":["url"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"integer","nullable":true},"status":{"type":"string","enum":["created","already_subscribed","queued"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/unsubscribe_from_feed":{"post":{"operationId":"unsubscribe_from_feed","summary":"Unsubscribe from feed","description":"Remove a subscription. feed_id accepts the numeric id, or the feed's exact title or URL as shown by get_feeds. Never throws for a miss: an unmatched reference returns status 'not_found' with a message, and a successful removal returns status 'unsubscribed'.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":["integer","string"],"description":"The feed's numeric id (preferred), or its exact title / URL as shown by get_feeds."}},"required":["feed_id"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"string"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/move_feed_to_folder":{"post":{"operationId":"move_feed_to_folder","summary":"Move feed to folder","description":"Reorganize an existing subscription into a folder. Omit `folder` to remove the feed from any folder (top-level). Does not affect articles or the feed's visibility window.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"integer","description":"The feed's integer id: the `id` from get_feeds, or the `feed_id` from subscribe_to_feed or any article. Must be a number, not a title or URL (only unsubscribe_from_feed accepts those). A feed the user is not subscribed to returns 'Not subscribed to feed <id>.'"},"folder":{"type":"string","description":"New folder name. Omit to remove from any folder (top-level)."}},"required":["feed_id"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"integer"},"folder":{"type":"string","nullable":true},"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_feed":{"post":{"operationId":"get_feed","summary":"Get feed","description":"One feed's metadata — id, title, URL, folder, last poll time, error status. For all feeds, use /get_feeds.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"integer","description":"The feed's integer id: the `id` from get_feeds or get_feed_health, or the `feed_id` from subscribe_to_feed or any article. Must be a number, not a title or URL (only unsubscribe_from_feed accepts those). A feed the user is not subscribed to returns 'Not subscribed to feed <id>.'"}},"required":["feed_id"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Feed"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_articles_by_feed":{"post":{"operationId":"get_articles_by_feed","summary":"Get articles by feed","description":"All articles from one feed, newest first, regardless of read or starred state. For unread-only, use /get_unread_articles with feed_id instead. Subject to tier retention (Pro 30d / Max 90d).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"integer","description":"The feed's integer id: the `id` from get_feeds, or the `feed_id` from subscribe_to_feed or any article. Must be a number, not a title or URL (only unsubscribe_from_feed accepts those). A feed the user is not subscribed to returns the error 'Not subscribed to feed <id>.', not an empty list."},"since_days":{"type":"number","description":"Only articles published in the last N days."},"limit":{"type":"integer","default":50,"minimum":1,"maximum":1000,"description":"Max results. Default 50; values outside 1-1000 are clamped."}},"required":["feed_id"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"articles":{"type":"array","items":{"$ref":"#/components/schemas/Article"}}},"required":["articles"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_article_counts":{"post":{"operationId":"get_article_counts","summary":"Get article counts","description":"Count articles grouped by feed, day, or hour. Use before list/search for cheap planning. Doubles as a feed-velocity check (group_by=feed, since_hours=1 shows which feeds are noisiest right now).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"group_by":{"type":"string","enum":["feed","day","hour"],"description":"Bucket dimension. 'feed' returns one row per feed; 'day' / 'hour' return time buckets."},"unread_only":{"type":"boolean","default":false,"description":"Only count articles the user hasn't marked read."},"since_hours":{"type":"number","description":"Only articles published in the last N hours."},"since_days":{"type":"number","description":"Only articles published in the last N days."},"feed_id":{"type":"integer","description":"Limit to a specific feed (useful with group_by=day or hour)."}},"required":["group_by"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"counts":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"count":{"type":"integer"}}}}},"required":["counts"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/search_articles_by_meaning":{"post":{"operationId":"search_articles_by_meaning","summary":"Search articles by meaning","description":"Conceptual search via vector embeddings; prefer over keyword search for paraphrased questions ('GPU bottleneck' matches 'AI chip supply'). Newsmind Max only — Pro gets max_required. The `retention` field carries the 90-day window + cutoff_hit flag — surface when set.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","description":"Natural-language description of what you're looking for. Conceptual / topical phrasing works better than keyword phrasing."},"limit":{"type":"integer","default":20,"minimum":1,"maximum":100,"description":"Max results. Default 20; values outside 1-100 are clamped."},"unread_only":{"type":"boolean","default":false,"description":"Exclude articles the user has marked read."}},"required":["query"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"count":{"type":"integer"},"articles":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"feed_id":{"type":"integer"},"feed_title":{"type":"string","nullable":true},"title":{"type":"string","nullable":true},"url":{"type":"string","nullable":true},"image_url":{"type":"string","format":"uri","nullable":true},"image_width":{"type":"integer","nullable":true},"image_height":{"type":"integer","nullable":true},"image_mime":{"type":"string","nullable":true},"summary":{"type":"string","nullable":true},"published_at":{"type":"string","format":"date-time","nullable":true},"added_at":{"type":"string","format":"date-time","nullable":true},"similarity":{"type":"number"}}}},"search_scope":{"$ref":"#/components/schemas/SearchScope"},"retention":{"$ref":"#/components/schemas/RetentionMeta"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_new_articles":{"post":{"operationId":"get_new_articles","summary":"Get new articles","description":"Articles since this token's last visit, oldest-first; advances a per-token cursor. has_more=true means more remain. mark_seen=false peeks; exclude_feed_ids skips noisy feeds. `feed_warnings` and `watch_summaries`, when present, report failing feeds and saved-watch matches.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"max_items":{"type":"integer","default":50,"minimum":1,"maximum":100,"description":"Max articles per batch (1-100, default 50). Values above 100 are silently clamped. Cursor only advances over what's returned, so older unseen articles aren't silently skipped. If the result hits this cap, `has_more` is true and you should call again to fetch the next batch."},"mark_seen":{"type":"boolean","default":true,"description":"If false, peek without advancing the cursor (subsequent default-mode call commits)."},"exclude_feed_ids":{"type":"array","items":{"type":"integer"},"description":"Exclude articles from these feeds. Useful when a high-volume feed (HN, WIRED) is overwhelming the queue."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"articles":{"type":"array","items":{"$ref":"#/components/schemas/Article"}},"count":{"type":"integer"},"has_more":{"type":"boolean"},"cursor_advanced":{"type":"boolean"},"feed_warnings":{"$ref":"#/components/schemas/FeedWarnings","nullable":true,"description":"Present only when one or more feeds have failed 3+ fetches in a row AND delivered nothing for 48h; absent on healthy accounts. v0.21.0, delivery clause v1.117.1."},"import_failures":{"$ref":"#/components/schemas/ImportFailures","nullable":true,"description":"Present only when feed imports failed permanently in the last 7 days; absent when there's nothing to report. v0.33.0."},"import_recovered":{"$ref":"#/components/schemas/ImportRecovered","nullable":true,"description":"Present only when a previously-failed import has recovered and been added in the last 7 days. v1.151.0 (#162)."},"watch_summaries":{"type":"array","nullable":true,"description":"Present only when the user has saved watches. Per-watch new_match_count tells the assistant which watches to mention. Call get_watch_matches for detail. v0.21.0.","items":{"$ref":"#/components/schemas/WatchSummary"}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_starred_articles":{"post":{"operationId":"get_starred_articles","summary":"Get starred articles","description":"Return the user's starred articles, newest-starred first. Subject to tier retention — articles starred but past the retention window (Pro 30d / Max 90d) are not returned. Stars do not bypass retention.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","default":50,"minimum":1,"maximum":1000,"description":"Max results. Default 50; values outside 1-1000 are clamped."},"since_days":{"type":"number","description":"Only articles starred in the last N days (filters by starred_at, not published_at)."},"feed_id":{"type":"integer","description":"Limit to articles from a specific feed."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"articles":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Article"},{"type":"object","properties":{"starred_at":{"type":"string","format":"date-time"}}}]}}},"required":["articles"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_reading_history":{"post":{"operationId":"get_reading_history","summary":"Get reading history","description":"Articles the user has marked read, most-recently-read first. Subject to tier retention.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","default":50,"minimum":1,"maximum":1000,"description":"Max results. Default 50; values outside 1-1000 are clamped."},"since_days":{"type":"number","description":"Only articles read in the last N days (filters by read_at)."},"feed_id":{"type":"integer","description":"Limit to articles from a specific feed."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"articles":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Article"},{"type":"object","properties":{"read_at":{"type":"string","format":"date-time"}}}]}}},"required":["articles"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_feed_health":{"post":{"operationId":"get_feed_health","summary":"Get feed health","description":"One row per subscribed feed: last poll, errors, last article date, weekly counts, newest date in the fetched document, and status, worst first: failing, empty (never held anything readable), no_items (empty since last_nonempty_at), outside_window, idle, stale, healthy.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"feed_id":{"type":"integer","description":"Limit to a specific feed; omit for all subscriptions."},"status":{"type":"string","enum":["healthy","stale","failing","empty","no_items","outside_window","idle"],"description":"Only return feeds matching this status (useful for 'show me broken feeds' style queries)."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"feeds":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"title":{"type":"string","nullable":true},"url":{"type":"string"},"site_url":{"type":"string","nullable":true},"folder":{"type":"string","nullable":true},"last_polled_at":{"type":"string","format":"date-time","nullable":true},"error_count":{"type":"integer"},"last_error":{"type":"string","nullable":true},"last_published_at":{"type":"string","format":"date-time","nullable":true},"last_item_published_at":{"type":"string","format":"date-time","nullable":true},"last_nonempty_at":{"type":"string","format":"date-time","nullable":true},"articles_7d":{"type":"integer"},"articles_30d":{"type":"integer"},"status":{"type":"string","enum":["healthy","stale","failing","empty","no_items","outside_window","idle"]}}}}},"required":["feeds"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/import_opml":{"post":{"operationId":"import_opml","summary":"Import OPML","description":"Bulk-subscribe from an OPML 2.0 document. Queues entries and returns at once — nothing is subscribed yet. Already-subscribed, duplicate and over-limit entries are skipped. Poll get_import_status for pending/succeeded/failed counts; report counts, never a completion time.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"opml":{"type":"string","description":"The OPML 2.0 XML document as a string. Must contain <outline xmlUrl=\"…\"> entries."},"folder":{"type":"string","description":"Optional override folder for every imported feed. If omitted, the OPML's own folder structure (parent <outline> titles) is preserved."}},"required":["opml"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"integer","description":"Entries accepted into the import queue. Nothing is subscribed yet — poll get_import_status."},"format_invalid":{"type":"integer","description":"Entries whose URL was unusable as written."},"skipped_already_subscribed":{"type":"integer"},"skipped_duplicate_in_opml":{"type":"integer","description":"The same feed appeared more than once in this document."},"skipped_already_queued":{"type":"integer","description":"Already pending from an earlier import."},"skipped_over_limit":{"type":"integer","description":"Beyond the plan's feed limit."},"total_in_opml":{"type":"integer"},"eta":{"type":"string","description":"Human-readable estimate of when the queue should drain. Advisory — report counts from get_import_status rather than this."},"message":{"type":"string"},"details":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"status":{"type":"string","enum":["queued","format_invalid","skipped_already_subscribed","skipped_duplicate_in_opml","skipped_already_queued","skipped_over_limit"]},"reason":{"type":"string"}}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_story_clusters":{"post":{"operationId":"get_story_clusters","summary":"Get story clusters","description":"Group redundant coverage into clusters. match=semantic (Max only; Max default) groups one story across outlets by meaning; match=exact (default elsewhere) groups one item syndicated into several feeds by URL/title; both merges the two. Returns canonical article plus also_covered_by.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"match":{"type":"string","enum":["semantic","exact","both"],"default":"semantic","description":"How to group: 'semantic' = same story by embedding similarity (Max only; default on Max); 'exact' = same item by canonical URL / near-identical title (default on other plans); 'both' = exact then semantic, de-overlapped (Max only)."},"since_hours":{"type":"number","description":"Look-back window in hours. Default 48 for semantic, 168 (7 days) for exact. Capped by tier retention."},"min_cluster_size":{"type":"integer","default":2,"description":"Minimum articles per cluster (default 2)."},"limit":{"type":"integer","default":20,"minimum":1,"maximum":50,"description":"Max clusters to return (default 20). Values outside 1-50 are clamped."},"unread_only":{"type":"boolean","default":false,"description":"If true, only cluster unread articles. Default false. Semantic matching only — ignored by 'exact'."},"match_by":{"type":"string","enum":["url","title","either"],"default":"either","description":"Exact-matching strictness, used only when match is 'exact' or 'both'. url = same canonical URL only (strictest); title = same lowercased title; either = url-OR-title."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"window_hours":{"type":"integer"},"match":{"type":"string","enum":["semantic","exact","both"]},"match_by":{"type":"string","description":"Present only when exact matching ran."},"clusters":{"type":"array","items":{"type":"object","properties":{"cluster_id":{"type":"integer"},"match":{"type":"string","enum":["semantic","url","title"],"description":"Which strategy produced this cluster."},"key":{"type":"string","nullable":true,"description":"The matched URL or title for exact clusters; null for semantic."},"size":{"type":"integer"},"feeds":{"type":"integer","description":"Number of distinct feeds the cluster spans."},"canonical":{"$ref":"#/components/schemas/Article"},"also_covered_by":{"type":"array","items":{"$ref":"#/components/schemas/Article"}}}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/create_watch":{"post":{"operationId":"create_watch","summary":"Create watch","description":"Save a watch so new matching articles surface later. Keyword watches (default, all plans) match the query as an exact phrase; semantic watches match by meaning (Max only). Optionally scope to one feed or folder, and deliver by email or webhook on a cadence. Returns the created watch.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","description":"What to watch for. For keyword watches: an exact phrase — words must appear adjacent and in order, so write it like a headline ('OLED iPad mini', not 'iPad mini OLED'). For semantic watches: a natural phrase describing the topic/product (order-insensitive)."},"label":{"type":"string","description":"Optional display name (e.g. 'M5 Mac mini'). Returned as-is by list_watches, watch_summaries and matched_watches - null when unset, with no fallback there. Only the email digest substitutes the query when the label is empty."},"kind":{"type":"string","enum":["keyword","semantic"],"description":"keyword (default, all plans) or semantic (Max only — matches by meaning). Semantic on a non-Max account returns max_required."},"feed_id":{"type":"integer","description":"Optional: limit to one feed. Omit for all subscriptions."},"folder":{"type":"string","description":"Optional: limit to feeds in this subscription folder. Omit for all subscriptions."},"threshold":{"type":"number","description":"Semantic only: cosine similarity cutoff in (0,1]. Any value is settable — this is a dial, not a set of presets. Calibration from real data: below ~0.45 is effectively unfiltered (it sits under the median similarity of arbitrary articles and will match thousands); 0.55-0.60 is on-topic; above 0.70 is near-exact and starts missing paraphrases. The useful band is narrow and near the top. Default 0.55. The response includes `threshold_preview` — how many of this watch's matches each stricter cutoff would leave — so pick the next value from that rather than guessing."},"delivery":{"type":"string","enum":["in_session","email","webhook"],"description":"Where matches are delivered. in_session (default) = retrieved when you call get_watch_matches or get_new_articles. email = digest to the account email. webhook = POST to webhook_url. email and webhook default to a daily cadence when none is passed."},"cadence":{"type":"string","enum":["realtime","weekdays","daily","weekly"],"description":"For email/webhook delivery: how often to send. weekdays = daily but Monday-Friday only (weekend matches ride Monday's digest). Defaults to daily when delivery is out-of-band."},"delivery_hour":{"type":"integer","minimum":0,"maximum":23,"description":"Daily/weekly email or webhook delivery: hour of day to send, 0-23, in the account's timezone ('email me daily at 7am' → 7). Default 8 (08:00)."},"delivery_day":{"type":"string","enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday"],"description":"Weekly cadence only: weekday to send ('weekly on friday'). Default monday."},"webhook_url":{"type":"string","description":"Required when delivery='webhook': an https:// endpoint we POST match batches to. Must be a public host — loopback, private and link-local addresses are rejected."},"require_terms":{"type":"array","items":{"type":"string"},"maxItems":12,"description":"Words an article must ALSO carry to match (max 12). ANY one is enough, not all - ['missing','Columbus','Ohio State'] keeps anything mentioning any of them. Use this to disambiguate a name that points at more than one person or thing: a watch on 'Brian Shaffer' with require_terms ['missing'] keeps the missing-persons coverage and drops the baseball player's stats page and two unrelated obituaries. DO NOT append the word to `query` instead - the query is matched as an exact phrase, so 'Brian Shaffer missing' matches nothing at all. Changing this rebuilds the match set."},"exclude_terms":{"type":"array","items":{"type":"string"},"maxItems":12,"description":"Words that DEMOTE a match rather than dropping it (max 12). Matches still appear, sorted last and tagged `user_excluded`, so an over-broad guess is recoverable. Prefer require_terms when you know what you DO want."}},"required":["query"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Watch"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/list_watches":{"post":{"operationId":"list_watches","summary":"List watches","description":"Return the user's saved watches with scope, delivery, active state, and current unseen-match counts. Read-only.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"watches":{"type":"array","items":{"$ref":"#/components/schemas/WatchWithCount"}},"unavailable":{"type":"object","description":"Present only when a watch's unseen-match count could not be read on this call. Names the field and says where the matches are; those watches carry new_match_count null.","properties":{"new_match_count":{"type":"string"}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/update_watch":{"post":{"operationId":"update_watch","summary":"Update watch","description":"Modify a saved watch (pause/resume via active, edit query/label/scope, change delivery channel/cadence). Only supplied fields change. Editing the query rebuilds the match set.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"watch_id":{"type":"integer","description":"From list_watches or create_watch."},"active":{"type":"boolean","description":"false pauses the watch (skipped by matching + delivery); true resumes it."},"label":{"type":"string","description":"Display name (e.g. 'M5 Mac mini'). Pass an empty string to clear it - the email digest then falls back to the query. Renaming never rebuilds the match set."},"query":{"type":"string","description":"New query. Rebuilds the watch's match set."},"feed_id":{"type":"integer","description":"New single-feed scope, from get_feeds. Pass null to widen back to all subscriptions; OMITTING it leaves the scope unchanged. Must be a feed you subscribe to: any other feed_id is refused with not_subscribed and nothing changes. Changing scope rebuilds the match set (rebackfilled_because 'scope')."},"folder":{"type":"string","description":"New folder scope: limit to feeds in this subscription folder, spelled exactly as get_feeds returns it (not validated - a name no subscription uses matches nothing). Empty string widens back to all subscriptions. Changing scope rebuilds the match set (rebackfilled_because 'scope')."},"threshold":{"type":"number","description":"Semantic similarity cutoff in (0,1]; any value is settable. Changing it re-runs the backfill so matches reflect the new cutoff, and the response returns a fresh threshold_preview. Calibration: below ~0.45 is effectively unfiltered, 0.55-0.60 is on-topic, above 0.70 is near-exact."},"delivery":{"type":"string","enum":["in_session","email","webhook"],"description":"Where matches are delivered: in_session = retrieved when you call get_watch_matches or get_new_articles; email = digest to the account email; webhook = POST to webhook_url. webhook_url is NOT checked here, unlike create_watch, which refuses delivery=webhook without one (webhook_url_required). Send one in the same call unless the watch already has one, or every send fails server-side with nothing surfaced to you. Speeding up delivery settles the matches that piled up under the slower setting rather than shipping them all at once — returned as settled_backlog, and they stay visible via get_watch_matches."},"cadence":{"type":"string","enum":["realtime","weekdays","daily","weekly"],"description":"For email/webhook delivery: how often to send. weekdays = daily but Monday-Friday only (weekend matches ride Monday's digest); realtime = the next 15-minute delivery tick. Unlike create_watch there is no default - an unset cadence stays unset and sends daily. A cadence on an in_session watch schedules nothing and returns cadence_inert. Switching to a FASTER cadence settles the backlog the slower one accumulated (settled_backlog) — a watch moved from weekly to realtime sends the next match, not the week behind it."},"delivery_hour":{"type":"integer","minimum":0,"maximum":23,"description":"Daily/weekly delivery hour, 0-23, account timezone. Default 8."},"delivery_day":{"type":"string","enum":["sunday","monday","tuesday","wednesday","thursday","friday","saturday"],"description":"Weekly delivery weekday. Default monday."},"webhook_url":{"type":"string","description":"The https:// endpoint we POST match batches to. Must be a public host - loopback, private and link-local addresses are rejected with invalid_webhook_url, and the whole update aborts, so no other field in the call is applied. Pass an empty string to clear it."},"require_terms":{"type":"array","items":{"type":"string"},"maxItems":12,"description":"Words an article must ALSO carry to match (max 12). ANY one is enough, not all - ['missing','Columbus','Ohio State'] keeps anything mentioning any of them. Use this to disambiguate a name that points at more than one person or thing: a watch on 'Brian Shaffer' with require_terms ['missing'] keeps the missing-persons coverage and drops the baseball player's stats page and two unrelated obituaries. DO NOT append the word to `query` instead - the query is matched as an exact phrase, so 'Brian Shaffer missing' matches nothing at all. Changing this rebuilds the match set."},"exclude_terms":{"type":"array","items":{"type":"string"},"maxItems":12,"description":"Words that DEMOTE a match rather than dropping it (max 12). Matches still appear, sorted last and tagged `user_excluded`, so an over-broad guess is recoverable. Prefer require_terms when you know what you DO want."}},"required":["watch_id"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Watch"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/delete_watch":{"post":{"operationId":"delete_watch","summary":"Delete watch","description":"Remove a saved watch. Idempotent — deleting a non-existent watch_id returns ok=false rather than throwing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"watch_id":{"type":"integer","description":"From list_watches or the response of create_watch."}},"required":["watch_id"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"watch_id":{"type":"integer"},"ok":{"type":"boolean"},"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/get_watch_matches":{"post":{"operationId":"get_watch_matches","summary":"Get watch matches","description":"Pull matches newer than each watch's last_seen_at cursor. With watch_id, runs one watch. Without, runs all. Atomically advances cursors when mark_seen=true (default). Subject to tier retention.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"watch_id":{"type":"integer","description":"Run a specific watch. Omit to run all of the user's watches."},"mark_seen":{"type":"boolean","default":true,"description":"If false, peek without marking the returned hits as seen. Default true."},"limit":{"type":"integer","default":50,"minimum":1,"maximum":100,"description":"Max articles per watch. Default 50; values outside 1-100 are clamped."}}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer","description":"Number of watches run (not articles)."},"mark_seen":{"type":"boolean"},"results":{"type":"array","items":{"type":"object","properties":{"watch_id":{"type":"integer"},"query":{"type":"string"},"label":{"type":"string","nullable":true},"feed_id":{"type":"integer","nullable":true},"cursor_was":{"type":"string","format":"date-time"},"cursor_advanced_to":{"type":"string","format":"date-time"},"count":{"type":"integer"},"articles":{"type":"array","items":{"$ref":"#/components/schemas/Article"}}}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}},"/report_issue":{"post":{"operationId":"report_issue","summary":"Report issue","description":"File a bug report, feature request, or feedback. Server stores it (visible only to the developer) and emails the owner. Returns the issue ID. Rate-limited to 10/user/day.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["bug","feature_request","feedback"],"description":"bug = something broke or didn't behave as expected; feature_request = user wants a capability that doesn't exist; feedback = general comment, praise, or critique."},"title":{"type":"string","maxLength":200,"description":"One-line summary, like a GitHub issue title."},"body":{"type":"string","maxLength":5000,"description":"Detailed description. Include reproducer steps, article/feed IDs that were involved, what was expected vs. what happened, and any other context the developers should see."}},"required":["kind","title","body"]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"kind":{"type":"string"},"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What was wrong with the request, in a sentence."},"code":{"type":"string","description":"A stable identifier for the refusal (e.g. url_required, feed_id_invalid, feed_limit_reached); absent on a refusal that has none."}}}}}},"401":{"description":"Unauthorized"},"402":{"description":"Subscription required (trial expired or cancelled)"},"429":{"description":"Too many calls. Either the per-account rate limit ({error: \"rate_limited\", message}) or the account's in-flight work ({error: <sentence>, code: \"busy\"}). Wait Retry-After seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"code":{"type":"string"}}}}}},"500":{"description":"Server error on our side (error: internal_error). The team has been notified.","content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}},"503":{"description":"Temporarily unable to answer. error: timeout (the request ran too long; narrow it), server_busy or tier_unavailable (retry after Retry-After seconds when sent).","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"A stable identifier, e.g. timeout, server_busy, tier_unavailable, internal_error."},"message":{"type":"string","description":"What happened, in a sentence. Never the database's own text."}}}}}}}}}}}