# Medium Posts Scraper (`leorochasantos/medium-posts`) Actor

A scraper for public Medium story feeds: the newest posts for any tag, author or publication, with title, canonical URL, author, tags, publish time, teaser and full public body text. No login, no cookies, no challenge bypass.

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

## Pricing

from $6.50 / 1,000 medium story rows

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 Posts Scraper

Scrape public Medium story feeds: the newest posts for any tag, author handle or
publication, or resolve specific public post URLs into the same typed rows. Every
row is flat, typed and null-safe for SEO research, content monitoring,
spreadsheets and pipelines.

### Why this actor?

- Two buyer-shaped modes: `search` (list the newest stories of tag, author and
  publication feeds) and `detail` (resolve exact post URLs).
- One flat row per story: title, canonical URL, author, tags, publish time,
  teaser and — where the feed carries it — the full public body text.
- Hard local bounds on sources and rows keep delivery and spend predictable.
- Invalid or unknown tags/authors/publications, empty feeds, unresolvable post
  URLs and blocked fetches are free error rows.
- Public feed surface only: no login, no cookies, no challenge bypass, no media
  download.

### Input

```json
{"mode":"search","tags":["web-scraping"],"maxItemsPerSource":10}
```

For detail mode, set `mode` to `detail` and provide `postUrls`.

| Input | Meaning |
|---|---|
| `mode` | `search` (list feeds) or `detail` (resolve exact post URLs). |
| `tags` | Medium tag slugs, e.g. `web-scraping`; or `medium.com/tag/…` URLs. |
| `authors` | Medium author handles, e.g. `@tripathi.ha`; or `medium.com/@handle` URLs. |
| `publications` | Medium publication slugs, e.g. `medium-engineering`. |
| `postUrls` | `detail` mode: public post URLs, deduplicated and capped at 50. |
| `maxItemsPerSource` | Hard local cap of rows per source (1–10). |
| `includeBodyHtml` | When true, rows carry the feed's full public story HTML. |
| `proxyConfiguration` | Proxy for Medium feed requests; datacenter is the default. |

Up to 20 sources per run across `tags` + `authors` + `publications`.

### Output

```json
{"mode":"search","input":"web-scraping","source_type":"tag","source":"web-scraping","post_id":"2a038f5ae896","url":"https://medium.com/@tripathi.ha/build-a-multi-site-competitor-price-monitor-in-10-minutes-no-selectors-no-selenium-2a038f5ae896","title":"Build a Multi-Site Competitor Price Monitor in 10 Minutes (No Selectors, No Selenium)","author":"Tripathi Harsh","author_handle":"tripathi.ha","tags":["ai-scraping","web-scraping","python","llm","ai"],"published_at":"2026-09-18T08:49:53.000Z","updated_at":"2026-09-18T08:49:53.274Z","snippet":"How to automate e-commerce price intelligence, Slack webhook alerts, and zero-cost GitHub Actions scheduling.","body_html":null,"body_text":null,"word_count":null,"image_url":null,"error":null,"scraped_at":"2026-09-18T10:12:41.000Z"}
```

| Field | Meaning |
|---|---|
| `mode` | Which mode produced the row: `search` or `detail`. |
| `input` | The source exactly as provided (tag, handle, publication, or post URL). |
| `source_type` | `tag`, `author` or `publication` in `search` mode; `post` in `detail` mode. |
| `source` | Normalized source key (tag slug, handle, publication slug); null when the input could not be normalized. |
| `post_id` | Medium post id — the 12-hex tail of the story URL. |
| `url` | Canonical public story URL, query string stripped. |
| `title` | Story title. |
| `author` | Display name of the story's author. |
| `author_handle` | `@handle` when the story link carries one; null for publication paths. |
| `tags` | Story tags from the feed; always an array, possibly empty. |
| `published_at` | Publish time, ISO 8601 (feed pubDate, falling back to atom:updated). |
| `updated_at` | Feed update time, ISO 8601. |
| `snippet` | Teaser text only the tag feeds carry; null on author/publication feeds. |
| `body_html` | Full public story HTML when the source feed carries it and `includeBodyHtml` is not false. |
| `body_text` | Plain text derived from the full public story HTML; null when the feed carries no body. |
| `word_count` | Word count of `body_text`. |
| `image_url` | First image referenced by the feed item, if any. |
| `error` | Free per-input error message; null on story rows. |
| `scraped_at` | Extraction timestamp. |

### Pricing

Pay per event, and **the platform usage is on us** — the price you see is the price you pay, with no compute bill on top.

| Event | Price | What one charge buys |
|---|---|---|
| Medium story row | **$0.01** | One delivered public Medium story row from a public feed: title, canonical URL, author, tags, publish time, teaser and — on author/publication feeds — the full public body text. An invalid or unknown tag/author/publication, a feed with no stories, an unresolvable post URL, and any blocked, 5xx or timeout fetch are free error rows and are never charged. |
| Actor Start | **$0.002** | One run, whatever it returns. |

Higher Apify subscription tiers pay less on every event (Silver −20%, Gold −35%).

### Proxy

Requests use the Apify proxy configured in `proxyConfiguration`. The validated
default is datacenter. This actor does not bypass challenges, log in, or read
private pages. Per-run residential bandwidth (only relevant if a run explicitly
selects the residential tier) is capped by `proxyGbytesCap` in
`.actor/publish.json`.

### Modes and limits

- **search** — one feed fetch per source (`medium.com/feed/tag/<tag>`,
  `medium.com/feed/@<handle>`, `medium.com/feed/<publication>`), newest first.
  A public Medium feed exposes at most 10 items and has no page parameter, so
  `maxItemsPerSource` (1–10) is the whole pagination surface; rows are
  deduplicated across sources so a story appearing in a tag feed and its
  author's feed is never charged twice.
- **detail** — each public post URL is resolved through its own author or
  publication feed, so a post resolves while it is among that feed's newest 10
  public items. `/p/<id>`-style links that do not name their source are free
  error rows.

### Use cases

- Watch what a tag publishes without opening Medium.
- Track a set of authors or publications and pull their newest stories.
- Build a dated story index (title, URL, author, tags, publish time) for SEO work.
- Pull the public full text of the newest stories of an author or publication.
- Resolve specific story URLs into typed rows for a spreadsheet or pipeline.

### FAQ

**Does this need a Medium account?** No. Only the public feed documents are read.

**Why is `snippet` null on author feeds?** Medium's author/publication feeds
carry the full body in `content:encoded` instead of a teaser; tag feeds are the
opposite. The keys are always present.

**Why is `body_text` null on some rows?** Medium omits the body for some feed
items (cross-posted or link stories); the row carries everything else, and the
keys stay null-safe.

**Does a `detail` lookup work for any old story?** No — a public feed lists only
its newest 10 items, so older stories resolve as a free "not found in the latest
items" error row.

***

*This Actor is an independent tool and is not affiliated with Medium.*

# Actor input Schema

## `mode` (type: `string`):

search: list the newest public stories from tag, author and publication feeds. detail: resolve specific public post URLs through their author/publication feed.

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

search mode: Medium tag slugs (e.g. web-scraping, machine-learning) or medium.com/tag/… URLs. Up to 20 sources per run across tags, authors and publications.

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

search mode: Medium author handles (e.g. @tripathi.ha) or medium.com/@handle URLs; each author's public story feed.

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

search mode: Medium publication slugs (e.g. medium-engineering) or medium.com/<publication> URLs.

## `postUrls` (type: `array`):

detail mode: public Medium post URLs (medium.com/@handle/<slug> or medium.com/<publication>/<slug>), deduplicated and capped at 50. A post resolves when it is among its feed's newest 10 public items.

## `maxItemsPerSource` (type: `integer`):

Hard local limit on rows delivered per source (1-10; a public Medium feed exposes at most 10 items).

## `includeBodyHtml` (type: `boolean`):

When true, author and publication rows carry the feed's full public story HTML in body\_html (body\_text is always derived).

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

Proxy for Medium feed requests. Datacenter is the validated default.

## Actor input object example

```json
{
  "mode": "search",
  "tags": [
    "web-scraping"
  ],
  "authors": [
    "tripathi.ha"
  ],
  "publications": [
    "medium-engineering"
  ],
  "postUrls": [
    "https://medium.com/@bhorowitz/andrew-chen-86abc917b807"
  ],
  "maxItemsPerSource": 10,
  "includeBodyHtml": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

# 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": [
        "web-scraping"
    ],
    "authors": [
        "tripathi.ha"
    ],
    "publications": [
        "medium-engineering"
    ],
    "postUrls": [
        "https://medium.com/@bhorowitz/andrew-chen-86abc917b807"
    ],
    "includeBodyHtml": true,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("leorochasantos/medium-posts").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": ["web-scraping"],
    "authors": ["tripathi.ha"],
    "publications": ["medium-engineering"],
    "postUrls": ["https://medium.com/@bhorowitz/andrew-chen-86abc917b807"],
    "includeBodyHtml": True,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("leorochasantos/medium-posts").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": [
    "web-scraping"
  ],
  "authors": [
    "tripathi.ha"
  ],
  "publications": [
    "medium-engineering"
  ],
  "postUrls": [
    "https://medium.com/@bhorowitz/andrew-chen-86abc917b807"
  ],
  "includeBodyHtml": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call leorochasantos/medium-posts --silent --output-dataset

```

## MCP server setup

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

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/8ofqpaaxdOhxec839/builds/j033UVoOB5qfWJoEk/openapi.json
