# Substack Scraper: Newsletter Posts, Likes & Comments (`larkmoor/substack-newsletter-posts`) Actor

Export any Substack newsletter's posts: title, subtitle, date, authors, free or paid, likes, comments, restacks, word count, podcast audio and optional full text. Paste publication URLs or names. Keyword and date filters. $1.50 per 1,000 posts, no big start fee.

- **URL**: https://apify.com/larkmoor/substack-newsletter-posts.md
- **Developed by:** [Larkmoor Data](https://apify.com/larkmoor) (community)
- **Categories:** News, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.28 / 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.
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 Scraper: Newsletter Posts, Likes & Comments

Export every post of any Substack newsletter into a clean table: title, subtitle, publish date, authors, free or paid, likes, comment count, restacks, word count, cover image, podcast audio and, if you want it, the full public text. Paste newsletter URLs (or just their names), set how far back to go, and download the results as JSON, CSV or Excel.

It reads each newsletter's own public archive data (the same JSON the newsletter's archive page loads in your browser), respects robots.txt, and never logs in.

### What you can use it for

- **Competitor and market research**: see which topics get the most likes, comments and restacks in your niche
- **Content strategy**: find the best-performing posts of the top newsletters in a category before you write your next issue
- **Sponsorship and partnership scouting**: compare how active and engaged newsletters are before you pitch or buy an ad
- **Monitoring**: run it daily with "only posts from the last 1 day" to get new posts from a list of newsletters in a spreadsheet, Slack or your CRM
- **AI and research datasets**: export the full text of free posts for summarisation, topic modelling or sentiment analysis
- **Writers**: back up your own archive with all engagement numbers in one file

### Why this one

- **Cheap and predictable**: $1.50 per 1,000 posts and a $0.001 start fee. No $0.09 start fee, no monthly rental.
- **Engagement numbers on every row**: likes, comments, restacks and word count, so you can rank posts immediately
- **Free vs paid flag** on every post (`audience`, `isPaid`), so you can measure how much of a newsletter is paywalled
- **Any address works**: `name.substack.com`, custom domains (`www.lennysnewsletter.com`), `substack.com/@name` or just `name`
- **Filters that save money**: date window, keywords and "skip paid posts" are applied before you are charged
- **Full archive depth**: page through the whole archive, newest first or most popular first

### Input

| Field | What it does |
|---|---|
| `publications` | List of newsletters: URL, custom domain, `substack.com/@name` or just the name |
| `maxPostsPerPublication` | How many posts per newsletter (default 50, up to 5,000) |
| `sort` | `new` (newest first) or `top` (most popular first) |
| `maxAgeDays` | Only posts from the last N days (with newest first) |
| `keywords` | Keep only posts whose title, subtitle, description or preview contains one of these words |
| `skipPaidPosts` | Leave out paid-subscribers-only posts |
| `includeContent` | Add the post body as plain text (free posts are complete; paid posts give their public preview) |
| `previewChars` | Length of the short preview text (0 = none) |
| `maxResults` | Hard cap across all newsletters, keeps the cost predictable |

Example input:

```json
{
  "publications": ["https://www.lennysnewsletter.com", "noahpinion"],
  "maxPostsPerPublication": 100,
  "sort": "new",
  "maxAgeDays": 90,
  "includeContent": false
}
```

### Output

One row per post:

```json
{
  "publication": "noahpinion.substack.com",
  "title": "A Nobel for New Keynesian macro, at last!",
  "subtitle": "...",
  "url": "https://noahpinion.substack.com/p/...",
  "publishedAt": "2026-10-13T...Z",
  "authors": ["Noah Smith"],
  "audience": "everyone",
  "isPaid": false,
  "type": "newsletter",
  "tags": [],
  "likes": 441,
  "comments": 120,
  "restacks": 35,
  "wordCount": 2753,
  "coverImage": "https://substackcdn.com/...",
  "description": "...",
  "preview": "First 300 characters of the post...",
  "podcastUrl": null,
  "podcastDurationSec": null,
  "language": "en"
}
```

With `includeContent` on, each row also gets `contentText` (plain text) and `contentIsFull` (true for free posts).

A summary (newsletters read, posts returned, and any newsletter that could not be read with the reason) is saved as `OUTPUT` in the run's key-value store.

### How much does it cost?

Pay per result: **$1.50 per 1,000 posts** on the Free plan plus **$0.001 per run**, with automatic discounts on paid Apify plans. Platform compute is included. Posts removed by your filters are not charged, and a newsletter that cannot be read costs nothing.

Worked examples:

- **Weekly check of 30 newsletters** in your niche, `maxAgeDays: 7`, about 2 posts each: 60 posts = **$0.09 a week**
- **Top 100 posts of 20 big newsletters** with `sort: top`: 2,000 posts = **$3.00**
- **Full archive of one newsletter** with about 800 posts: **$1.20**

Set `maxResults` to cap any run.

### FAQ

**Is it legal to scrape Substack?**
This actor only reads data that each newsletter publishes openly to every visitor: its public archive listing and public post pages. It respects robots.txt, does not log in, does not bypass the paywall and does not collect subscriber lists or personal data. Paid posts return only what Substack shows the public (title, numbers and preview). As always, use the data in line with the newsletter's terms and copyright: analysis and research are fine, republishing someone's writing is not.

**Can I get the full text of paid posts?**
No. Paid posts give their public preview only (`contentIsFull: false`). Free posts are complete.

**Does it work with custom domains?**
Yes. Most big newsletters use their own domain (for example `www.lennysnewsletter.com`); paste it as is.

**Can I search all of Substack by keyword?**
This actor works per newsletter. Give it a list of newsletters and use `keywords` to keep only posts about your topic.

**How fresh is the data?**
Live: every run reads the newsletter's archive at that moment, including like and comment counts.

**Why is a newsletter missing from the results?**
It has moved off Substack, is private, or the address is wrong. The reason is listed in the `OUTPUT` summary.

**Can I schedule it?**
Yes. Use Apify Schedules (for example daily at 8:00) with `maxAgeDays: 1` and connect the dataset to Google Sheets, Slack, Zapier or Make.

### Integrations

Results work with every Apify integration: Google Sheets, Slack, Zapier, Make, Airtable, webhooks, the Apify API and the Python and JavaScript clients. Download as JSON, CSV, Excel, XML or HTML.

### Changelog

- **2026-10-20** — First release: posts with likes, comments, restacks, word count, free/paid flag, podcast info, optional full text; date, keyword and paid filters; custom domains and `@name` profiles.

# Actor input Schema

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

Newsletter URLs (https://name.substack.com, custom domains like https://www.lennysnewsletter.com, or substack.com/@name) or just the name ("platformer"). One per line.

## `maxPostsPerPublication` (type: `integer`):

Newest first (or most-liked first with Sort = top). Default 50.

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

Order of the archive.

## `maxAgeDays` (type: `integer`):

Only with Sort = newest. Empty = whole archive.

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

Keep only posts whose title, subtitle, description or preview contains one of these words. Empty = all posts.

## `skipPaidPosts` (type: `boolean`):

Leave out posts marked paid-subscribers-only.

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

Adds the post's public body as plain text (one extra request per post, slower). Paid posts only return their free preview.

## `previewChars` (type: `integer`):

Length of the free preview text. 0 = no preview.

## `maxResults` (type: `integer`):

Hard cap across all publications, keeps cost predictable.

## Actor input object example

```json
{
  "publications": [
    "https://www.lennysnewsletter.com",
    "noahpinion"
  ],
  "maxPostsPerPublication": 50,
  "sort": "new",
  "keywords": [],
  "skipPaidPosts": false,
  "includeContent": false,
  "previewChars": 300,
  "maxResults": 1000
}
```

# Actor output Schema

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

All results as JSON items in the run's default dataset (also downloadable as CSV/Excel from the dataset).

# 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 = {
    "publications": [
        "https://www.lennysnewsletter.com",
        "noahpinion"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("larkmoor/substack-newsletter-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 = { "publications": [
        "https://www.lennysnewsletter.com",
        "noahpinion",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("larkmoor/substack-newsletter-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 '{
  "publications": [
    "https://www.lennysnewsletter.com",
    "noahpinion"
  ]
}' |
apify call larkmoor/substack-newsletter-posts --silent --output-dataset

```

## MCP server setup

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