# Medium Scraper — Articles, Full Text, Authors, Claps & Comments (`logiover/medium-scraper`) Actor

Scrape Medium by tag, search, author, publication or URL: posts with claps, responses, reading time, tags, author and publication stats, plus optional full article text as Markdown and comments. Whole tag archives, thousands of posts per run, no login. Not affiliated with Medium.

- **URL**: https://apify.com/logiover/medium-scraper.md
- **Developed by:** [Logiover](https://apify.com/logiover) (community)
- **Categories:** Social media, News
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Scraper

**Medium Scraper** is a fast Medium article scraper and a practical **Medium API alternative**. Scrape Medium posts, claps, responses (comments), reading time, tags, authors and publications **without login, cookies or a Medium account** — by tag, by keyword search, by author, by publication or from any Medium URL — and export the Medium data to **CSV, Excel, JSON, XML or Google Sheets**. Turn on one switch and every story also comes with its **full article text as clean Markdown and plain text**, ready for AI, RAG and LLM pipelines.

Use it as a **Medium tag scraper** (the complete newest-first archive of any tag, not just the 10 stories an RSS feed shows), a **Medium author scraper** (every story a writer has published, plus their follower count and bio), a **Medium publication scraper** (any publication, custom domains such as `levelup.gitconnected.com` included), a **Medium search scraper** (posts, people, publications or tags for any keyword) and a **Medium comments scraper** (responses and reply threads with full text). Measured on 2026-10-13: **10,000 posts in 40 seconds**, the entire history of a tag (4,840 posts back to 2018) in 34 seconds, and 1,000 posts in under 10 seconds.

> Medium Scraper is an independent tool. It is **not affiliated with, endorsed by or sponsored by Medium** (A Medium Corporation). See the [disclaimer](#disclaimer).

***

### What you get

One run can mix several sources. Every row has a `rowType`, so posts, responses and profiles can live in one dataset and be filtered or exported separately (the Dataset tab has a ready-made view for each).

| Row type | One row per | Main fields |
|---|---|---|
| `post` | Medium story | title, subtitle, URL, canonical URL, published / updated date, reading time, word count, **claps**, **clappers**, **responses count**, tags, member-only flag, language, preview image, author (name, username, followers, bio, member status, X/Twitter), publication (name, slug, URL, followers), rank, source |
| `post` + full text | Medium story with `includeContent` | everything above plus **article text** (plain), **Markdown** (headings, lists, quotes, code blocks with language, links, images), image URLs, outbound links, code-block and paragraph counts, preview flag |
| `response` | response (comment) or reply | full text, claps, clappers, replies count, depth (1 = response, 2 = reply …), parent and root post IDs, root post URL, author name / username / followers, date |
| `author` | writer | name, username, profile URL, bio, followers, following, photo, member since, membership tier, Partner Program, book-author badge, X/Twitter, custom domain |
| `publication` | publication | name, slug, URL, custom domain, description, followers, logo, X/Twitter, topics, owner, editors and editor count |
| `tag` | tag / topic | name, slug, URL, **total posts in the tag**, followers |

**71 documented fields**, typed in the dataset schema, with consistent empty values (`null`, never `0` for "unknown").

***

### Why this Medium scraper

- **No login, no cookies, no browser.** It reads the same data Medium's own web app loads for signed-out visitors. Nothing to configure, nothing to get banned.
- **Real depth.** Tags are read from Medium's complete chronological tag archive — every story ever tagged, newest first — not from a ranked list that stops at ~100. Authors and publications are read back to their first story.
- **Fast.** Big sources are read in parallel time slices and delivered strictly newest-first: 1,000 posts in 6–10 s, 10,000 in 40 s.
- **Full article text** as Markdown and plain text with one switch, at no extra request cost (the text rides along with each page of posts).
- **Comments too.** Responses and reply threads, with full text, claps and authors.
- **Exact limits.** `maxItems` is honoured to the row — you never get (or pay for) more rows than you asked for. Duplicates are removed across all sources in a run.
- **Honest run status.** A tag, author or publication that does not exist ends the run as *Succeeded* with a clear message ("tag … does not exist on Medium"), not as a mysterious failure. A run fails only when Medium could not be reached at all.
- **Built-in reliability.** Fresh connection per request, automatic retries and hedged requests, automatic fallback to Apify Proxy if Medium ever refuses direct requests, and a time budget that always leaves room to save your data before the run's timeout.
- **Fresh data.** Everything is fetched live at run time — claps, follower counts and responses are as of the minute you run it.

***

### Use cases

#### Content research and topic discovery

Pull the newest 5,000 stories of `artificial-intelligence`, `product-management` or `web3` and see what people write about, which angles get claps, which subtitles work, how long successful posts are (`wordCount`, `readingTimeMinutes`) and which tags travel together.

#### Competitor and blog monitoring

Track a competitor's engineering blog or a publication in your niche. Schedule the Actor daily with `publishedAfter: "1 day"` and get only the new stories, with claps and responses, in your inbox, Slack or Google Sheet.

#### Writer and influencer discovery

Search people for "data engineering" or scrape the top lists of a tag to find the writers your audience reads — with follower counts, bios, X/Twitter handles, Partner Program status and their full story history. Great for outreach lists, guest-post sourcing and creator marketing.

#### AI, RAG and LLM datasets

Collect clean article text and Markdown (code blocks keep their language) with authors, dates, tags and canonical URLs for retrieval-augmented generation, semantic search, summarisation or classification. Use `skipMemberOnly` to keep only stories whose complete text is public, and `language: "en"` to keep one language.

#### Trend analysis and market intelligence

Measure how a topic grows: posts per day in a tag, average claps, share of member-only stories, top publications, top authors. Because the tag archive is chronological and complete, `publishedAfter` / `publishedBefore` give exact time windows (a whole month of a busy tag is cheap).

#### SEO and content-gap analysis

Search Medium for your keywords, export titles, subtitles and tags, and find the questions your blog has not answered yet.

***

### How it works

1. **Inputs are normalised.** Tag names, `@usernames`, profile URLs, publication slugs, custom domains, story URLs and search URLs are all recognised automatically.
2. **Medium's own data endpoint is queried** — the GraphQL API medium.com's web app uses: exact clap counts, IDs and timestamps, not HTML that changes with every redesign.
3. **Chronological sources are read in parallel time slices.** The first page measures how many stories per hour a tag, author or publication produces; the time range that should hold your `maxItems` is then cut into small slices that up to 16 lanes read at once. Rows are released strictly newest-first, so when the run reaches `maxItems` you still have exactly the newest N stories, and `rank` 1 is the newest.
4. **Ranked lists and search** are paged in parallel and delivered in Medium's own order (`rank` = position).
5. **Responses** are fetched for many posts per request, best first, with optional reply threads.
6. **Every row is checked against the dataset schema**, de-duplicated across sources and counted before it is written, so `maxItems` is exact even with 20 requests in flight.
7. A **SUMMARY** record (key-value store) lists rows per type, notes such as "author @x not found", and why the run stopped (`stoppedBy`: `MAX_ITEMS`, `MAX_COST`, `TIMEOUT` or none).

***

### Quick start

1. Open the Actor and type a tag into **Tags**, e.g. `artificial-intelligence` (that is the prefilled example).
2. Set **Max rows** (100 for a test, 10,000+ for a real export).
3. Click **Start**. Rows appear in the Dataset tab within a few seconds. Export as CSV, Excel, JSON, XML or HTML, or connect the run to Google Sheets, Make, Zapier or n8n.

Prefilled example (finishes in about 4 seconds):

```json
{
  "tags": ["artificial-intelligence"],
  "tagSort": "newest",
  "maxItems": 100,
  "proxyConfiguration": { "useApifyProxy": true }
}
```

***

### Input reference

| Field | Type | Default | What it does |
|---|---|---|---|
| `tags` | list | — | Tags as slugs, names or URLs: `artificial-intelligence`, `Machine Learning`, `https://medium.com/tag/python`. |
| `tagSort` | select | `newest` | `newest` = complete chronological archive (unlimited depth). `trending`, `top-week`, `top-month`, `top-year`, `top-all-time` = Medium's ranked lists (~100 each). `all-top` = all ranked lists combined. |
| `searchQueries` | list | — | Keywords for Medium search, one per line. |
| `searchType` | select | `posts` | What the queries search: `posts`, `people`, `publications` or `tags`. |
| `authors` | list | — | `@username`, `username`, `https://medium.com/@name` or `https://name.medium.com`. Returns the profile and all stories, newest first. |
| `publications` | list | — | Slug (`better-programming`), URL (`https://medium.com/swlh`) or custom domain (`levelup.gitconnected.com`). Returns the publication and all its stories. |
| `startUrls` | list | — | Any Medium URL: story (also on custom domains), tag page, profile, publication, or search page (`https://medium.com/search?q=rust`). Story IDs work too. |
| `maxItems` | number | 1000 | Total rows for the run (posts + responses + profile rows). `0` = no limit. |
| `maxPostsPerSource` | number | 0 | Cap per tag / author / publication / query, so one big source cannot use up the run. `0` = no cap. |
| `publishedAfter` | text | — | Only stories published on/after this date: `2025-01-01`, `2025-01-01T12:00:00Z`, or relative `30 days`, `6 months`, `1 year`. Chronological sources stop reading here. |
| `publishedBefore` | text | — | Only stories published before this date (same formats). Chronological sources start reading here. |
| `minClaps` | number | 0 | Skip stories with fewer claps. |
| `language` | text | — | Keep only these languages, ISO codes comma-separated: `en` or `en, es, pt`. |
| `skipMemberOnly` | yes/no | no | Leave out member-only (paywalled) stories. |
| `includeContent` | yes/no | no | Add article text (plain + Markdown), image URLs, links, code-block count. |
| `includeResponses` | yes/no | no | Add responses (comments) of every collected story as `response` rows. |
| `maxResponsesPerPost` | number | 50 | Responses (and replies) per story, best first. `0` = all. |
| `includeReplies` | yes/no | no | Follow reply threads under responses (depth 2, 3 …). |
| `includeProfiles` | yes/no | yes | Add one profile row for each tag, author and publication you list. |
| `maxConcurrency` | number | 20 | Requests in flight (1–50). |
| `proxyConfiguration` | proxy | Apify Proxy | Default: direct connection (fastest) with automatic switch to Apify Proxy if Medium refuses. Pick a proxy group or your own proxies to force them. |

#### Examples for every mode

**Newest stories of several tags (complete archive):**

```json
{ "tags": ["artificial-intelligence", "startup", "javascript"], "maxItems": 5000 }
```

**A precise time window — every English story in a tag during January 2025 with at least 50 claps:**

```json
{
  "tags": ["machine-learning"],
  "publishedAfter": "2025-01-01",
  "publishedBefore": "2025-02-01",
  "minClaps": 50,
  "language": "en",
  "maxItems": 0
}
```

**Medium's best stories of a topic (trending + top week / month / year / all time):**

```json
{ "tags": ["productivity"], "tagSort": "all-top", "maxItems": 500 }
```

**Keyword search for posts:**

```json
{ "searchQueries": ["rust async", "vector database"], "searchType": "posts", "maxItems": 2000 }
```

**Find writers (people search):**

```json
{ "searchQueries": ["data engineering", "growth marketing"], "searchType": "people", "maxItems": 1000 }
```

**Every story of some authors, with their profiles:**

```json
{ "authors": ["@ev", "https://medium.com/@devrimozcay"], "maxItems": 0 }
```

**A whole publication (custom domains work):**

```json
{ "publications": ["levelup.gitconnected.com", "better-programming"], "maxPostsPerSource": 2000, "maxItems": 4000 }
```

**Specific stories with full text and comments:**

```json
{
  "startUrls": [
    "https://medium.com/ai-engineering-simplified/5-claude-tricks-that-give-you-an-unfair-advantage-most-people-still-use-the-old-way-0385a58057c7",
    "https://levelup.gitconnected.com/macos-never-got-the-file-manager-developers-deserved-so-i-built-it-e8ac81480578"
  ],
  "includeContent": true,
  "includeResponses": true,
  "includeReplies": true,
  "maxResponsesPerPost": 0
}
```

**An AI / RAG corpus — free stories only, full text, one language:**

```json
{
  "tags": ["llm", "retrieval-augmented-gen", "prompt-engineering"],
  "includeContent": true,
  "skipMemberOnly": true,
  "language": "en",
  "maxItems": 10000
}
```

**Daily monitoring (schedule this every morning):**

```json
{ "tags": ["your-niche-tag"], "publications": ["competitor-publication"], "publishedAfter": "1 day", "maxItems": 0 }
```

***

### Output data dictionary

#### Post rows (`rowType: "post"`)

| Field | Type | Description |
|---|---|---|
| `id` | string | Medium story ID (12 hex characters). Stable — use it to de-duplicate across runs. |
| `title`, `subtitle` | string | Headline and the subtitle / preview line Medium shows in feeds. |
| `url` | string | Story URL (the publication's custom domain when it has one). |
| `canonicalUrl` | string | Canonical link; differs from `url` when the story was first published elsewhere. |
| `publishedAt`, `updatedAt` | ISO date | First publication and latest published edit (UTC). |
| `readingTimeMinutes` | number | Medium's reading-time estimate. |
| `wordCount` | number | Words in the full story (also for member-only stories). |
| `claps`, `voters` | number | Total claps and number of distinct readers who clapped. |
| `responsesCount` | number | Number of responses (comments). |
| `tags` | string | Tag slugs, pipe-separated (`ai \| startup \| productivity`). |
| `isMemberOnly`, `visibility` | boolean, string | Paywall flag; `PUBLIC`, `LOCKED` or `UNLISTED`. |
| `language` | string | Detected language (ISO 639-1). |
| `previewImageUrl` | string | Feature image, 1200 px wide. |
| `isShortform` | boolean | Medium short-form post. |
| `authorId`, `authorName`, `authorUsername`, `authorUrl` | string | The writer. |
| `authorBio`, `authorFollowers`, `authorFollowing`, `authorImageUrl` | mixed | Writer's bio, followers, following, photo. |
| `authorIsMember`, `authorTwitter` | boolean, string | Paying Medium member; X/Twitter handle. |
| `publicationId`, `publicationName`, `publicationSlug`, `publicationUrl`, `publicationFollowers` | mixed | Publication the story appeared in (empty for personal-profile stories). |
| `contentText`, `contentMarkdown` | string | Article body (with `includeContent`). Member-only stories: the public preview. |
| `contentIsPreview` | boolean | `true` when the body is the paywall preview, `false` when it is the complete story. |
| `contentImageUrls`, `contentLinks` | string | Images and outbound links / embeds inside the article, pipe-separated. |
| `codeBlockCount`, `paragraphCount` | number | Code blocks and paragraphs in the body. |
| `source` | string | Which input produced the row: `tag:<slug>`, `tag:<slug>/top-week`, `search:<query>`, `author:<username>`, `publication:<slug>`, `url`. |
| `rank` | number | Position in its source: search rank, position in a ranked list, or 1 = newest for chronological sources. |
| `scrapedAt` | ISO date | Collection time. |

#### Response rows (`rowType: "response"`)

| Field | Type | Description |
|---|---|---|
| `id`, `url` | string | Response ID and URL (responses are small Medium stories). |
| `text` | string | Full response text. |
| `publishedAt`, `updatedAt` | ISO date | When it was written / edited. |
| `claps`, `voters`, `repliesCount` | number | Claps, clappers, replies under it. |
| `depth` | number | 1 = response to the story, 2 = reply to a response, … |
| `parentPostId`, `rootPostId`, `rootPostUrl` | string | What it answers, and the story the thread belongs to. |
| `authorId`, `authorName`, `authorUsername`, `authorUrl`, `authorFollowers` | mixed | Who wrote it. |

#### Author rows (`rowType: "author"`)

`id`, `name`, `username`, `url`, `bio`, `followers`, `following`, `imageUrl`, `isMember`, `memberSince`, `membershipTier` (`MEMBER` / `FRIEND`), `isPartnerProgram`, `isBookAuthor`, `twitter`, `customDomain`, `source`, `rank` (search results only), `scrapedAt`.

#### Publication rows (`rowType: "publication"`)

`id`, `name`, `slug`, `url`, `domain`, `description`, `followers`, `imageUrl`, `twitter`, `tags`, `creatorName`, `creatorUsername`, `editors`, `editorsCount`, `source`, `rank` (search results only), `scrapedAt`.

#### Tag rows (`rowType: "tag"`)

`id` (slug), `name`, `url`, `postCount` (total stories in the tag), `followers`, `source`, `rank` (search results only), `scrapedAt`.

***

### Example output

Real rows from test runs on 2026-10-13 (article text shortened here).

**Post with full text** (`tags: ["productivity"]`, `tagSort: "top-all-time"`, `includeContent: true`):

```json
{
  "rowType": "post",
  "id": "5f26de35b84c",
  "title": "You Shouldn’t Be Prompting AI Anymore. You Should Be Designing Loops.",
  "subtitle": "Peter Steinberger built one of the fastest-growing open-source projects in history by doing something most developers were too busy to…",
  "url": "https://aiadvances.org/you-shouldnt-be-prompting-ai-anymore-you-should-be-designing-loops-5f26de35b84c",
  "canonicalUrl": "https://aiadvances.org/you-shouldnt-be-prompting-ai-anymore-you-should-be-designing-loops-5f26de35b84c",
  "publishedAt": "2026-06-09T14:14:08.695Z",
  "updatedAt": "2026-06-09T14:14:08.695Z",
  "readingTimeMinutes": 11,
  "wordCount": 2853,
  "claps": 379,
  "voters": 87,
  "responsesCount": 9,
  "tags": "software-development | artificial-intelligence | programming | productivity | technology",
  "isMemberOnly": false,
  "visibility": "PUBLIC",
  "language": "en",
  "previewImageUrl": "https://miro.medium.com/v2/resize:fit:1200/1*FpjCUf87cdd6H5sedbi0bg.png",
  "isShortform": false,
  "authorId": "106212946ead",
  "authorName": "Aftab",
  "authorUsername": "aftab001x",
  "authorUrl": "https://medium.com/@aftab001x",
  "authorBio": "AI",
  "authorFollowers": 381,
  "authorFollowing": 2,
  "authorImageUrl": "https://miro.medium.com/v2/resize:fit:400/1*6EU4UFLYSEtZqDugnds7tQ.jpeg",
  "authorIsMember": false,
  "authorTwitter": null,
  "publicationId": "3fe99b2acc4",
  "publicationName": "AI Advances",
  "publicationSlug": "ai-advances",
  "publicationUrl": "https://aiadvances.org",
  "publicationFollowers": 113201,
  "contentText": "You Shouldn’t Be Prompting AI Anymore. You Should Be Designing Loops.\n\nPeter Steinberger built one of the fastest-growing open-source projects in history by doing something most developers were too busy to notice: he stopped talki …",
  "contentMarkdown": "### You Shouldn’t Be Prompting AI Anymore. You Should Be Designing Loops.\n\n![](https://miro.medium.com/v2/resize:fit:1400/1*FpjCUf87cdd6H5sedbi0bg.png)\n\nPeter Steinberger built one of the fastest-growing open-source projects in hi …",
  "contentIsPreview": false,
  "contentImageUrls": "https://miro.medium.com/v2/resize:fit:1400/1*FpjCUf87cdd6H5sedbi0bg.png",
  "contentLinks": null,
  "codeBlockCount": 0,
  "paragraphCount": 72,
  "source": "tag:productivity/top-all-time",
  "rank": 50,
  "scrapedAt": "2026-10-13T10:37:17.858Z"
}
```

**Response (comment):**

```json
{
  "rowType": "response",
  "id": "ad76cb0bf1bf",
  "url": "https://medium.com/@libbyswinkler/there-are-good-recommendations-here-many-i-have-not-yet-consumed-ad76cb0bf1bf",
  "text": "There are good recommendations here—many I have not yet consumed. I am saving this as a reading list. Thanks!",
  "publishedAt": "2026-06-14T16:00:37.474Z",
  "updatedAt": "2026-06-14T16:00:37.474Z",
  "claps": 96,
  "voters": 37,
  "repliesCount": 1,
  "depth": 1,
  "parentPostId": "eed8d14f3bee",
  "rootPostId": "eed8d14f3bee",
  "rootPostUrl": "https://medium.com/the-useful-life/8-books-you-should-definitely-read-at-least-once-in-your-life-eed8d14f3bee",
  "authorId": "af03698341e7",
  "authorName": "Healthy Me Happy Me",
  "authorUsername": "libbyswinkler",
  "authorUrl": "https://medium.com/@libbyswinkler",
  "authorFollowers": 727,
  "source": "tag:productivity/top-all-time",
  "scrapedAt": "2026-10-13T10:37:18.225Z"
}
```

**Author profile** (`authors: ["@ev"]`):

```json
{
  "rowType": "author",
  "id": "268314bb7e7e",
  "name": "Ev Williams",
  "username": "ev",
  "url": "https://medium.com/@ev",
  "bio": "Curious human, cofounder @ Mozi, chairman @ Medium, partner @ Obvious Ventures",
  "followers": 213261,
  "following": 2699,
  "imageUrl": "https://miro.medium.com/v2/resize:fit:400/1*Oa7aZoW8TPZrQjlKYzH5dg.jpeg",
  "isMember": true,
  "memberSince": "2017-02-13T19:37:08.000Z",
  "membershipTier": "FRIEND",
  "isPartnerProgram": true,
  "isBookAuthor": false,
  "twitter": null,
  "customDomain": "ev.medium.com",
  "source": "author:ev",
  "scrapedAt": "2026-10-13T10:27:19.303Z"
}
```

**Publication profile** (`publications: ["levelup.gitconnected.com"]`):

```json
{
  "rowType": "publication",
  "id": "5517fd7b58a6",
  "name": "Level Up Coding",
  "slug": "gitconnected",
  "url": "https://levelup.gitconnected.com",
  "domain": "levelup.gitconnected.com",
  "description": "Coding tutorials and news. The developer homepage gitconnected.com && skilled.dev && levelup.dev",
  "followers": 365226,
  "imageUrl": "https://miro.medium.com/v2/resize:fit:400/1*5D9oYBd58pyjMkV_5-zXXQ.jpeg",
  "twitter": "gitconnected",
  "tags": "programming | artificial intelligence | software engineering",
  "creatorName": "Trey Huffine",
  "creatorUsername": "treyhuffine",
  "editors": "treyhuffine | warlyware",
  "editorsCount": 2,
  "source": "publication:levelup.gitconnected.com",
  "scrapedAt": "2026-10-13T10:27:18.690Z"
}
```

***

### How much data can I get?

Measured on the Apify platform on **2026-10-13**:

| Source | How deep it goes | Measured |
|---|---|---|
| Tag, `newest` | **The whole tag history.** Medium keeps a complete chronological archive per tag. | `rust-programming-language`: **4,840 posts — every story back to Nov 2018** (Medium's own count: 4,830) in 34 s. `programming`: **10,000 posts in 40 s** (the newest 15 days of a tag that gets ~330 posts a day). |
| Tag, ranked lists | ~100 posts per list (Medium's limit); `all-top` combines five lists. | `startup`, `all-top`: 287 unique top posts in 4.5 s. |
| Search, posts | ~1,000 results per query (Medium's limit). Add more queries for more. | "rust" + "product management": 1,000 posts in 6.1 s. |
| Search, people / tags | ~1,000 per query. | "data science" people: 300 writers in 4.8 s. |
| Search, publications | ~500 per query. | — |
| Author | Every story since the account started. | Ev Williams: all 121 stories back to Aug 2012 in 3.5 s; two authors, 1,000 rows in 9.7 s. |
| Publication | Every story in the publication. | Level Up Coding: 1,000 posts in 8.2 s. |
| Responses | All responses and replies of a story (`maxResponsesPerPost: 0`). | 100 top stories + **1,899 responses and replies** in 8.2 s. |

**Need tens of thousands of rows?** Add more tags (each tag's archive is complete and independent), widen the date window, combine tags with searches, authors and publications, and set `maxItems` to `0` or a large number. Duplicates across sources are removed automatically. A 10,000-row run is routine.

**Big runs and "Maximum cost per run".** Every pay-per-event run on Apify has a maximum cost — the one you set in the run options, or the platform's default. Medium Scraper reads it at the start and plans for exactly the number of results it covers. If you ask for 20,000 rows but the run's maximum cost covers 11,000, it saves 11,000, ends as **Succeeded** and says so in the status message (*"Saved 11000 results — this run's maximum cost ($…) covers 11000 results at the current price. Raise "Maximum cost per run" to get more."*). It never fails or gets aborted for this. **For 10,000+ rows, set "Maximum cost per run" to at least rows × price per result** (Pricing tab).

***

### Speed and resources per 1,000 rows

All measured on 2026-10-13, default settings:

| Run | Rows | Time | Peak memory |
|---|---|---|---|
| Prefilled example (tag, 100 rows) | 100 | **4.2 s** | 54 MB |
| Tag `artificial-intelligence`, newest | 1,000 | **8.7 s** | 60 MB |
| Tag `programming`, newest | 10,000 | **39.8 s** | 111 MB |
| Two tags with full article text | 10,000 | 103 s | 216 MB |
| Search, 2 queries | 1,000 | 6.1 s | 53 MB |
| Publication | 1,000 | 8.2 s | 60 MB |
| Responses + replies | 2,000 | 8.2 s | 60 MB |

- **Metadata-only runs:** about **4 s per 1,000 posts** at scale.
- **With full article text:** about **10 s per 1,000 posts**; the text comes in the same requests, so it costs time and memory, not extra requests.
- **Memory:** 1 GB default (it gives the run more CPU for article text); 512 MB is plenty for metadata-only runs.
- **No residential proxy needed.** No browser.
- **What you pay:** a fixed price per result — see the *Pricing* tab (on 2026-10-13: $1.50 per 1,000 results on the Free plan, down to $1.00 per 1,000 from the Gold plan up). Compute, storage and proxies are included; there is no separate proxy bill and no subscription.

***

### Integrations

#### Run it from the API (cURL)

```bash
curl -X POST "https://api.apify.com/v2/acts/logiover~medium-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tags":["artificial-intelligence"],"maxItems":100}'
```

Returns the rows as a JSON array. For long runs use the asynchronous `/runs` endpoint.

#### Node.js

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('logiover/medium-scraper').call({
    tags: ['startup'],
    includeContent: true,
    maxItems: 500,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((r) => r.rowType === 'post').map((r) => `${r.claps}\t${r.title}`).join('\n'));
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("logiover/medium-scraper").call(run_input={
    "authors": ["@ev"],
    "includeResponses": True,
    "maxItems": 1000,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["rowType"] == "post":
        print(row["publishedAt"], row["claps"], row["title"])
```

#### Make, Zapier, n8n

Use the Apify app in Make, Zapier or n8n ("Run an Actor" → `logiover/medium-scraper`, then "Get dataset items"), e.g. new stories in a tag → Slack; a competitor publication → Notion or Airtable; scheduled daily with `publishedAfter: "1 day"`.

#### Google Sheets

Export the dataset as CSV, or use Apify's Google Sheets integration to append every run's rows to a sheet automatically — ideal for a living content-research tracker.

#### Webhooks

Add a "Run succeeded" webhook and fetch `https://api.apify.com/v2/datasets/{datasetId}/items?format=csv` (or `json`, `xlsx`).

#### AI agents and MCP

Through Apify's MCP server, AI agents (Claude, ChatGPT, Cursor and other MCP clients) can call `logiover/medium-scraper` as a tool: "find the 50 most-clapped posts about RAG this month and summarise them". The Markdown output is ready for LLM context windows and RAG chunking.

***

### Medium Scraper vs other ways to get Medium data

| | **Medium Scraper** | Medium RSS feeds | Copy-paste by hand | Generic browser scrapers |
|---|---|---|---|---|
| Stories per tag | **Complete archive** (thousands) | Latest ~10 | As many as you can stand | Usually the first screen |
| Claps, clappers, responses count | **Yes** | No | Manually | Sometimes |
| Search (posts / people / publications / tags) | **Yes** | No | Yes, slowly | Rarely |
| Comments (responses + replies) | **Yes, full text** | No | Manually | Rarely |
| Author & publication stats (followers, bio, editors) | **Yes** | Partial | Manually | Sometimes |
| Full article text as Markdown | **Yes** (free stories complete) | Partial HTML | Yes | HTML only |
| Speed | **10,000 posts in ~40 s** | — | Hours | Seconds per page |
| Login / cookies needed | **No** | No | No | Often |
| Export | CSV, Excel, JSON, XML, Sheets, API | XML | — | Varies |

***

### Limits and tips

- **Member-only stories:** Medium shows signed-out readers only the beginning of a paywalled story, and that is what you get (`contentIsPreview: true`). Titles, claps, word count and all metadata are complete. Use `skipMemberOnly` when you need complete text only. This Actor does not bypass the paywall.
- **Ranked lists and search have Medium's own ceilings** (~100 per ranked list, ~1,000 per search query, ~500 per publication search). For more, use `tagSort: "newest"` (unlimited) or add queries.
- **Order:** chronological sources are numbered newest-first in `rank`; rows can land in the dataset slightly out of order because they are written in parallel — sort by `rank` or `publishedAt` if order matters.
- **Exact limits:** `maxItems` counts every row (posts, responses and profile rows). Turn off `includeProfiles` if you want posts only.
- **Fewer rows than `maxItems`** means the sources ran out (e.g. an author with 120 stories), your filters excluded the rest, or duplicates across sources were removed. The run's status message and SUMMARY say which.
- **Date filters are cheap on chronological sources** — the scraper starts and stops reading at your dates instead of filtering afterwards.

***

### FAQ

#### Do I need a Medium account, login or cookies?

No. The Actor only reads what Medium shows to signed-out visitors. You never share credentials.

#### Is there an official Medium API for this?

Medium's official API was built for publishing stories and does not offer tag archives, search, claps or responses. This Actor is the practical Medium API alternative for reading public data, with a stable JSON output.

#### How many posts can I scrape from one tag?

All of them. `tagSort: "newest"` walks the complete tag archive back in time — measured 4,840 of 4,830 counted posts for `rust-programming-language` (the whole history since 2018) and 10,000 posts of `programming` in 40 seconds.

#### Can I get the full text of member-only (paywalled) stories?

You get the public preview Medium shows to signed-out readers, flagged with `contentIsPreview: true`. Free stories come complete (`contentIsPreview: false`). The Actor does not bypass the paywall.

#### How do I scrape all stories of a Medium author?

Put their `@username` or profile URL into **Authors** and set `maxItems` to `0`. You get one profile row and every story, newest first. Custom subdomains like `name.medium.com` work too.

#### How do I scrape a Medium publication?

Put the slug (`swlh` for The Startup), URL or custom domain (`levelup.gitconnected.com`) into **Publications**. You get the publication's profile (followers, editors, description) and its stories, newest first.

#### Can I scrape Medium comments?

Yes — enable **Include responses**. Each response is a row with full text, claps, author and reply count; **Include replies** follows the threads below them. `maxResponsesPerPost` limits per story (0 = all).

#### How do I get only new stories since my last run?

Schedule the Actor with `publishedAfter: "1 day"` (or `"6 hours"`, `"1 week"`). Use `id` to de-duplicate across runs.

#### How do I export Medium data to CSV, Excel or Google Sheets?

Open the run's Dataset tab and choose CSV, Excel, JSON, XML or HTML — or use the Google Sheets integration or the API URL with `format=csv` / `format=xlsx`. Pick a view (Posts, Article text, Responses, Authors, Publications, Tags) to export only those columns.

#### Why did I get fewer rows than maxItems?

The sources were exhausted (a small tag, an author with few stories, a search limit), your filters removed stories, the same story came from two sources and was kept once, or the run's **Maximum cost per run** covered fewer results. The run is still *Succeeded*; its status message and SUMMARY (`stoppedBy`) explain.

#### My big run stopped with a "maximum cost" message — what do I do?

Pay-per-event runs have a maximum cost per run (yours or the platform default). The scraper stops cleanly at the number of results that budget covers instead of being aborted. Raise **Maximum cost per run** in the run options (or `maxTotalChargeUsd` in the API) to at least rows × price per result and run again.

#### What happens if a tag, author or publication does not exist?

The run finishes as *Succeeded* with a message such as `tag "xyz" does not exist on Medium`, so your automation does not break. Other valid sources in the same run are scraped normally.

#### Do I need a proxy?

No setup needed. By default the Actor connects directly (fastest) and switches to Apify Proxy automatically if Medium ever refuses direct requests. You can force a specific proxy group in the input.

#### What does `rank` mean?

For search and ranked lists it is Medium's position; for chronological sources (tag archive, author, publication) it numbers stories newest-first, so `rank: 1` is the newest story of that source.

#### Can I use the data to train AI models or build a RAG app?

The text of every story belongs to its author. Analysis, search indexes, summaries and internal research are common uses; republishing or training on copyrighted text may need permission depending on your jurisdiction and purpose. See the legality section below.

#### How fast is it really?

Measured 2026-10-13: 100 rows in 4 s, 1,000 posts in 6–10 s, 10,000 posts in 40 s, 10,000 posts with full article text in 103 s.

#### What if Medium changes something?

The Actor reads structured data from Medium's own web app endpoint, which changes far less often than page layouts. Runs report problems clearly instead of silently returning nothing, and issues are fixed fast — see support below.

***

### Is it legal to scrape Medium?

This Actor collects only **publicly available data** that Medium shows to any signed-out visitor; it does not log in, does not bypass the paywall and does not collect private information. Web scraping of public data is generally lawful, but you are responsible for how you use the results:

- **Personal data** (names, bios, social handles) may be protected by GDPR, CCPA and similar laws. Have a legitimate purpose and store only what you need.
- **Copyright:** stories and responses belong to their authors. Quote, analyse and link responsibly; do not republish full articles without permission.
- Respect Medium's terms and reasonable request volumes.

If in doubt, consult a lawyer about your specific use case.

***

### Changelog

- **2026-10-13 — first release.** Tag archive (complete history), ranked tag lists, search (posts / people / publications / tags), authors, publications, story URLs, full article text as Markdown + plain text, responses with reply threads, author / publication / tag profile rows, date / claps / language / paywall filters, parallel time-sliced reading (10,000 posts in 40 s), exact `maxItems`, honest run outcomes, automatic proxy fallback.

Full history in the Changelog tab.

***

### Support

Found a bug, need a field or a new source? **Open an issue on the Issues tab — we answer within 24 hours.** Please include the run ID so we can look at the exact run.

***

### Disclaimer

Medium Scraper is an independent tool and is **not affiliated with, endorsed by or sponsored by Medium** or A Medium Corporation. "Medium" is a trademark of A Medium Corporation and is used here only to describe the website this tool works with. Use the data in line with applicable laws and Medium's terms.

# Changelog

This Actor's version history is a separate document: https://apify.com/logiover/medium-scraper/changelog.md

# Actor input Schema

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

Medium tags as slugs, names or URLs: "artificial-intelligence", "Machine Learning", "https://medium.com/tag/python". The newest-first archive covers the tag's whole history, so one tag can give thousands of posts.

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

Which posts to read for each tag. "Newest first" walks the complete tag archive back in time; the ranked lists are Medium's own trending/top selections (about 100 posts each).

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

Medium search, one query per line, e.g. "rust async", "product-led growth". Up to ~1,000 results per query.

## `searchType` (type: `string`):

What the search queries look for. People, publications and tags come back as profile rows (followers, bio, description …).

## `authors` (type: `array`):

Writers as @username, username or profile URL (https://medium.com/@ev, https://name.medium.com). Returns the profile and every story, newest first.

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

Publications as slug, URL or custom domain: "better-programming", "https://medium.com/swlh", "levelup.gitconnected.com". Returns the publication and all its stories, newest first.

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

Any Medium URL — a story (also on custom domains), a tag page, a profile, a publication or a search page (medium.com/search?q=…). Each is detected automatically. Story IDs (12 hex characters) work too.

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

Stop after this many rows in total (posts + responses + profile rows). 0 = no limit.

## `maxPostsPerSource` (type: `integer`):

Cap for each single tag, author, publication or search query, so one big source cannot use up the whole run. 0 = no per-source cap.

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

Only posts published on or after this date: 2025-01-01, 2025-01-01T12:00:00Z, or relative like "30 days" / "6 months". Chronological sources stop reading at this date.

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

Only posts published before this date (same formats). Chronological sources start reading here, so a past date range is cheap.

## `minClaps` (type: `integer`):

Skip posts with fewer claps. 0 = keep all.

## `language` (type: `string`):

Keep only posts in these languages (ISO codes, comma-separated): "en" or "en, es, pt". Empty = all languages.

## `skipMemberOnly` (type: `boolean`):

Leave out stories behind the Medium paywall — useful when you need complete article text (e.g. for AI/RAG datasets).

## `includeContent` (type: `boolean`):

Add the article body as plain text and Markdown (headings, lists, code blocks, links, images). Free stories come complete; member-only stories return the public preview. Same number of requests — the text rides along with each page.

## `includeResponses` (type: `boolean`):

Add the responses of every collected post as separate rows (rowType = response) with full text, author, claps and reply count.

## `maxResponsesPerPost` (type: `integer`):

Responses (and replies) to collect per post, best first. 0 = all.

## `includeReplies` (type: `boolean`):

Also follow the reply threads under each response (depth 2, 3 …), within the per-post limit.

## `includeProfiles` (type: `boolean`):

One extra row for every tag, author and publication you list: followers, bio, description, post count, editors and more.

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

Requests in flight at once. The default is fast and safe; lower it only if you see many retries.

## `proxyConfiguration` (type: `object`):

Leave the default (Apify Proxy, no group selected) for the fastest runs: the Actor talks to Medium directly and switches to Apify Proxy by itself if Medium starts refusing. Pick a proxy group or your own proxies to send every request through them; "No proxy" means direct only.

## Actor input object example

```json
{
  "tags": [
    "artificial-intelligence"
  ],
  "tagSort": "newest",
  "searchQueries": [],
  "searchType": "posts",
  "authors": [],
  "publications": [],
  "startUrls": [],
  "maxItems": 100,
  "maxPostsPerSource": 0,
  "minClaps": 0,
  "skipMemberOnly": false,
  "includeContent": false,
  "includeResponses": false,
  "maxResponsesPerPost": 50,
  "includeReplies": false,
  "includeProfiles": true,
  "maxConcurrency": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

All rows collected in this run.

# 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 = {
    "tags": [
        "artificial-intelligence"
    ],
    "maxItems": 100,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("logiover/medium-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 = {
    "tags": ["artificial-intelligence"],
    "maxItems": 100,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("logiover/medium-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 '{
  "tags": [
    "artificial-intelligence"
  ],
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call logiover/medium-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,logiover/medium-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/Q6asva78iZT891tmp/builds/vN5pBM1ZY58AOpcin/openapi.json
