# RSS, Atom & JSON Feed to JSON (`kadev-code/rss-atom-to-json`) Actor

Turn RSS 0.9x/1.0/2.0, Atom and JSON Feed URLs into clean JSON entries with ISO 8601 dates, plain-text summaries, categories and podcast enclosures. No article downloads.

- **URL**: https://apify.com/kadev-code/rss-atom-to-json.md
- **Developed by:** [Kadev Code](https://apify.com/kadev-code) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 feed item parseds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## RSS, Atom & JSON Feed to JSON

Parse public RSS 0.9x/1.0/2.0, Atom and JSON Feed documents into a clean,
normalized dataset. Each row is one feed entry with ISO 8601 UTC dates, a
plain-text summary, categories, author and podcast enclosures. This Actor reads
the feed only; it never downloads the linked article.

- No API key, browser or AI model.
- Tolerant XML parsing for real-world feeds, with DOCTYPE/ENTITY documents
  rejected before parsing.
- Redirects and gzip/deflate responses are supported.
- Invalid feeds produce one `error` row and are not billed.
- Duplicate GUIDs are removed within each feed. Duplicate links are removed
  across all feeds in the same run.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `feedUrls` | array of strings | required | 1–20 public HTTP(S) feed URLs. Ports 80 and 443 only. |
| `maxItemsPerFeed` | integer | `50` | 1–200 entries per feed, applied after filtering and deduplication. |
| `since` | string | omitted | Optional ISO 8601 date or timestamp. Entries with an effective date older than it are excluded. |

`since` uses `published`, or `updated` when `published` is absent. Values without
an offset are treated as UTC. When `since` is set, entries with neither date are
excluded because the filter cannot be applied to them.

Example:

```json
{
  "feedUrls": [
    "https://hnrss.org/frontpage",
    "https://blog.example.com/feed.xml"
  ],
  "maxItemsPerFeed": 25,
  "since": "2026-10-01T00:00:00Z"
}
```

### Output

One row per entry, plus one row per failed feed. All fields are present on every
row; nullable fields are `null` when the source does not provide them.

| Field | Type | Meaning |
|---|---|---|
| `feedUrl` | string | null | Requested feed URL; `null` for an invalid-input row |
| `feedTitle` | string | null | Channel or feed title |
| `title` | string | null | Entry title as plain text |
| `link` | string | null | Entry link, resolved against the final feed URL |
| `guid` | string | null | Feed identifier (`guid`, `id`, or Atom ID) |
| `published` | string | null | ISO 8601 UTC, e.g. `2026-10-20T13:00:00Z` |
| `updated` | string | null | ISO 8601 UTC |
| `author` | string | null | First author name |
| `summary` | string | Summary or content converted to text (HTML removed, entities decoded) |
| `contentHtml` | string | null | Entry content HTML when the feed supplies it |
| `categories` | array of strings | Entry categories or tags, deduplicated |
| `enclosures` | array of objects | `{url, type, length}` podcast/media attachments |
| `error` | string | null | Safe diagnostic for a failed feed; `null` on success |

Dates are normalized to UTC with second precision. RSS dates with numeric
offsets, Atom dates with `Z` or offsets, RFC 822 dates and JSON Feed RFC 3339
dates are supported. An unparseable date becomes `null`; it is never invented.

`summary` selects the feed summary, then the first content block, and converts
HTML to text with scripts, styles and markup removed. `contentHtml` uses
feedparser's HTML sanitizer for both XML and JSON Feed content. Relative JSON
Feed entry and attachment URLs are resolved against the final feed URL. A typical row:

```json
{
  "feedUrl": "https://example.com/feed.xml",
  "feedTitle": "Example RSS",
  "title": "First & Best",
  "link": "https://example.com/posts/1",
  "guid": "post-1",
  "published": "2026-10-20T13:00:00Z",
  "updated": null,
  "author": "Alice",
  "summary": "Hello world & friends.",
  "contentHtml": "<article><p>Full <em>content</em>.</p></article>",
  "categories": ["Tech", "News"],
  "enclosures": [
    {"url": "https://example.com/ep1.mp3", "type": "audio/mpeg", "length": 12345}
  ],
  "error": null
}
```

### Deduplication and limits

- Within a feed, entries are deduplicated by GUID, or by link when no GUID exists.
- Across all feeds in one run, duplicate links are returned once; the first
  feed wins. Link fragments are ignored. GUID matching stays within each feed.
- Duplicate feed URLs in the input are processed once.
- `maxItemsPerFeed` is applied after `since` filtering and deduplication, keeping
  the first entries in feed order.
- Each decoded feed is limited to 5 MiB (5,242,880 bytes), even when compressed.
  Feeds with more than 50,000 source entries are rejected before normalization
  to bound parser memory. XML entry tags are counted conservatively, including
  tags inside comments or CDATA. `maxItemsPerFeed` does not bypass these limits.
  `summary` is truncated to 20,000 characters and
  `contentHtml` to 50,000 characters. At most 50 categories and 50 enclosures per
  entry are returned.
- A run has a 240-second processing budget and a 25-second per-feed timeout. Feeds
  not attempted in time receive a `Skipped` error row without a custom charge.
  Parsing runs in a worker thread. If parsing times out, its results are
  discarded and the run stops before another feed, to prevent overlapping
  parsers. Python cannot terminate an already running thread; it may finish
  while the process shuts down.
- Set the Actor run timeout to **300 seconds in Apify Console** before release.
  This platform setting is separate from the internal 240-second budget and is
  not configured by `actor.json`.

### Network and XML safety

Feed URLs must use HTTP or HTTPS on port 80 or 443, with no embedded credentials.
The host must resolve to public internet addresses; private, loopback, link-local,
reserved, multicast, NAT64 and unsafe embedded IPv4 addresses (including 6to4 and
Teredo) are rejected. The Actor dials
only the validated IP, validates the connected peer again, and keeps the original
hostname for the `Host` header and TLS SNI. Redirects are followed manually for at
most five hops and every hop is revalidated.

Downloaded XML is checked with `defusedxml`; any `DOCTYPE` or `ENTITY` declaration
is rejected to prevent entity expansion and external entity access. `feedparser`
then handles RSS 0.9x/1.0/2.0 and Atom, including relative links. JSON Feed 1.x is
parsed directly from UTF-8 JSON. Malformed HTML or arbitrary JSON returns a clear
error row.

### Pricing

One `item-parsed` event costs **$0.002** per normalized entry delivered. There is
no startup event. Error rows, duplicates and filtered entries are not charged. See
[`.actor/pricing.md`](.actor/pricing.md) for the cost model and assumptions.

On the Apify platform, configure only `item-parsed` and remove the synthetic
`apify-actor-start` and `apify-default-dataset-item` events. The runtime refuses to
push rows if either synthetic event has a positive price.

### Local development

Python 3.12 is the runtime. With `uv`:

```powershell
uv run --python 3.12 --no-project --with-requirements requirements-dev.txt python -m pytest tests -q -W error
```

Or with an existing Python 3.12 environment:

```powershell
python -m pip install -r requirements-dev.txt
python -m pytest tests -q
```

Tests block outbound sockets and use temporary local storage. Network behavior is
tested with `httpx.MockTransport`; no live feed is contacted and no event is
charged. Run the Actor locally with `python -m src` after installing
`requirements.txt`. Deployment is not performed by the test suite.

# Actor input Schema

## `feedUrls` (type: `array`):

1 to 20 public HTTP(S) feed URLs (RSS 0.9x/1.0/2.0, Atom or JSON Feed). Duplicate URLs are processed once. Only ports 80 and 443 are allowed; private and reserved addresses are rejected.

## `maxItemsPerFeed` (type: `integer`):

Applied after the since filter and deduplication. Default 50; maximum 200.

## `since` (type: `string`):

Optional ISO 8601 date or timestamp, for example 2026-10-01 or 2026-10-01T00:00:00Z. Values without an offset are treated as UTC. Entries without a published or updated date are excluded when this filter is set.

## Actor input object example

```json
{
  "feedUrls": [
    "https://hnrss.org/frontpage"
  ],
  "maxItemsPerFeed": 50
}
```

# Actor output Schema

## `entries` (type: `string`):

One row per feed entry plus per-feed error rows in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "feedUrls": [
        "https://hnrss.org/frontpage"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kadev-code/rss-atom-to-json").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "feedUrls": ["https://hnrss.org/frontpage"] }

# Run the Actor and wait for it to finish
run = client.actor("kadev-code/rss-atom-to-json").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "feedUrls": [
    "https://hnrss.org/frontpage"
  ]
}' |
apify call kadev-code/rss-atom-to-json --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kadev-code/rss-atom-to-json"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/MhnO8wyaQmUllFwj1/builds/baZeXCcdFh9NkqgGW/openapi.json
