> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nephia.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# RSS & News

> What a Nephia Watch on a feed or a news term captures, and what arrives in each event.

This Source is a **format**, not a site. It reads whatever feed you point it at — RSS 2.0, Atom 1.0 or RDF — and it follows recent news when you give it a search term instead of a URL. You are responsible for the terms of the feeds you point us at.

## The entry

Every entry becomes an **Article**: a title, a URL, a text summary, a publication date, the publisher's domain, and the feed it came from.

**Text only.** The summary is the feed's own description or content with markup stripped and capped at 2 000 characters. There is no HTML field: Nephia reads feeds, it does not render them, and a stored HTML blob is a sanitising problem for every consumer downstream.

### Identity

An entry's `id` is, in order: its `guid`, then Atom's `id`, then its link, then a hash of its title and publication date. The fallback matters — a feed with none of the first three is a real feed, not a broken one, and a poll that cannot tell entries apart would re-emit the whole page every tick.

## Coverage

A Watch takes **exactly one** of `feed_url` or `query`. A URL polls that feed; a term follows the news for it. Both at once is not a filtered feed — it is two different requests — so it is rejected with **400**.

On a news Watch, `lang` (default `en`) and `country` (default `US`) select an **edition**, not a filter: they have to agree with each other, and the API assembles the pair for you. Each result names its publisher in `source`, and its `url` can be an aggregator redirect rather than the publisher's own address.

A `feed_url` must be **HTTPS**, and it is checked the same way a webhook URL is: `localhost`, literal private addresses, and any hostname that *resolves* to a private or link-local address are rejected with **400**. Bodies over 5 MB are refused, and the fetch times out after 15 s.

Interval: **120–86400** seconds. Cap: **20** active Watches per Account. Each tick costs **1 credit**, including a tick that finds nothing.

## What an event carries

The first tick establishes a silent baseline and emits nothing — a feed's front page is its whole recent history, and firing fifty webhooks about posts that predate the Watch is not what "watch this blog" means. Later ticks emit `article.created`.

```bash theme={null}
curl -X POST https://api.nephia.cc/v1/rss/watches \
  -H "x-api-key: $NEPHIA_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "feed_url": "https://blog.rust-lang.org/feed.xml",
    "refresh_interval_seconds": 900,
    "webhook_url": "https://example.com/hooks/nephia"
  }'
```

### Conditional GET

Each Watch remembers the `ETag` and `Last-Modified` the feed last sent, and sends them back on the next tick. A feed that answers **304 Not Modified** costs the tick and nothing else — no parse, no diff, no events.

A 304 is not an empty feed: the Watch keeps the entries it already knew about. That distinction is what stops a quiet feed from looking like a feed that lost all its posts, and then re-emitting everything on the tick after.

## Finding a feed

Plan 129-E. RSS is the only Source whose criterion is a URL nobody knows by heart, so the console can find one for you. All three routes below write an ordinary `feed_url` — there is no separate "preset" or "catalog" criterion, and a search created from a chip is indistinguishable from one you pasted a URL into.

### Discovery

`GET /api/rss/discover?url=` reads a page and reports the feeds it declares. If the page declares none, five well-known paths are tried — `/feed`, `/rss.xml`, `/atom.xml`, `/feed.xml`, `/index.xml` — and only then: a site that links to its feed is answering the question, and guessing past that answer would be five requests to prove it wrong.

At most five candidates come back, each marked `declared` (the site linked to it) or `probed` (a path answered and parsed). Costs **1 credit** for the run, whatever it takes.

This is a **dashboard route**, not `/v1`: it reads a host you supply, so it carries the same SSRF guard `feed_url` does — on the page *and* on every candidate, because a page can declare a feed on any host it likes.

### OPML import

`POST /api/rss/opml` takes a Feedly, Inoreader or NetNewsWire export, flattens its folders, dedupes by URL, and hands back the feeds that fit in your remaining watch slots — plus, by name, the ones that did not. Up to **200 feeds** per file; a larger one is refused rather than truncated, because a half-import that says nothing looks like a whole one.

The call creates nothing. The console adds the feeds as ordinary RSS searches, and you save them the way you save any other change.

### DEV.to, Medium and App Store reviews

These are **feeds, not Sources** — deliberately. Their content is articles, `article` already stores exactly what they carry, and inventing a `dev.to` Source would add an envelope key for what RSS holds today.

| What                    | Feed URL                                                                                         |
| ----------------------- | ------------------------------------------------------------------------------------------------ |
| DEV.to, one tag         | `https://dev.to/feed/tag/{tag}`                                                                  |
| DEV.to, one author      | `https://dev.to/feed/{user}`                                                                     |
| Medium, one tag         | `https://medium.com/feed/tag/{tag}`                                                              |
| Medium, one author      | `https://medium.com/feed/@{user}`                                                                |
| Medium, one publication | `https://medium.com/feed/{publication}`                                                          |
| App Store reviews       | `https://itunes.apple.com/{country}/rss/customerreviews/page=1/id={appId}/sortby=mostrecent/xml` |

The App Store URL is the one worth reading closely: `sortby=mostrecent` is not optional. Apple's default order is "most helpful", which on a review feed means a Watch would re-read the same page forever and never see a new review. Each entry arrives as an `article` — the review's title, the reviewer in `author`, and the star rating among `categories`.

## Errors

A feed that cannot be reached, or that answers with something that is not a feed, is a **503** and is not charged; the Watch reports it in its activity, readable through `GET /v1/rss/watches/{id}/activity`. A `feed_url` that is malformed, not HTTPS, or pointed at a private address is rejected with **400** at create time and never fetched.
