# Substack Comments Scraper (`deriverge/substack-comments-scraper`) Actor

Public comments and replies under Substack posts with the commenter's name and handle, date, likes, reply count and thread depth. Give post links or newsletter names, and schedule the new-only mode to get only the comments added since the last run.

- **URL**: https://apify.com/deriverge/substack-comments-scraper.md
- **Developed by:** [deriverge s.r.o.](https://apify.com/deriverge) (community)
- **Categories:** News, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.15 / 1,000 comments

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

## Substack Comments Scraper

Substack Comments Scraper returns the public comments under Substack posts, one row per comment or reply: the commenter's display name and handle, the time, the text, likes, the number of replies and the comment's place in the thread. Paste post links into **Post links**, or name newsletters to read the comments under their newest posts, and click **Start**. Substack shows comments under paid posts to subscribers only, so such posts return no rows. It costs $0.30 per 1,000 comments on the Free plan ($0.15 on Business), and the $5 of monthly credit in Apify's Free plan covers about 16,000 comments.

### What data does it return?

| Field | Description |
|---|---|
| `id`, `key` | Substack's numeric ID of the comment, and `comment:<id>`, which the new-only mode compares between runs. |
| `postId`, `postUrl`, `postTitle` | The post the comment is under: its ID, its link on the newsletter's own address and its title. |
| `newsletterSubdomain` | The newsletter's `substack.com` subdomain, such as `garymarcus`, also for newsletters on their own domain: comments under Noahpinion posts on `www.noahpinion.blog` give `noahpinion`. |
| `parentId`, `depth` | The comment this one answers, `null` for a comment on the post itself, and the level in the thread: `0` at the top, `1` for a reply, `2` for a reply to a reply. |
| `author`, `authorHandle` | The display name and Substack handle shown next to the comment, such as `Gary Marcus` and `garymarcus`. |
| `publishedAt` | When the comment was posted, as an ISO 8601 UTC timestamp. |
| `body` | The comment text with its line breaks. |
| `likes` | Likes on the comment at the time of the run. |
| `replies` | How many direct replies the comment has. Replies to those replies are counted on the comment they answer. |
| `edited` | `true` when the commenter changed the comment after posting it. |
| `type` | The row type, always `comment` in this actor. |

### How to scrape Substack comments

1. In **Post links**, add one link per line in the form `https://<newsletter address>/p/<slug>`. Posts on a `substack.com` subdomain and on a custom domain such as `https://www.noahpinion.blog` both work.
2. To follow whole newsletters, add them to **Newsletters** as a subdomain (`lenny`), a `substack.com` address or a custom domain. The actor reads the comments under each newsletter's newest posts, as many as **Maximum posts per newsletter** allows (5 by default). Both fields can be used in one run.
3. Pick the **Comment order**: Best first, Newest first or Oldest first. Rows come in thread order, each top-level comment followed by its replies, and **Maximum comments per post** (500 by default) counts replies too, so a capped post can end in the middle of a thread.
4. Set **Maximum rows** and click **Start**. Comments appear in the **Output** tab, where you can download them as CSV, Excel or JSON. The `SUMMARY` record in the key-value store counts posts and comments and lists every link that was skipped.

As an API input:

```json
{
  "posts": ["https://garymarcus.substack.com/p/we-must-recall-open-ended-ai-agents"],
  "newsletters": ["lenny", "https://www.noahpinion.blog"],
  "maxPostsPerNewsletter": 10,
  "maxCommentsPerPost": 1000,
  "commentSort": "newest_first"
}
```

### Example output

A reply by the newsletter's author under one of his posts, from a run on 11 October 2026. Comments by readers have the same fields, with the name and handle they comment under.

```json
{
  "type": "comment",
  "key": "comment:357345097",
  "id": 357345097,
  "postId": 219679899,
  "postUrl": "https://garymarcus.substack.com/p/we-must-recall-open-ended-ai-agents",
  "postTitle": "We must recall open-ended AI agents with internet access from the market, now",
  "newsletterSubdomain": "garymarcus",
  "parentId": 357326579,
  "depth": 1,
  "author": "Gary Marcus",
  "authorHandle": "garymarcus",
  "publishedAt": "2026-10-10T07:19:34.804Z",
  "body": "I don’t think the AI itself has motivation.",
  "likes": 7,
  "replies": 1,
  "edited": false
}
```

### How much does it cost to scrape Substack comments?

| | Free plan | Starter | Scale | Business |
|---|---|---|---|---|
| 1,000 comments | $0.30 | $0.24 | $0.195 | $0.15 |

You pay only for the events in the table. There is no start fee, and compute time and proxies are included.

The 15 newest posts of Lenny's Newsletter, Noahpinion and Gary Marcus's newsletter held 792 public comments on 11 October 2026, which cost $0.2376 on the Free plan and $0.1188 on Business. Posts without public comments add nothing to the bill, and comments the new-only mode skips are not charged.

### Limits

- Substack serves each post's whole discussion in one response, so runs are short: the 792 comments above took under 4 seconds, and a single post with 76 comments about 3 seconds.
- Comments under paid posts are hidden from visitors without a subscription. Four of the five newest posts in Lenny's Newsletter were for paid subscribers on 11 October 2026, and they returned no comments although they showed 1 to 21 each.
- Deleted comments and comments Substack has not published are left out, so a post can show one or two more than the actor returns: a Gary Marcus post that showed 130 comments gave 129 rows.
- Threads can run deep. A Noahpinion post on utilitarianism had a reply at `depth` 10.
- With **Newsletters**, the newest posts are read first and there is no date filter. To reach older posts, raise **Maximum posts per newsletter**.
- A link without `/p/` is skipped and named in `SUMMARY`. Substack Notes and chats are not read.
- A run stops after 10,000 rows unless you raise **Maximum rows**.

### Alerts for new comments

Turn on **Return only rows new since the last run**, give the run a **Watch name** or save it as a task, and schedule it. Each run returns only comments whose `id` the previous run under that name had not seen, and comments that did not fit under **Maximum rows** come with the next run. With **Newsletters** filled, a daily schedule also picks up the comments under posts published since the day before. Choose **Newest first** so that fresh comments fall inside **Maximum comments per post**.

### FAQ

#### Is it legal to scrape Substack comments?

The actor reads the comments that Substack shows to any visitor under free posts, without logging in. A display name and handle can identify a person, so data protection laws such as the GDPR may apply to how you store and use them. The actor collects nothing else about commenters: no emails, profiles or subscriptions. You are responsible for how you use the data.

#### Can I rebuild the comment threads?

Yes. Each reply carries the `id` of the comment it answers in `parentId`, and the rows of a post come in the order Substack nests them. Start from the rows where `parentId` is `null` and attach the replies below them, or use `depth` to indent them in a spreadsheet.

#### Does it return the posts too?

No, only comments, with each post's ID, link and title on every row. For the posts with their text, likes and restacks, use our Substack Scraper, which can also add comments to the posts of the newsletters you name.

### Related scrapers

- [Substack Scraper](https://apify.com/deriverge/substack-scraper)
- [Substack Leaderboard Scraper](https://apify.com/deriverge/substack-newsletter-directory)
- [Hacker News Comments Scraper](https://apify.com/deriverge/hacker-news-comments-scraper)
- [Article Extractor](https://apify.com/deriverge/article-extractor)

### Support

This actor is built and maintained by deriverge s.r.o., a software company based in the Czech Republic. If a run fails or a field you need is missing, please open an issue in the **Issues** tab or write to us at info@deriverge.com. We respond in English and Czech. Runs can be scheduled in Apify Console or started from the **API** tab, which has examples for Python, JavaScript and cURL and works with Make, Zapier, n8n and the Apify MCP server. If the actor saves you time, a short review helps other people find it.

# Changelog

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

# Actor input Schema

## `posts` (type: `array`):

Links to Substack posts, on substack.com or on a custom domain.

## `newsletters` (type: `array`):

Alternatively, newsletters whose latest posts to read the comments of.

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

Newest first. Archives go back years, so this is what keeps a broad run affordable.

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

Replies count towards the cap.

## `commentSort` (type: `string`):

The order Substack returns them in; the cap applies after ordering.

## `newOnly` (type: `boolean`):

Keeps a snapshot per watch name (or per saved task) and returns only comments that were not there before. Schedule it and you have a comment alert that bills only for what is new.

## `watchName` (type: `string`):

Name of the snapshot used by the new-only mode, for example "my-newsletters". Runs from a saved task get a snapshot automatically even without a name.

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

Hard cap on returned rows in the run.

## Actor input object example

```json
{
  "posts": [
    "https://www.noahpinion.blog/p/the-problems-with-utilitarianism"
  ],
  "newsletters": [],
  "maxPostsPerNewsletter": 5,
  "maxCommentsPerPost": 500,
  "commentSort": "best_first",
  "newOnly": false,
  "maxItems": 10000
}
```

# Actor output Schema

## `rows` (type: `string`):

One row per comment with author, date, text, likes, replies and thread depth.

## `changes` (type: `string`):

Comments that appeared since the previous snapshot of the same watch name or task.

## `summary` (type: `string`):

Counts per row type, what each filter dropped, what was charged and notes.

# 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 = {
    "posts": [
        "https://www.noahpinion.blog/p/the-problems-with-utilitarianism"
    ],
    "newsletters": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("deriverge/substack-comments-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 = {
    "posts": ["https://www.noahpinion.blog/p/the-problems-with-utilitarianism"],
    "newsletters": [],
}

# Run the Actor and wait for it to finish
run = client.actor("deriverge/substack-comments-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 '{
  "posts": [
    "https://www.noahpinion.blog/p/the-problems-with-utilitarianism"
  ],
  "newsletters": []
}' |
apify call deriverge/substack-comments-scraper --silent --output-dataset

```

## MCP server setup

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