Use this repository to call the Medium Author RSS Scraper from Node.js. It is one ES module built on the official apify-client package: hand it Medium handles or publication feed URLs, and it returns each source's latest posts as plain objects with title, URL, writer, tags, publish timestamp and an optional HTML body. Apify fetches and parses the feeds in the cloud, so your service, serverless handler or bot never parses XML itself. The objects are written to results.json and results.csv, or you can import the call and keep them in memory.
Actor: Medium Author RSS Scraper · Publisher: Devil Scrapes · Also in Python: medium-user-articles-scraper-python
- Accounts and publications in one list: a handle such as
@netflixtechblog(the@is optional) is turned into its Medium feed address, and a full feed URL likehttps://medium.com/feed/netflix-techblogis fetched as given. - Rows that carry the essentials: headline, article URL, the feed's guid as
article_id, the byline, the tags the writer picked and the publication time converted to ISO-8601 in UTC. - Article HTML on request: with
includeContenton,content_htmlholds the body from the feed, or its summary when that is all the feed provides; switch it off and the column isnull, which keeps rows small. - A per-account cap:
maxArticlesPerUser(1 to 50) trims each feed, although Medium itself rarely lists more than the ten newest posts. - One dead handle does not stop the batch: a feed that answers with an error is logged and skipped, and the remaining accounts are still delivered.
- Billed per article written: a start event plus a fee per row, so a handle whose feed is empty or unreachable costs nothing extra.
Run node index.js on a schedule, remember which article_id values you have already announced and send the rest to an incoming webhook. A team channel then hears about every new post from the engineering blogs it follows, without anyone subscribing to anything.
A Next.js or Express backend can pull fresh posts from a curated list of company accounts and render them as cards with title, tags and date. With includeContent enabled you can show an excerpt cut from content_html server-side.
If your team publishes on Medium, a serverless function can copy each new article's HTML into your own CMS or static site so the content also lives under your domain. Keep the original link for a canonical tag.
Configure an Apify webhook on run success and let it call your endpoint, which reads the dataset and queues translation, summarisation or tagging work for each article it has not processed before.
You need an Apify account and an API token from Console → Settings → Integrations. Apify's free plan includes monthly usage credit; this Actor's event prices are listed under Pricing below.
git clone https://github.com/devilscrapes/medium-user-articles-scraper-javascript.git
cd medium-user-articles-scraper-javascript
npm install
export APIFY_TOKEN=your_token_here
node index.js # uses input.json
node index.js my-input.json --out data --max-charge 2The script starts the Medium Author RSS Scraper, waits for it to finish, prints a link to the run in Apify Console and saves every result to results.json and results.csv. It exits with code 1 and the run's status message if the run fails.
Remove the CLI flags and file output from index.js and this is what remains. call() waits for the run to finish before resolving, then listItems() fetches the articles from the run's default dataset in one request.
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('DevilScrapes/medium-user-articles-scraper').call({"usernames": ["@netflixtechblog", "@AirbnbEng"], "maxArticlesPerUser": 5, "includeContent": false, "proxyConfiguration": {"useApifyProxy": true}});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);index.js passes input.json through unchanged. The bundled example follows the official Netflix Technology Blog and Airbnb Engineering accounts, takes five articles from each and skips the HTML body, which caps a trial at ten lightweight objects. Extend usernames with more handles or https://medium.com/feed/<publication> URLs as your list grows.
{
"usernames": [
"@netflixtechblog",
"@AirbnbEng"
],
"maxArticlesPerUser": 5,
"includeContent": false,
"proxyConfiguration": {
"useApifyProxy": true
}
}| Field | Type | Required | Default | Allowed values |
|---|---|---|---|---|
usernames |
array | yes | ["@medium"] |
— |
maxArticlesPerUser |
integer | no | 10 |
— |
includeContent |
boolean | no | true |
— |
proxyConfiguration |
object | no | {"useApifyProxy": true} |
— |
You get one object per article. Feed links carry a tracking query string, so split on ? when you use link as an ID or show it to readers; article_id, the feed's guid, is the steadier key. author holds the byline of that article, and published may be null when an entry has no parseable date.
| Field | Type | Always present |
|---|---|---|
username |
string | yes |
article_id |
string or null | no |
title |
string | yes |
link |
string | yes |
author |
string | no |
content_html |
string or null | no |
categories |
array | no |
published |
string or null | no |
scraped_at |
string | no |
| Event | Price (USD) |
|---|---|
| Actor start | $0.2 |
| Result emitted | $0.002 |
Prices as configured on 2026-08-20. The Actor page always shows the current price. Runs are billed to the Apify account that owns the token. Use --max-charge to cap what a single run can cost.
This Node-only snippet compares the saved articles with a small posted.json state file, formats the new ones as a Slack message with up to three tags each and POSTs it when SLACK_WEBHOOK_URL is set. Without the variable it prints the message instead.
import { readFile, writeFile } from 'node:fs/promises';
const articles = JSON.parse(await readFile('results.json', 'utf8'));
const statePath = 'posted.json';
const posted = new Set(JSON.parse(await readFile(statePath, 'utf8').catch(() => '[]')));
const idOf = (a) => a.article_id ?? a.link.split('?')[0];
const fresh = articles
.filter((a) => !posted.has(idOf(a)))
.sort((a, b) => (a.published ?? '').localeCompare(b.published ?? ''));
const lines = fresh.map((a) => {
const tags = a.categories?.length ? ` [${a.categories.slice(0, 3).join(', ')}]` : '';
return `• ${a.username}: <${a.link.split('?')[0]}|${a.title}>${tags}`;
});
const webhook = process.env.SLACK_WEBHOOK_URL;
if (webhook && lines.length > 0) {
const res = await fetch(webhook, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text: `New Medium articles:\n${lines.join('\n')}` }),
});
if (!res.ok) throw new Error(`Webhook answered HTTP ${res.status}`);
} else {
console.log(lines.join('\n') || 'Nothing new.');
}
for (const a of fresh) posted.add(idOf(a));
await writeFile(statePath, JSON.stringify([...posted]));Medium has not offered third-party developers a supported reading API for years. Its RSS feeds are the remaining public way to read posts from code, and this Actor turns them into JSON objects for you.
Medium answers that feed with an error, the Actor logs it and moves on, and the run still succeeds with the other accounts' articles. Check the run log through the Console link if a source is missing from the results.
Yes, as long as the platform lets a function wait for the run, which for a few feeds is usually well within common timeouts. Otherwise start the run with start() instead of call() and collect the dataset later from an Apify webhook.
A $0.20 start event per run and $0.002 for each article written to the dataset. Following ten accounts at ten posts each comes to $0.40 per run, and --max-charge caps any run at the amount you set.
Set APIFY_TOKEN first: export the token in the same shell, or put it in a local.envfile your shell loads. Never commit it.- Run finished with
FAILEDorABORTED: open the printed Console link; the run log says why. Input validation errors name the field. - Fewer rows than expected: a
--max-chargecap also limits how many results can be charged. Raise the cap or lower the limits in your input. - Something looks wrong in the data: open an issue on the Actor page so the maintainers see it.
- Medium Author RSS Scraper on Apify Store
- Python version of this example
- All Devil Scrapes Actors
- Apify web scraping cookbook
Related Actors:
- DEV.to Scraper — Articles by Tag & Author — the same kind of article feed from DEV.to, by tag or author.
- Substack Newsletter Scraper — full Substack archives with reaction and word counts.
- Hacker News Scraper — see which of those posts reached Hacker News and how they scored.
MIT licensed example code. The Actor itself runs on Apify and is maintained by Devil Scrapes. Not affiliated with Medium or A Medium Corporation.