# Substack Scraper (`normdata/substack-scraper`) Actor

Scrape any Substack. Get full articles in Markdown, text or HTML, plus comments, aut
hor profiles, subscriber counts, pricing and category leaderboards, no login. Made for sales teams build
ing sponsor lists, analysts benchmarking newsletters, and AI teams gathering clean long form training da
ta.

- **URL**: https://apify.com/normdata/substack-scraper.md
- **Developed by:** [Norm Data](https://apify.com/normdata) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

![Norm Data](https://i.ibb.co/rGbhM5Y8/Chat-GPT-Image-Sep-8-2026-02-20-50-PM.png)

## 📰 Substack Scraper

Extract public Substack data (**posts, comments, publications, authors, and Leaderboard rankings**) as clean, structured records. Full article content in **HTML, Markdown, and plain text**, plus subscriber counts, structured pricing, posting cadence, and the recommendation graph between newsletters. No login, no Substack account, no browser extension, no manual copying.

Point it at newsletter URLs, discover them by keyword, or start from a category Leaderboard, and get export-ready data for lead generation, competitor research, content analysis, and AI training sets.

### 🎯 Who uses it?

#### 📇 Sales & partnership teams

Build lists of newsletters by category, size, and pricing, with author names and handles for sponsorship and partnership outreach.

#### 📈 Market & competitor researchers

Benchmark newsletters by subscriber count, paid/free mix, posting cadence, engagement, and price. Map which publications recommend each other.

#### 🧑‍💻 Content & SEO strategists

Track topics, headlines, and publishing frequency across a niche. Pull full archives to study what performs.

#### 🤖 AI / ML engineers

Collect high-quality long-form writing as Markdown or clean text, with metadata, for training and retrieval datasets.

#### 🗂️ Data journalists & analysts

Build exportable datasets of newsletter content, comments, and author networks.

### ✨ What it does

- **Posts:** full article body in Markdown, plain text, or HTML, with title, subtitle, tags, word count, reading time, reactions, restacks, cover image, podcast details, and outbound links.
- **Comments:** complete threads with nested replies, author info, and reaction counts. Optionally flatten to one row per comment.
- **Publications:** one row per newsletter, with subscriber count, structured pricing (monthly / annual / founding), language, first post date, podcast flag, posting cadence, paid ratio, and average reactions.
- **Leaderboard:** the top newsletters in any Substack category, ranked, with all publication fields. The fastest way to map a niche.
- **Author:** everything a writer runs from their `@handle`: bio, links, follower and subscriber counts, and every publication they operate.
- **Recommendations:** the newsletters each publication endorses, for network mapping.
- **Notes:** a publication's recent short-form Notes, when public.
- **Discovery:** don't know the URLs? Give keywords and the Actor finds matching newsletters for you.
- **Handles any size:** a single newsletter or an entire category, with automatic pagination.
- **Filters:** date range, content type (newsletter / podcast / thread), free-only, and incremental "only new since".
- **CRM-ready layout:** one row per post, per comment, or per publication.

### Why this scraper

- **Eight modes in one actor:** posts, comments, publications, leaderboard, author, recommendations, notes, and keyword discovery. The other Substack scrapers on Apify do posts and little else.
- **Leaderboard mode** ranks every newsletter in a category, so you can map a whole niche in one run.
- **Discovery by keyword** finds newsletters when you do not have the URLs.
- **Full article bodies in Markdown**, ready for an AI dataset or a content pipeline.
- **CRM-ready flattening:** one row per post, per comment, or per publication.
- **No login, no API key.**

### How it compares

| Capability | This actor | Other Substack scrapers on Apify |
|---|:--:|:--:|
| Posts with full body (Markdown / text / HTML) | yes | yes |
| Comment threads with nested replies | yes | some |
| Publication firmographics (subscribers, pricing, cadence) | yes | rare |
| **Leaderboard: rank a whole category** | **yes** | **no** |
| **Discovery: find newsletters by keyword** | **yes** | **no** |
| Author, recommendations, notes modes | yes | no |
| Incremental "only new since" runs | yes | rare |
| One row per post / comment / publication | yes | no |

### 📦 What data you get

| Entity | Useful fields |
| --- | --- |
| Post | Title, subtitle, URL, slug, type, audience, paid flag, tags, published/updated dates, word count, reading time, reactions, comment count, restacks, cover image, podcast URL & duration, outbound links, description, and body in Markdown / text / HTML. |
| Publication | Name, URL, subdomain, custom domain, author name & handle & bio, tagline, language, logo, subscriber count, structured pricing, first post date, podcast flag, posting cadence, paid ratio, average reactions. |
| Author | Handle, name, bio, photo, external links, follower count, subscriber count, bestseller tier, leaderboard ranking, and every publication they run. |
| Comment | Body, author name & handle & photo, date, edited date, reaction count, restacks, pinned flag, reply count, and nested replies. |
| Recommendation | Recommended publication name, URL, id, and the recommending note. |

Every record includes `scraped_at` (UTC) and `source`. Download your dataset from Apify as CSV, JSON, Excel, or XML.

### 💡 Use cases

#### 📇 Map a niche and its biggest newsletters

```json
{
  "mode": "leaderboard",
  "leaderboardCategory": "Technology",
  "leaderboardType": "paid",
  "maxItems": 100,
  "includeRecommendations": true
}
```

#### 📈 Benchmark competitor newsletters

```json
{
  "mode": "publications",
  "urls": [
    "https://www.lennysnewsletter.com",
    "https://newsletter.pragmaticengineer.com"
  ],
  "includeRecommendations": true
}
```

#### 🤖 Pull a full archive as Markdown for an AI dataset

```json
{
  "mode": "posts",
  "urls": ["https://www.astralcodexten.com"],
  "maxPostsPerNewsletter": 0,
  "contentFormat": "markdown",
  "onlyFree": true
}
```

#### 🧑‍💼 One row per comment for analysis

```json
{
  "mode": "posts",
  "urls": ["https://www.lennysnewsletter.com"],
  "maxPostsPerNewsletter": 20,
  "includeComments": true,
  "outputMode": "comment"
}
```

#### 🔎 Discover newsletters by topic

```json
{
  "mode": "posts",
  "keywords": ["artificial intelligence", "climate"],
  "maxNewslettersPerKeyword": 15,
  "maxPostsPerNewsletter": 5
}
```

### ⚙️ How the input is organised

The input form is split into six numbered sections:

| Section | What it's for |
| --- | --- |
| **1 · What do you want to scrape?** | Pick one **type of data**: Articles, Newsletter profiles, Top newsletters (Leaderboard), Writer profile, or Notes. |
| **2 · Where to get it from** | Fill in **only** the field that matches your choice: newsletter **links** (Articles / profiles / Notes), **topics** to search (Articles), **writer handles** (Writer profile), or a **category** (Leaderboard). The others can stay empty. |
| **3 · How much to collect** | **Maximum rows** for the whole run, **articles per newsletter** (`0` = full archive, paid plans), and how many **newsletters to find per topic**. |
| **4 · What to include in each article** | Full text (and its format: Markdown / plain / HTML / all), newsletter stats, recommended newsletters, reader comments, notes. |
| **5 · Filters (optional)** | Only regular posts / podcasts / threads, a published-date range, free-only, and "only newer than" for scheduled runs. Leave blank to get everything. |
| **6 · Output format** | One row per article (comments nested), or one row per comment (flat, spreadsheet-ready). |

Pagination, retries, deduplication, and parallelism are handled automatically. There is no "concurrency" knob to set.

> **Apify Free plan:** every run is limited to a fixed 10-row sample. Upgrade your Apify plan to run your own settings.

### 🛡️ Limits & responsible use

This Actor collects only publicly available information from Substack. It never signs in and never accesses private, paid-only, or subscriber-gated content. For paywalled posts you get the public metadata (title, word count, reactions) but not the body text.

Author and comment data are personal data. Use them in line with Substack's Terms of Service and applicable law (GDPR / CCPA). Prefer publication-level and business use; do not use the output for spam.

If a newsletter's page can't be loaded, its row is still saved with whatever was available plus an `error` field.

### 🧪 Local development

```powershell
bun install
bun test
bun run typecheck
apify run
```

Local results are stored in `storage/datasets/default`.

# Actor input Schema

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

**Articles** — the posts from one or more newsletters (choose them by link, or find them by topic).
**Newsletter profiles** — one row per newsletter: subscribers, subscription price, how often it posts, who writes it.
**Top newsletters (Leaderboard)** — the biggest newsletters in a category. Great for exploring a niche.
**Writer profile** — everything about a person from their @handle: bio, links, follower count, and every newsletter they run.
**Notes** — a newsletter's short posts (the Notes feed).

*On the Apify **Free** plan every run is limited to a fixed 10‑row sample; upgrade to run your own settings.*

## `urls` (type: `array`):

Paste one or more newsletter addresses, e.g. `https://www.lennysnewsletter.com` or `https://mynewsletter.substack.com`. A link to a single article works too.

## `keywords` (type: `array`):

No links? Type one or more topics and the scraper pulls the top newsletters in that area from Substack's own category rankings — e.g. `artificial intelligence`, `crypto`, `investing`, `climate`, `marketing`, `politics`, `health`. Newsletters that mention your exact words are listed first. Can be combined with the links above.

## `authorHandles` (type: `array`):

The person's Substack handle, with or without the `@` (e.g. `lenny`). You get their bio, links, follower and subscriber counts, and every newsletter they run.

## `leaderboardCategory` (type: `string`):

One category name. Examples: Technology, Business, Finance, Culture, U.S. Politics, News, Health & Wellness, Faith & Spirituality, Food & Drink, Sports, Music, Fiction, Art & Illustration, Climate & Environment, Education, Fashion & Beauty, Travel, Parenting, Humor, Crypto, Science, Literature.

## `leaderboardType` (type: `string`):

Rank the newsletters by their number of paying subscribers, or by their whole audience (free readers included).

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

Stop once this many rows are in the dataset. Leave empty to get everything. *(Free plan: always capped at 10.)*

## `maxPostsPerNewsletter` (type: `integer`):

Newest first. Use `0` for a newsletter's entire archive (paid Apify plans).

## `maxNewslettersPerKeyword` (type: `integer`):

When you search by topic, how many newsletters to take for each topic you typed.

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

Include the whole article body. Turn this off for a quick, cheap run with just the headline, author and stats.

## `contentFormat` (type: `string`):

**Markdown** — clean and easy to reuse (recommended). **Plain text** — no formatting. **HTML** — the original web markup. **Everything** — all three.

## `includePublicationInfo` (type: `boolean`):

Add the newsletter's subscriber count, subscription price (monthly / yearly / founding) and author details to every article row.

## `includeRecommendations` (type: `boolean`):

The other newsletters this one recommends — handy for mapping out a niche.

## `includeComments` (type: `boolean`):

Include the comment threads under each article, with replies, authors and likes. Slower.

## `maxCommentsPerPost` (type: `integer`):

Cap the number of comments (replies included) collected per article. `0` means no comments.

## `includeNotes` (type: `boolean`):

Attach the newsletter's recent Notes when they're public.

## `contentType` (type: `string`):

Keep only regular posts, podcast episodes, or discussion threads.

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

Only articles from this date onward. Format: `YYYY-MM-DD` (e.g. `2025-01-01`).

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

Only articles up to this date. Format: `YYYY-MM-DD`.

## `onlyFree` (type: `boolean`):

Only include articles that are free to read in full.

## `onlyNewerThan` (type: `string`):

For a scheduled run: only pick up articles published after this date, so each run returns just what's new. Format: `YYYY-MM-DD`.

## `outputMode` (type: `string`):

**One row per article** — comments stay nested inside the article row.
**One row per comment** — every comment gets its own row with the article details repeated on it, ready for a spreadsheet or CRM.

## Actor input object example

```json
{
  "mode": "posts",
  "urls": [
    "https://www.lennysnewsletter.com",
    "https://newsletter.pragmaticengineer.com"
  ],
  "keywords": [
    "artificial intelligence",
    "crypto"
  ],
  "authorHandles": [
    "lenny",
    "packym"
  ],
  "leaderboardCategory": "Technology",
  "leaderboardType": "paid",
  "maxItems": 10,
  "maxPostsPerNewsletter": 10,
  "maxNewslettersPerKeyword": 10,
  "includeContent": true,
  "contentFormat": "markdown",
  "includePublicationInfo": true,
  "includeRecommendations": true,
  "maxCommentsPerPost": 50,
  "contentType": "all",
  "outputMode": "post"
}
```

# Actor output Schema

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

One row per post, comment, publication, or author depending on the selected mode and row layout.

# 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 = {
    "mode": "posts",
    "urls": [
        "https://www.astralcodexten.com"
    ],
    "keywords": [],
    "authorHandles": [],
    "leaderboardCategory": "Technology",
    "leaderboardType": "paid",
    "maxItems": 10,
    "maxPostsPerNewsletter": 10,
    "maxNewslettersPerKeyword": 10,
    "includeContent": true,
    "contentFormat": "markdown",
    "includePublicationInfo": true,
    "includeRecommendations": true,
    "includeComments": false,
    "maxCommentsPerPost": 50,
    "includeNotes": false,
    "contentType": "all",
    "onlyFree": false,
    "outputMode": "post"
};

// Run the Actor and wait for it to finish
const run = await client.actor("normdata/substack-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 = {
    "mode": "posts",
    "urls": ["https://www.astralcodexten.com"],
    "keywords": [],
    "authorHandles": [],
    "leaderboardCategory": "Technology",
    "leaderboardType": "paid",
    "maxItems": 10,
    "maxPostsPerNewsletter": 10,
    "maxNewslettersPerKeyword": 10,
    "includeContent": True,
    "contentFormat": "markdown",
    "includePublicationInfo": True,
    "includeRecommendations": True,
    "includeComments": False,
    "maxCommentsPerPost": 50,
    "includeNotes": False,
    "contentType": "all",
    "onlyFree": False,
    "outputMode": "post",
}

# Run the Actor and wait for it to finish
run = client.actor("normdata/substack-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 '{
  "mode": "posts",
  "urls": [
    "https://www.astralcodexten.com"
  ],
  "keywords": [],
  "authorHandles": [],
  "leaderboardCategory": "Technology",
  "leaderboardType": "paid",
  "maxItems": 10,
  "maxPostsPerNewsletter": 10,
  "maxNewslettersPerKeyword": 10,
  "includeContent": true,
  "contentFormat": "markdown",
  "includePublicationInfo": true,
  "includeRecommendations": true,
  "includeComments": false,
  "maxCommentsPerPost": 50,
  "includeNotes": false,
  "contentType": "all",
  "onlyFree": false,
  "outputMode": "post"
}' |
apify call normdata/substack-scraper --silent --output-dataset

```

## MCP server setup

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