# Substack Scraper: Posts, Newsletters & Archive (`automatic/substack-scraper`) Actor

Scrape any Substack publication, including custom domains: post titles, dates, free or paid status, word counts, likes, comments and public bylines, plus the text of free posts. Up to 20 publications per run. Export JSON, CSV or Excel. $1 per 1,000 posts.

- **URL**: https://apify.com/automatic/substack-scraper.md
- **Developed by:** [Philip Kirkbride](https://apify.com/automatic) (community)
- **Categories:** News
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 posts

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

## Substack Publication Scraper

Export posts from Substack publications, including publications on their own domains: titles, publication dates, free or paid status, word counts, likes and comment counts, public bylines and, if you want, the text of free posts.

**Price:** $0.005 per run start plus **$0.001 per delivered post**. The Console example (20 posts from one publication) costs about **$0.025**; the same run with 50 posts costs about $0.055. Post text is included in the per-post price.

No login, no subscription and no API key needed.

### Who it is for

- **Newsletter writers and media teams** comparing engagement across publications: which posts get likes, comments and replies.
- **Researchers and analysts** tracking what a set of newsletters publishes, by date and word count.
- **Content and SEO teams** building a calendar of a niche's output, or checking how often competitors post.
- **Anyone who wants a clean spreadsheet** of a publication's archive instead of scrolling a website.

### How to use it

1. Paste one or more **publication URLs**, such as `https://www.oneusefulthing.org` or `https://name.substack.com`. Custom domains work.
2. Choose **newest first** or **top posts**.
3. Set **maximum posts per publication** (default 50) and turn on **free-post text** if you need the words.
4. Run it and export the dataset as JSON, CSV or Excel.

An empty form is safe: it collects a small example (10 posts from one publication) and says so in the run summary.

### Input

- **Publication URLs**: up to 20 publication home or archive URLs, such as `https://name.substack.com` or a custom domain like `https://www.oneusefulthing.org`.
- **Post order**: newest first (default) or top posts.
- **Maximum posts per publication**: 1 to 5,000, default 50.
- **Include free-post text**: off by default. Turn it on to add the text of free posts.

```json
{
  "publicationUrls": ["https://www.oneusefulthing.org"],
  "sort": "new",
  "maxItems": 50,
  "includeContent": false
}
```

Several publications in one run:

```json
{
  "publicationUrls": ["https://www.oneusefulthing.org", "https://www.platformer.news"],
  "sort": "new",
  "maxItems": 100,
  "includeContent": false
}
```

### Output

One row per post:

| Field | Meaning |
|---|---|
| `postId` | Substack post ID |
| `publication` | Publication name |
| `title`, `subtitle` | Post title and subtitle |
| `slug`, `url` | Post slug and link |
| `postedAt` | Publication date and time (UTC) |
| `audience` | `free` or `paid` |
| `wordcount` | Word count |
| `reactionCount`, `commentCount` | Likes and comments |
| `authorDisplayName` | Public byline shown on the post |
| `bodyText` | Post text (free posts, when text is turned on) |

### Output example

```json
{
  "postId": "12345",
  "publication": "Example Newsletter",
  "title": "A practical guide to evaluating new ideas",
  "subtitle": null,
  "slug": "a-practical-guide",
  "url": "https://example.substack.com/p/a-practical-guide",
  "postedAt": "2026-10-20T15:00:00Z",
  "audience": "free",
  "wordcount": 1200,
  "reactionCount": 42,
  "commentCount": 3,
  "authorDisplayName": "Example Editorial Team",
  "bodyText": null
}
```

Download results as JSON, CSV or Excel.

### Cost examples

| Run | Posts | Approximate cost |
|---|---:|---:|
| Empty input (10-post example) | 10 | $0.015 |
| Console example, one publication | 20 | $0.025 |
| One publication, 50 posts | 50 | $0.055 |
| Five publications, 100 posts each | 500 | $0.505 |
| One publication, full archive of 1,000 posts with text | 1,000 | $1.005 |

You pay per delivered post, not per page or per request, so a smaller **maximum posts per publication** costs less.

### What can be empty, and why

- **Paid posts never include text.** You still get their title, date and counts.
- **Some free posts have no text.** Text is left empty when Substack shows only part of a post (the rest is locked or must be unlocked). A free post that simply mentions subscribing or upgrading still includes its text. Text that is returned is the post as published, links, names and contact details included.
- **`publication` can be empty** on guest posts whose author does not belong to the publication.
- Any other value the publication does not show is empty.

### Limits

- Works with Substack publications and custom-domain Substack sites. Profile links such as `https://substack.com/@name` are not supported yet; use the publication's own URL. A profile link is skipped and does not fail the run.
- Single post links, keyword search and date filters are not supported.
- Up to 5,000 posts per publication. A run with the default settings comfortably covers a few thousand posts without text; with text turned on, plan for up to about 1,000 posts per run, because text is collected post by post and is slower.
- A publication that cannot be reached or has no posts is noted in the run summary; posts from the other publications are still delivered.

### FAQ

**How is it charged?** $0.005 to start and $0.001 per delivered post, including the post text when you turn it on. A publication with no posts costs only the start charge.

**Can I get the full text of paid posts?** No. Only publicly free post text is collected; paid and truncated posts keep their metadata.

**Why did I get fewer posts than I asked for?** The publication either has fewer posts than your limit or shows only part of its archive. The run summary lists each publication with the number of posts it delivered.

**Can I track only new posts over time?** Save the input and schedule the task (daily or weekly). Each run is independent, so compare `postId` values with the previous run or keep your own baseline.

**Do you support notes, comments or subscriber counts?** Not in this Actor. It returns posts, their counts and the public byline.

**Is the byline personal data?** `authorDisplayName` is the public byline shown on the post, as published, and free-post text may contain names, emails, links or addresses supplied by the author.

### Privacy

The actor returns only public post information and the public byline shown on each post. It does not collect commenter, subscriber or profile data. Titles, subtitles, bylines, publication names and free-post text are delivered as published.

### Pricing

See the Pricing tab. The start charge is counted per GB of memory; the default 512 MB run is one start event.

# Actor input Schema

## `publicationUrls` (type: `array`):

HTTPS publication home or archive URLs, including custom domains. Up to 20. Profile URLs (substack.com/@handle) are not supported yet; use the publication URL. Unsupported profiles receive a status and do not fail the run. Leave empty to get a small example from one publication.

## `sort` (type: `string`):

Optional: choose new for newest posts first or top for top posts. If omitted, posts are ordered newest first.

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

Optional: enter the maximum number of posts to collect from each publication, from 1 to 5,000 (for example, 100). If omitted, collects up to 50 posts per publication.

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

Include the post text, as published, for publicly free posts only. Paid, truncated or access-restricted content remains empty.

## Actor input object example

```json
{
  "publicationUrls": [
    "https://www.oneusefulthing.org"
  ],
  "sort": "new",
  "maxItems": 20,
  "includeContent": false
}
```

# 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 = {
    "publicationUrls": [
        "https://www.oneusefulthing.org"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automatic/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 = {
    "publicationUrls": ["https://www.oneusefulthing.org"],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("automatic/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 '{
  "publicationUrls": [
    "https://www.oneusefulthing.org"
  ],
  "maxItems": 20
}' |
apify call automatic/substack-scraper --silent --output-dataset

```

## MCP server setup

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