# RSS & Feed API — universal RSS, Atom and JSON Feed parser: fetch, discover, merge and search any web feed in one normalized schema

> Fetch ONE feed URL (RSS 2.0/0.9x, RSS 1.0/RDF, Atom, JSON-Feed 1.1 — auto-detected) and normalise it into one clean JSON schema. Broken XML, HTML entities, encoding mess and truncated feeds are repaired automatically (recovery step reported in meta).
> ReefAPI engine `feed-gateway` · 5 endpoints · clean JSON, no scraping or browsers to manage.

## How to call
- **Endpoint:** `POST https://api.reefapi.com/feed-gateway/v1/<action>` with a JSON body.
- **Auth:** header `x-api-key: <YOUR_REEFAPI_KEY>` — create one free (1,000 credits, no card): https://reefapi.com/signup
- **Response (every call):** `{ ok: boolean, data: ..., meta: { record_count, credits, ... }, error: { code, message } }` — branch on `ok`. Failed or blocked calls are free.
- **One key + one shared credit pool** across every ReefAPI API. Per-call credits are listed on each endpoint below.
- **Use it from an AI agent (MCP):** connect `https://api.reefapi.com/mcp` (remote streamable-http, `Authorization: Bearer <key>`) and your assistant can call these actions directly.

## Endpoints

### POST /feed-gateway/v1/fetch — 1 credit
Fetch ONE feed URL (RSS 2.0/0.9x, RSS 1.0/RDF, Atom, JSON-Feed 1.1 — auto-detected) and normalise it into one clean JSON schema. Broken XML, HTML entities, encoding mess and truncated feeds are repaired automatically (recovery step reported in meta).

**Parameters:**
- `url` (string, required) — Feed URL (RSS/Atom/JSON-Feed). Bare hosts get https:// prefixed. Private/internal/metadata targets are SSRF-blocked.
- `limit` (integer, optional, default 50) — Max items returned (1-100, default 50). Out-of-range values are clamped, never rejected.
- `sort_by` (enum, optional, default "feed_order") — Item ordering. Competitors paywall sorting (rss2json needs an api_key for order_by) — here it is free. [one of: feed_order, published, title, author]
- `sort_dir` (enum, optional, default "desc") — Sort direction (applies when sort_by is not feed_order). [one of: desc, asc]
- `include_content` (boolean, optional, default false) — Include the raw item HTML body as content_html (capped 30 KB). Off by default: summary is always returned as clean plain text.
- `since` (string, optional) — Only items published at/after this date (ISO 8601, e.g. 2026-06-01 or 2026-06-01T12:00:00Z).
- `until` (string, optional) — Only items published at/before this date (ISO 8601).

**Returns:** {feed: {url, final_url, format, title, link, description, language, updated, image, author, generator, categories[]}, items[]: {id, title, link, published (ISO 8601 Z), updated, author, summary (clean plain text), categories[], enclosures[] {url,type,length,width,height,duration_seconds} (podcast/media RSS), image, podcast {duration_seconds,episode,season,episode_type,explicit,subtitle,keywords[]} (iTunes — present only on podcasts), media {credit,description,keywords[]} (Media RSS), content_html (only when include_content=true)}, item_count, total_items}

**Example request body:**
```json
{
  "url": "https://feeds.bbci.co.uk/news/world/rss.xml",
  "limit": 10
}
```

### POST /feed-gateway/v1/discover — 1 credit
Find the feeds of ANY website: parses <link rel=alternate> tags + visible anchor hints, then probes common feed paths at BOTH the host root (/feed, /rss.xml, /atom.xml, /index.xml, …) AND under the given path (e.g. /r/programming/.rss). Every candidate is fetched and VERIFIED as a real parseable feed before being returned — so it works even on sites that don't declare a feed link-tag.

**Parameters:**
- `url` (string, required) — Website URL (homepage or any page).
- `probe` (boolean, optional, default true) — Also probe common feed paths (/feed, /rss.xml, /atom.xml, …) beyond the page's declared link tags.
- `limit` (integer, optional, default 10) — Max feeds returned (1-20, default 10).

**Returns:** {site, feeds[]: {url, format, title, item_count, discovered_via (link-tag|anchor|probe)}, count, candidates_checked}

**Example request body:**
```json
{
  "url": "https://www.theverge.com"
}
```

### POST /feed-gateway/v1/merge — 1 credit
Merge up to 10 feeds into ONE de-duplicated, date-sorted stream. Duplicates are detected on a canonical link key (tracking params stripped) falling back to guid/title. Each item carries its source_feed.

**Parameters:**
- `urls` (array, required) — Feed URLs to merge (max 10). Each URL is independently SSRF-guarded and fetched concurrently; one bad feed never fails the whole call.
- `limit` (integer, optional, default 100) — Max items returned (1-200, default 100). Out-of-range values are clamped, never rejected.
- `sort_dir` (enum, optional, default "desc") — Sort direction (applies when sort_by is not feed_order). [one of: desc, asc]
- `dedup` (boolean, optional, default true) — Remove duplicate stories across feeds (default true).
- `since` (string, optional) — Only items published at/after this date (ISO 8601, e.g. 2026-06-01 or 2026-06-01T12:00:00Z).
- `until` (string, optional) — Only items published at/before this date (ISO 8601).
- `include_content` (boolean, optional, default false) — Include the raw item HTML body as content_html (capped 30 KB). Off by default: summary is always returned as clean plain text.

**Returns:** {items[]: {id, title, link, published (ISO 8601 Z), updated, author, summary (clean plain text), categories[], enclosures[] {url,type,length,width,height,duration_seconds} (podcast/media RSS), image, podcast {duration_seconds,episode,season,episode_type,explicit,subtitle,keywords[]} (iTunes — present only on podcasts), media {credit,description,keywords[]} (Media RSS), content_html (only when include_content=true)} each + source_feed {url, title}, item_count, total_before_dedup, unique_after_dedup, removed_duplicates, feeds[]: {url, ok, title, item_count, error}}

**Example request body:**
```json
{
  "urls": [
    "https://feeds.bbci.co.uk/news/world/rss.xml",
    "https://feeds.bbci.co.uk/news/technology/rss.xml"
  ],
  "limit": 20
}
```

### POST /feed-gateway/v1/search — 1 credit
Keyword-filter feed items (like RSSHub filter / Feedly search, but no account). Searches title/summary/author/categories (and content_html when include_content=true); multi-word queries match any or all terms; exclude= drops unwanted items.

**Parameters:**
- `url` (string, optional) — Single feed URL to search. (search needs url OR urls.)
- `urls` (array, optional) — Multiple feed URLs to search at once (max 10). Each URL is independently SSRF-guarded and fetched concurrently; one bad feed never fails the whole call.
- `query` (string, optional) — Keyword(s) to match, space-separated. Empty query + exclude acts as a pure exclusion filter. Case-insensitive substring match.
- `exclude` (string, optional) — Keyword(s) that DROP an item when present (RSSHub filter_out equivalent).
- `fields` (array, optional, default ["title","summary"]) — Which item fields to match against (default title+summary). [one of: title, summary, content, author, categories]
- `match` (enum, optional, default "any") — Multi-term combination mode. [one of: any, all]
- `limit` (integer, optional, default 50) — Max items returned (1-100, default 50). Out-of-range values are clamped, never rejected.
- `sort_by` (enum, optional, default "feed_order") — Item ordering. Competitors paywall sorting (rss2json needs an api_key for order_by) — here it is free. [one of: feed_order, published, title, author]
- `sort_dir` (enum, optional, default "desc") — Sort direction (applies when sort_by is not feed_order). [one of: desc, asc]
- `include_content` (boolean, optional, default false) — Include the raw item HTML body as content_html (capped 30 KB). Off by default: summary is always returned as clean plain text.
- `since` (string, optional) — Only items published at/after this date (ISO 8601, e.g. 2026-06-01 or 2026-06-01T12:00:00Z).
- `until` (string, optional) — Only items published at/before this date (ISO 8601).

**Returns:** {items[]: {id, title, link, published (ISO 8601 Z), updated, author, summary (clean plain text), categories[], enclosures[] {url,type,length,width,height,duration_seconds} (podcast/media RSS), image, podcast {duration_seconds,episode,season,episode_type,explicit,subtitle,keywords[]} (iTunes — present only on podcasts), media {credit,description,keywords[]} (Media RSS), content_html (only when include_content=true)} each + source_feed when multi-feed, item_count, total_scanned, query, feeds[] (per-feed ok/error when urls used)}

**Example request body:**
```json
{
  "url": "https://feeds.bbci.co.uk/news/world/rss.xml",
  "query": "a e i o u",
  "match": "any"
}
```

### POST /feed-gateway/v1/batch — 1 credit
Fetch up to 20 feeds in ONE call (concurrent, fault-isolated): each feed returns independently with its own ok/error — one dead feed never fails the batch.

**Parameters:**
- `urls` (array, required) — Feed URLs to fetch (max 20). Each URL is independently SSRF-guarded and fetched concurrently; one bad feed never fails the whole call.
- `limit` (integer, optional, default 20) — Max items returned (1-100, default 20). Out-of-range values are clamped, never rejected.
- `include_content` (boolean, optional, default false) — Include the raw item HTML body as content_html (capped 30 KB). Off by default: summary is always returned as clean plain text.
- `sort_by` (enum, optional, default "feed_order") — Item ordering. Competitors paywall sorting (rss2json needs an api_key for order_by) — here it is free. [one of: feed_order, published, title, author]
- `sort_dir` (enum, optional, default "desc") — Sort direction (applies when sort_by is not feed_order). [one of: desc, asc]

**Returns:** {results[]: {url, ok, feed, items[], item_count, error}, feed_count, ok_count, total_items}

**Example request body:**
```json
{
  "urls": [
    "https://feeds.bbci.co.uk/news/world/rss.xml",
    "https://hnrss.org/frontpage"
  ],
  "limit": 5
}
```

## More
- Try it live, no code: https://reefapi.com/playground?engine=feed-gateway
- Human docs page: https://reefapi.com/docs/feed-gateway
- Every ReefAPI API in one file (for your AI): https://reefapi.com/llms-full.txt
