# Medium Article Scraper (`memo23/medium-article-scraper`) Actor

Scrape full Medium article content past the paywall — title, subtitle, authors, publish and updated dates, reading time, cover image, table of contents, and the complete body as HTML, Markdown, and plain text. Accepts article URLs, author profiles, or publications. No login needed.

- **URL**: https://apify.com/memo23/medium-article-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** News, AI, Social media
- **Stats:** 2 total users, 1 monthly users, 89.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 articles

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

## Medium Article Scraper

**Turn Medium articles into structured content — past the paywall.** Scrape any Medium article's title, subtitle, author profile, publication, dates, reading time, word count, tags, claps, response count, cover image, table of contents, and the **full body as HTML, Markdown, and plain text** — even for member-only posts. Start from article URLs, an author's or publication's full archive, a tag, or a keyword search, and optionally pull each article's responses (comments) too. JSON or CSV out, billed per article.

### How it works

![How Medium Article Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-medium.png)

### ✨ Why use this scraper?

Building an LLM/RAG corpus from Medium? Archiving a publication? Tracking which posts take off? Need the full text of paywalled posts without a Medium subscription?

- **Five starting points.** Article URLs, author profiles (`@username`), publications (`data-science-collective` or a custom domain like `levelup.gitconnected.com`), tags (`machine-learning`), and keyword searches (`rust async`). Authors and publications are read as **full archives**, not just the latest 10 posts.
- **Paywall bypass, no login.** Every article body is resolved through the Freedium mirror, which returns the complete text for member-only posts — no cookies, no session, no subscription.
- **Engagement and context from Medium itself.** Claps, clapper count, response count, tags, language, license, and Medium's own word count, plus the author's bio, follower count and membership, and the publication's name and subscriber count.
- **Responses on demand.** Set `maxResponsesPerArticle` to attach each article's comments: author, claps, date, and text.
- **Three body formats.** Each row carries the body as `html`, `markdown`, and `text` (or pick one to shrink the dataset). Plain text is SVG-stripped and newline-normalized, ready for embeddings. Every image in the body is listed in `images`.
- **Clean exports.** One row per article. No padding, no duplicates across sources, and no empty rows: articles Freedium cannot resolve are skipped, so you never pay for them. Export as JSON, CSV, Excel, or through the API.

### Overview

Medium hides most of its best writing behind the member paywall and offers no public API for article bodies. This actor combines two sources. Article bodies and core metadata come from the Freedium mirror, which serves member-only posts in full. Listings (author and publication archives, tags, search) and engagement data (claps, responses, tags, author and publication profiles) come from Medium's own public GraphQL API, the one medium.com's web pages call. The result is one clean dataset row per article with the complete body in three formats.

### Supported inputs

Five input arrays, freely combined. Each is classified automatically.

| Input | Accepts | Behaviour |
|---|---|---|
| `startUrls` | `https://medium.com/@user/slug-<id>`, `https://medium.com/<pub>/slug-<id>`, `https://<user>.medium.com/slug-<id>`, custom-domain Medium blogs (`https://levelup.gitconnected.com/slug-<id>`) | Resolved directly |
| `searchQueries` | Any keywords, e.g. `rust async` | Medium's own search results; past Medium's ~1,000-per-query cap through publish-date windows |
| `authorProfiles` | `@username`, `https://medium.com/@username`, `username.medium.com` | The author's post history, newest first |
| `publications` | `data-science-collective`, `https://medium.com/data-science-collective`, `https://medium.com/feed/...`, `levelup.gitconnected.com` | The publication's archive, newest first |
| `tags` | `machine-learning`, `Machine Learning`, `https://medium.com/tag/machine-learning` | Newest posts under the tag |

Each author, publication, tag and search contributes up to `maxArticlesPerSource` articles (default 50); `maxItems` caps the whole run.

You can also paste any Medium link into `startUrls`: a search page (`https://medium.com/search?q=rust+async`) runs as a search, a tag page as a tag, a profile as an author, and a publication home page as a publication.

Easiest workflow: copy a Medium article URL from the address bar into `startUrls`, or drop an `@username` into `authorProfiles` to grab their posts.

**Not supported:**

- Articles that have been deleted from Medium (skipped and not charged, never a failed run).
- Replies to responses. `responses` holds the top-level responses only.
- Drafts, unlisted and private posts.

### 🎯 Use cases

| Team | What they build |
|------|-----------------|
| **AI / LLM apps** | RAG corpora and embedding pipelines from Medium's long-form content, filtered by tag and language |
| **Content & research** | Archive a publication's or author's full body of work before it is edited or paywalled |
| **Media monitoring** | Track what a tag, author or publication publishes, and which posts collect claps and responses |
| **NLP / data science** | Datasets of article text, word counts, tags, engagement, and reader responses for analysis |
| **Newsletter curators** | Find the most-clapped posts on a topic and pull their full text for summarization |

### Under the hood

1. **Gather article URLs.** `startUrls` are validated directly. Authors, publications, tags and searches are listed through Medium's GraphQL API, page by page. If that API is unreachable, authors, publications and tags fall back to their RSS feed (which only lists the 10 most recent posts).
2. **Dedupe + cap.** All article URLs are merged into one set, deduped, and capped at `maxItems`.
3. **Fetch Medium stats** for every queued article in batches of 25: claps, responses, tags, language, author and publication profiles. This step is best effort: if it fails, those fields are `null` and the article still comes back.
4. **Resolve each article body** through Freedium's `__data.json` endpoint in parallel (sliding-window concurrency), with up to 3 attempts per article. Both the cached payload and the streamed one Freedium sends on a cache miss are parsed.
5. **Fetch responses** (only when `maxResponsesPerArticle` is above 0), 25 per request.
6. **Push one row per article.** Articles Freedium cannot resolve are logged and skipped, never pushed as empty rows.

### Input configuration

| Parameter | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array | `[]` | Medium article URLs to scrape directly. |
| `searchQueries` | array | `[]` | Keyword searches through Medium's own search. Goes past 1,000 results per query. |
| `authorProfiles` | array | `[]` | Authors (`@username`, profile URLs, or `username.medium.com`). Full post history, newest first. |
| `publications` | array | `[]` | Publications (slug, URL, feed URL, or custom domain). Full archive, newest first. |
| `tags` | array | `[]` | Medium tags (slug, name, or tag URL). |
| `tagSort` | select | `NEWEST` | Tag feed order: `NEWEST`, `OLDEST`, or `MOST_READ` (a short list: about 15 to 20 posts per tag, or per month with a date range). |
| `publishedAfter` / `publishedBefore` | date | none | Only list articles first published in this window (inclusive). Applies to searches, tags, authors and publications. |
| `outputFormat` | select | `all` | Body formats per row: `all` (html + markdown + text), `markdown`, or `text`. |
| `includeTableOfContents` | boolean | `true` | Attach the parsed table of contents array to each row. |
| `maxResponsesPerArticle` | integer | `0` | Responses (comments) to attach per article. `0` = none. |
| `maxItems` | integer | `100` | Hard cap on articles scraped across all sources. |
| `maxArticlesPerSource` | integer | `50` | Cap per author, publication, tag or search query. |
| `maxConcurrency` | integer | `10` | Concurrent article fetches. |
| `proxy` | object | none | Optional proxy for all requests. Usually unnecessary. |

#### Example input

```json
{
    "startUrls": ["https://medium.com/data-science/diy-ai-how-to-build-a-linear-regression-model-from-scratch-7b4cc0efd235"],
    "authorProfiles": ["@karpathy"],
    "tags": ["machine-learning"],
    "searchQueries": ["rust async"],
    "publishedAfter": "2025-01-01",
    "outputFormat": "markdown",
    "maxArticlesPerSource": 20,
    "maxResponsesPerArticle": 10,
    "maxItems": 100
}
```

### Output overview

Each scraped article is one **single dataset row**. The row count equals the article count exactly — no padding rows, no separate metadata rows. Responses ride inside their article's row.

### Output sample

One row per article (body fields and long arrays trimmed):

```json
{
    "url": "https://medium.com/data-science/diy-ai-how-to-build-a-linear-regression-model-from-scratch-7b4cc0efd235",
    "postId": "7b4cc0efd235",
    "title": "DIY AI: How to Build a Linear Regression Model from Scratch",
    "subtitle": "How to implement a linear regression model in Python without using machine learning libraries",
    "authors": [{ "name": "Jacob Ingle", "avatar": "https://freedium-mirror.cfd/img/medium/700/1*ijIJvbZk0dDHKto0wcZrGQ.png" }],
    "authorNames": ["Jacob Ingle"],
    "author": {
        "id": "3da2be442381",
        "username": "jaingle77",
        "name": "Jacob Ingle",
        "bio": "Lead Data Scientist | AWS Machine Learning Certified | Passion for Quantitative Finance",
        "followerCount": 830,
        "isMediumMember": true,
        "avatar": "https://miro.medium.com/v2/resize:fill:176:176/1*ijIJvbZk0dDHKto0wcZrGQ.png",
        "profileUrl": "https://medium.com/@jaingle77"
    },
    "publication": {
        "id": "7f60cf5620c9",
        "name": "TDS Archive",
        "slug": "data-science",
        "domain": null,
        "subscriberCount": 828089,
        "url": "https://medium.com/data-science"
    },
    "publishedAt": "2025-02-03T18:54:44.674Z",
    "updatedAt": "2025-02-24T19:21:55.680Z",
    "readingTime": "14 min read",
    "wordCount": 3225,
    "language": "en",
    "isFree": false,
    "tags": [
        { "slug": "ai", "title": "AI" },
        { "slug": "data-science", "title": "Data Science" },
        { "slug": "machine-learning", "title": "Machine Learning" }
    ],
    "clapCount": 331,
    "clapperCount": 72,
    "responsesCount": 11,
    "license": "ALL_RIGHTS_RESERVED",
    "canonicalUrl": "https://medium.com/data-science/diy-ai-how-to-build-a-linear-regression-model-from-scratch-7b4cc0efd235",
    "coverImage": "https://freedium-mirror.cfd/img/medium/700/0*VAv-rUb3QaCnks82",
    "coverImageLarge": "https://freedium-mirror.cfd/img/medium/4000/0*VAv-rUb3QaCnks82",
    "coverImageCaption": "Photo by Sam Clarke on Unsplash",
    "images": ["https://freedium-mirror.cfd/img/medium/700/1*sKGe_yoXSuJSWPfE9_srog.png"],
    "tableOfContents": [
        { "id": "introduction", "level": 3, "title": "Introduction" },
        { "id": "multiple-linear-regression", "level": 3, "title": "Multiple Linear Regression" }
    ],
    "html": "<h4 id=\"a-walkthrough...\">A walkthrough on how one can implement...</h4>\n<p>The data used in this article...</p>",
    "markdown": "## A walkthrough on how one can implement...\n\nThe data used in this article...",
    "text": "A walkthrough on how one can implement a linear regression model in Python...\nThe data used in this article is licensed under a creative commons license...",
    "responses": [
        {
            "id": "e194e8d43f5f",
            "url": "https://medium.com/@iuaugmenta/great-article-e194e8d43f5f",
            "authorUsername": "iuaugmenta",
            "authorName": "Augmenta",
            "clapCount": 5,
            "publishedAt": "2025-04-02T19:13:53.643Z",
            "text": "great article"
        }
    ],
    "cacheStatus": "l2_hit_cdn",
    "error": null
}
```

### Key output fields

| Field | Type | Description |
|---|---|---|
| `url` | string | Canonical Medium article URL. |
| `postId` | string | Medium's 12-character post id. Stable; good as a dedupe key. |
| `title` / `subtitle` | string | Article title and subtitle. |
| `authors` / `authorNames` | array | `{ name, avatar }` per byline author, and the names alone. |
| `author` | object | Primary author: `username`, `name`, `bio`, `followerCount`, `isMediumMember`, `avatar`, `profileUrl`. |
| `publication` | object | `name`, `slug`, `domain`, `subscriberCount`, `url`; `null` for posts outside a publication. |
| `publishedAt` / `updatedAt` | string (ISO 8601) | First published and last updated timestamps. |
| `readingTime` | string | Medium's reading-time estimate (e.g. `"14 min read"`). |
| `wordCount` | number | Medium's word count (counted from the text if Medium's is unavailable). |
| `language` | string | Detected language code, e.g. `"en"`. |
| `isFree` | boolean | `true` if free to read, `false` if member-only. |
| `tags` | array | `{ slug, title }` per tag. |
| `clapCount` / `clapperCount` | number | Total claps, and the number of people who clapped. |
| `responsesCount` | number | Number of responses (comments). |
| `license` | string | Medium license, e.g. `ALL_RIGHTS_RESERVED` or a Creative Commons variant. |
| `canonicalUrl` | string | The canonical URL set by the author, when there is one. |
| `coverImage` / `coverImageLarge` | string | Cover image URL at 700 px and at full size. |
| `coverImageCaption` | string | Cover image caption / credit, as plain text. |
| `images` | array | Every image URL in the article body, in order. |
| `tableOfContents` | array | `{ id, level, title }` per heading, when `includeTableOfContents` is on. |
| `html` / `markdown` / `text` | string | Full article body, per `outputFormat`. |
| `responses` | array | `{ id, url, authorUsername, authorName, clapCount, publishedAt, text }` per response; `null` unless `maxResponsesPerArticle` is above 0. |
| `bodySource` | string | `freedium` (the paywall mirror) or `medium` (Medium's own API, used for free articles when the mirror is unreachable). |
| `cacheStatus` | string | Freedium cache state (`l2_hit_cdn`, `inline`, `miss`), or `medium-api` when the body came from Medium. |
| `error` | object | Freedium's error object, normally `null`. Articles Freedium cannot resolve are skipped instead of pushed. |

Engagement fields (`clapCount`, `clapperCount`, `responsesCount`, `language`, `license`, `author.bio`, `author.followerCount`, publication details) come from Medium's API and are `null` if that lookup failed for a batch; the article itself still comes back.

### FAQ

**Does this work for member-only / paywalled articles?** Yes. Freedium returns the full body regardless of the paywall, so `isFree: false` articles come back complete just like free ones.

**Can I scrape everything by an author or publication?** Yes. Authors and publications are read as full archives, newest first. Raise `maxArticlesPerSource` (up to 5,000) and `maxItems` to go deeper. In testing, 120 article URLs from an author's archive were listed in about 2 seconds.

**Can I scrape by topic or keyword?** Yes. Put tag slugs or names in `tags` for the newest posts under a tag, or keywords in `searchQueries` for Medium's own search results. A pasted `medium.com/search?q=...` link in `startUrls` works the same way. Set `tagSort` to `MOST_READ` for a tag's most-read posts, and `publishedAfter` / `publishedBefore` to limit any listing to a date window.

**Can I get more than 1,000 search results?** Yes. Medium's search serves only about 1,000 results per query (40 pages of 25). When a search hits that cap and you asked for more, the actor repeats it over publish-date windows, newest first (about a month each, halved again when a window is itself capped), and merges the results without duplicates. In testing, "python" returned 2,500 unique articles in about 90 seconds. Raise `maxArticlesPerSource` and `maxItems` to go deeper.

**Does it get comments?** Set `maxResponsesPerArticle` (for example 50) and each row gets a `responses` array with the top-level responses: author, claps, date, and text.

**Does it work for Medium blogs on their own domain?** Yes. Custom-domain Medium blogs such as `levelup.gitconnected.com` or `netflixtechblog.com` work as article URLs in `startUrls` and as publications (`levelup.gitconnected.com` in `publications`).

**Why are image/avatar URLs on `freedium-mirror.cfd`?** Freedium proxies Medium's images through its own host. The URLs are absolute and load directly. `author.avatar` comes from Medium's own image CDN.

**What happens with a deleted or invalid article URL?** Freedium answers `ARTICLE_NOT_FOUND`. Freedium sometimes sends that for live articles too (about 1 request in 13 on freshly published posts in testing), so the actor tries each article up to 3 times before skipping it. Skipped articles are not pushed and not charged, and they never fail the run. An author, publication or tag that does not exist is skipped with a note in the run's status message.

**What if the Freedium mirror or Medium is down?** Freedium is a third-party mirror and has outages (one ran for more than a day in October 2026). When it is unreachable, the actor gets the body from Medium's own API instead: free articles still come back in full, marked `bodySource: "medium"`. Medium only serves a short preview of member-only articles, so those are skipped and not charged, and the run's status message says how many. After a few failed connections in a row the actor stops trying the mirror for the rest of the run, so an outage costs seconds, not minutes. If nothing at all comes back, the run ends as FAILED with the reason in its status message. If only Medium's API is unreachable, authors, publications and tags fall back to their RSS feeds (latest 10 posts) and the engagement fields are `null`.

**Is a proxy needed?** Usually not. Freedium and Medium's API both answered from datacenter IPs in testing. Leave `proxy` empty unless you hit rate limits; when set, it is used for every request.

**How is it billed?** Pay per event: a small fee when the run starts, then a fee per article row pushed to the dataset. Engagement data and responses are included in the article price. Skipped and failed articles are never charged. If you set a maximum cost per run, the actor stops at the number of articles that budget covers. On Apify's free plan, each run is capped at 60 articles and the first start URL; paid plans have no cap beyond your own `maxItems`. See the Pricing tab for current rates.

### Your feedback

If this scraper saves you time, a **short review on this page** helps other people find it. If something's missing or broken instead, open an issue first: bugs and feature requests usually ship within days.

### Support

- For issues or feature requests, please use the **Issues** tab of this actor on Apify Console.
- For further assistance, contact the author:
  - Author's website: <https://muhamed-didovic.github.io/>
  - Email: <muhamed.didovic@gmail.com>

### Additional services

- Request customization, a full dataset, or a private build: <muhamed.didovic@gmail.com>
- Need scheduled monitoring of authors, publications or tags delivered to your database, or other blogging platforms scraped (Substack, Dev.to, Hashnode, Ghost)? Contact <muhamed.didovic@gmail.com>
- For API services of this scraper, reach out to <muhamed.didovic@gmail.com>
- Explore other scrapers at [memo23's Apify profile](https://apify.com/memo23)

### Explore More Scrapers

Other actors in the memo23 portfolio you might find useful:

- [**Substack Scraper**](https://apify.com/memo23/substack-scraper): posts, comments, and subscriber data from Substack newsletters
- [**Google News Scraper**](https://apify.com/memo23/google-news-scraper): news results with full article bodies and entities
- [**LinkedIn Posts Scraper**](https://apify.com/memo23/linkedin-posts-scraper): posts from LinkedIn profiles and company pages, no cookies
- [**Quora Scraper**](https://apify.com/memo23/quora-scraper): questions, answers, and topics by search or URL
- [**Reddit Scraper**](https://apify.com/memo23/reddit-scraper): posts, users, and subreddits
- [**YouTube Transcript Scraper**](https://apify.com/memo23/youtube-transcript-scraper): transcripts as text, SRT, or VTT for any YouTube video

### 🤖 For AI Agents & LLM Apps

Compact reference for AI agents calling this actor via the [Apify MCP server](https://mcp.apify.com) or the Apify API (actor: `memo23/medium-article-scraper`).

**Purpose:** Returns the full body (HTML, Markdown, plain text) of Medium articles, member-only posts included, with engagement (claps, responses), tags, and author and publication profiles. One dataset row per article.

**Minimal input:**

```json
{
  "startUrls": ["https://karpathy.medium.com/software-2-0-a64152b37c35"],
  "outputFormat": "markdown"
}
```

Other modes: `{ "authorProfiles": ["@karpathy"] }`, `{ "publications": ["data-science-collective"] }`, `{ "tags": ["machine-learning"] }`, `{ "searchQueries": ["rust async"] }`. All five arrays can be combined in one run.

**Output:** url, postId, title, subtitle, authors, authorNames, author {username, name, bio, followerCount, isMediumMember, avatar, profileUrl}, publication {name, slug, domain, subscriberCount, url}, publishedAt, updatedAt, readingTime, wordCount, language, isFree, tags \[{slug, title}], clapCount, clapperCount, responsesCount, license, canonicalUrl, coverImage, coverImageLarge, coverImageCaption, images, tableOfContents, html, markdown, text, responses, cacheStatus, error.

**Behaviors an agent should know:**

- Set `outputFormat: "markdown"` or `"text"` to cut row size; `all` returns the body three times.
- `maxArticlesPerSource` (default 50) caps each author, publication, tag and search; `maxItems` (default 100) caps the run. `ACTOR_MAX_PAID_DATASET_ITEMS` / `ACTOR_MAX_TOTAL_CHARGE_USD` run limits are honoured. Free-plan users are capped at 60 articles and the first start URL per run.
- `responses` is `null` unless `maxResponsesPerArticle` > 0; it holds top-level responses only.
- Engagement fields are `null` when Medium's API lookup fails; the body still comes back.
- Deleted or invalid URLs are skipped after 3 attempts and not billed. Search, tag, profile and publication links pasted into `startUrls` are routed to the matching source.
- Searches go past Medium's ~1,000-per-query cap through publish-date windows; deep searches take longer (2,500 results ≈ 90 s).
- `publishedAfter` / `publishedBefore` limit searches, tags, authors and publications to a date window; `tagSort` picks `NEWEST`, `OLDEST` or `MOST_READ`.
- If the Freedium mirror is down, free articles still come back (from Medium's API, `bodySource: "medium"`); member-only ones are skipped and not charged.
- A run where every article (or every requested source) fails ends FAILED with the reason in the status message.
- Pay-per-event billing: a small actor-start fee plus one charge per article row; see the Pricing tab.

### ⚠️ Disclaimer

This actor scrapes publicly accessible Medium article content via the third-party Freedium mirror and Medium's public web API for research and personal use. It does not bypass authentication on Medium itself. You are responsible for complying with Medium's Terms of Service and the copyright of the content you scrape. The actor and its author are not affiliated with, endorsed by, or sponsored by Medium (A Medium Corporation) or Freedium; all trademarks belong to their respective owners and are used here only to describe the sources the actor reads. Do not use this actor to bulk-republish copyrighted articles.

The author provides this actor "as is", without warranty of any kind, and disclaims any liability for damages arising from its use.

### SEO Keywords

medium scraper, medium article scraper, medium paywall bypass, medium to markdown, medium rss scraper, scrape medium articles, medium content extractor, medium author scraper, medium publication scraper, medium tag scraper, medium search scraper, medium claps scraper, medium comments scraper, medium responses, medium article text, medium to text, medium api, freedium scraper, medium member-only article, medium archive, medium rag dataset, medium llm training data, medium custom domain scraper

# Actor input Schema

## `startUrls` (type: `array`):

Medium article URLs to scrape directly: medium.com/@user/slug-id, medium.com/publication/slug-id, username.medium.com/slug-id, or custom-domain Medium blogs (levelup.gitconnected.com/slug-id). Each is resolved through the Freedium mirror, so the full body comes back even for paywalled posts. Other Medium links are routed automatically: a search page (medium.com/search?q=...) runs as a search query, a tag page as a tag, a profile as an author, and a publication home page as a publication.

## `searchQueries` (type: `array`):

Keyword searches run through Medium's own search (e.g. `rust async`). Up to `maxArticlesPerSource` articles per query. Medium's search serves only ~1,000 results per query, so for more the actor repeats the query over publish-date windows (newest first) and merges them: "python" gave 2,500 unique articles in testing. A pasted medium.com/search?q=... link in Article URLs works too.

## `authorProfiles` (type: `array`):

Medium author profiles to enumerate. Accepts `@username`, `https://medium.com/@username`, or `username.medium.com`. The actor pages through the author's full post history, newest first, up to `maxArticlesPerSource`.

## `publications` (type: `array`):

Medium publications to enumerate. Accepts a slug (`data-science-collective`), `https://medium.com/data-science-collective`, the feed URL, or a custom-domain publication's home page (`levelup.gitconnected.com`). The actor pages through the publication's archive, newest first, up to `maxArticlesPerSource`.

## `tags` (type: `array`):

Medium tags to enumerate, newest posts first. Accepts a slug (`machine-learning`), a name (`Machine Learning`), or a `https://medium.com/tag/machine-learning` URL. Up to `maxArticlesPerSource` articles per tag.

## `tagSort` (type: `string`):

Order of the posts taken from each tag: newest first, oldest first, or Medium's most-read ranking. The most-read list is short (about 15 to 20 posts per tag, or per month when a date range is set).

## `publishedAfter` (type: `string`):

Only list articles first published on or after this date (YYYY-MM-DD). Applies to search queries, tags, authors and publications, not to article URLs. Searches use it server-side, so a narrow window returns results from that period.

## `publishedBefore` (type: `string`):

Only list articles first published on or before this date (YYYY-MM-DD, inclusive). Same scope as `publishedAfter`.

## `outputFormat` (type: `string`):

Which body representations to emit per article. `all` = html + markdown + text (largest, most flexible). `markdown` = markdown only. `text` = plain text only (smallest).

## `includeTableOfContents` (type: `boolean`):

Attach the parsed table of contents array to each row.

## `maxResponsesPerArticle` (type: `integer`):

Attach up to this many responses (Medium comments) to each article as a `responses` array with author, claps, date and text. 0 = none (faster). Each 25 responses cost one extra request per article.

## `maxItems` (type: `integer`):

Maximum articles to scrape across all sources.

## `maxArticlesPerSource` (type: `integer`):

Cap on articles taken from any one author, publication, tag or search query. `maxItems` still caps the whole run.

## `maxConcurrency` (type: `integer`):

Concurrent article fetches against the Freedium mirror.

## `proxy` (type: `object`):

Optional proxy for the Medium RSS and Freedium requests. Freedium is CDN-served and accepts datacenter IPs, so a proxy is usually unnecessary.

## Actor input object example

```json
{
  "startUrls": [
    "https://medium.com/data-science/diy-ai-how-to-build-a-linear-regression-model-from-scratch-7b4cc0efd235",
    "https://karpathy.medium.com/software-2-0-a64152b37c35"
  ],
  "tagSort": "NEWEST",
  "outputFormat": "all",
  "includeTableOfContents": true,
  "maxResponsesPerArticle": 0,
  "maxItems": 100,
  "maxArticlesPerSource": 50,
  "maxConcurrency": 10
}
```

# Actor output Schema

## `articles` (type: `string`):

Full Medium articles with engagement (claps, clappers, responses), tags, author profile (bio, followers), publication, images, table of contents, and body in HTML, Markdown, and plain text. Optional responses (comments) per article.

# 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 = {
    "startUrls": [
        "https://medium.com/data-science/diy-ai-how-to-build-a-linear-regression-model-from-scratch-7b4cc0efd235",
        "https://karpathy.medium.com/software-2-0-a64152b37c35"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/medium-article-scraper").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 = { "startUrls": [
        "https://medium.com/data-science/diy-ai-how-to-build-a-linear-regression-model-from-scratch-7b4cc0efd235",
        "https://karpathy.medium.com/software-2-0-a64152b37c35",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("memo23/medium-article-scraper").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 '{
  "startUrls": [
    "https://medium.com/data-science/diy-ai-how-to-build-a-linear-regression-model-from-scratch-7b4cc0efd235",
    "https://karpathy.medium.com/software-2-0-a64152b37c35"
  ]
}' |
apify call memo23/medium-article-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,memo23/medium-article-scraper"
        }
    }
}
```

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/bMMISwBjdJnYp6MYF/builds/vrHbKXA9Vl9MGsPcu/openapi.json
