# Substack Posts API: Newsletter Posts, Likes, Comments (`ailu_studio/substack-posts-api`) Actor

Get posts from any Substack newsletter (substack.com or custom domain): title, date, free or paid, likes, comments, word count, authors and optional full text of free posts. Public API, no login.

- **URL**: https://apify.com/ailu_studio/substack-posts-api.md
- **Developed by:** [Ailu Studio](https://apify.com/ailu_studio) (community)
- **Categories:** Social media, News, Marketing
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 post rows

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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 Posts API: Newsletter Posts, Likes, Comments and Full Text

Get the posts of any Substack newsletter as clean JSON rows: title, subtitle, URL, publish time, free or paid, likes, comments, restacks, word count, authors, section, cover image and preview text, plus the optional full text of free posts. Works with `name.substack.com` publications and custom domains (for example `newsletter.pragmaticengineer.com`). Unlike the most used Substack Actor in the Store, full text of free posts costs nothing extra and there is no start fee; you can filter by free/paid and by date, and the price is $1.00 per 1,000 rows.

### Use cases

- Track what competitors, analysts or creators publish and how readers react (likes, comments, restacks).
- Build a reading list or RAG corpus from the free posts of a set of newsletters.
- Compare publishing cadence, free vs paid mix and post length across newsletters.
- Schedule a daily run with `sinceDays: 1` to collect new posts from many publications.

### Input

| Field | Default | Meaning |
|---|---|---|
| `publications` | `["newsletter.pragmaticengineer.com", "astralcodexten.substack.com"]` | Subdomain (`astralcodexten`), `*.substack.com` address, custom domain or any post URL. |
| `sinceDays` | `0` | Only posts from the last N days. 0 = no date limit. |
| `audience` | `all` | `all`, `free` (open to everyone) or `paid` (paying or founding members). |
| `includeBody` | `false` | Also fetch the full text of free posts into `bodyText`. |
| `maxBodiesPerPublication` | `20` | Cap on full-text requests per publication, newest first. |
| `maxPostsPerPublication` | `50` | Cap on posts per publication, newest first. |
| `maxItems` | `200` | Cap on rows for the whole run. |

### Output

One row per post, newest first per publication.

| Field | Meaning |
|---|---|
| `id` | Stable key `<publicationHost>:<postId>`. |
| `publication`, `publicationId` | Publication host (from the canonical URL) and Substack ID. |
| `title`, `subtitle`, `url`, `slug` | Post identity and canonical link. |
| `publishedAt` | Publish time, ISO 8601 UTC. |
| `postType` | `newsletter`, `podcast`, `thread` or `video`. |
| `audience`, `isPaid` | Substack audience value and a paid flag. |
| `likes`, `comments`, `restacks` | Engagement counts at fetch time. |
| `wordCount` | Word count of the full post (also for paid posts). |
| `authors`, `authorHandles` | Byline names and Substack handles. |
| `sectionName`, `coverImageUrl`, `description`, `previewText`, `podcastUrl` | Extra metadata. |
| `bodyText` | Plain-text body of free posts when `includeBody` is on, else `null`. |
| `fetchedAt` | When the row was fetched. |

```json
{
  "id": "newsletter.pragmaticengineer.com:223351496",
  "postId": 223351496,
  "publication": "newsletter.pragmaticengineer.com",
  "publicationId": 458709,
  "title": "How software engineering is changing: a tale of two eras",
  "subtitle": "We invited readers to write about how building software changed, in an essay challenge. Many of you took part, and today we’re sharing the winning piece from a strong set of entries",
  "url": "https://newsletter.pragmaticengineer.com/p/how-software-engineering-is-changing",
  "slug": "how-software-engineering-is-changing",
  "publishedAt": "2026-11-03T16:58:22.397Z",
  "postType": "newsletter",
  "audience": "everyone",
  "isPaid": false,
  "likes": 156,
  "comments": 2,
  "restacks": 11,
  "wordCount": 5921,
  "authors": ["Gergely Orosz", "Jessica Salmon", "Ivan Klaric"],
  "authorHandles": ["pragmaticengineer", "thejessicasalmon", "ivanklaric182781"],
  "sectionName": "Deepdives",
  "coverImageUrl": "https://substackcdn.com/image/fetch/$s_!WYGs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c874c2-9b02-4a24-9046-879cb1500023_1920x899.png",
  "description": "We invited readers to write about how building software changed, in an essay challenge. Many of you took part, and today we’re sharing the winning piece from a strong set of entries",
  "previewText": "Before we start: I’m in San Francisco this week and there are a few remaining spots for these live events (registration required):",
  "podcastUrl": null,
  "bodyText": null,
  "fetchedAt": "2026-11-04T12:00:00.000Z"
}
```

### How it works and limits

- Reads each publication's archive listing (`/api/v1/archive?sort=new`, up to 50 posts per request) and, for full text, `/api/v1/posts/<slug>`. These are the public JSON endpoints the Substack website itself uses; no login or cookies are sent.
- Requests to one publication are spaced at least 0.5 s apart with retries on 429/5xx. Substack rate-limits aggressive clients, so keep `maxBodiesPerPublication` moderate.
- Paid posts are listed with full metadata, but their body is never fetched or returned (only Substack's short `previewText`).
- Counts (likes, comments, restacks) are a snapshot at fetch time.
- Not covered: Substack Notes, comments text, subscriber counts, profile pages (`substack.com/@name`), private or deleted posts.

### Pricing

Pay per result: **$1.00 per 1,000 rows** (`result` event). Platform usage is billed separately by Apify. Removed/free rows are not charged.

### Legal

Not affiliated with Substack Inc. Uses the public Substack publication JSON endpoints only. Data licence/terms: https://substack.com/tos. Post texts are copyrighted by their authors; respect their rights when you republish anything.

### FAQ

**Does it work with custom domains?**
Yes. Enter the domain (e.g. `newsletter.pragmaticengineer.com`); `astralcodexten.substack.com` style addresses are followed to their custom domain automatically.

**Can I get the full text of paid posts?**
No. Only free posts include `bodyText`; paid posts return metadata and Substack's preview line.

**How do I pull a whole archive?**
Set `maxPostsPerPublication` and `maxItems` high (e.g. 5000). The Actor pages through the archive newest to oldest.

**Why is `publication` different from what I entered?**
It is taken from each post's canonical URL, so `astralcodexten.substack.com` becomes `www.astralcodexten.com`. This keeps `id` stable however you write the input.

# Actor input Schema

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

Substack newsletters as a subdomain ("astralcodexten"), a substack.com address ("platformer.substack.com"), a custom domain ("newsletter.pragmaticengineer.com") or any post URL. Profile links like substack.com/@name are not supported.

## `sinceDays` (type: `integer`):

Only return posts published within the last N days. 0 (default) means no date limit, so the newest posts up to the per-publication cap are returned.

## `audience` (type: `string`):

Filter by audience. "free" keeps posts open to everyone, "paid" keeps posts for paying or founding members. Paid posts are listed with their metadata only.

## `includeBody` (type: `boolean`):

When true, fetches the full article of free posts and returns it as plain text in bodyText (one extra request per post). Paid posts never include the body.

## `maxBodiesPerPublication` (type: `integer`):

Upper limit on full-text requests per publication when "Include full text" is on (default 20), newest posts first. Keeps runs fast and polite.

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

Upper limit on posts returned per publication, newest first (default 50). Use a large value with sinceDays to pull a whole archive.

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

Upper limit on returned rows for the whole run (default 200). Fetching stops once it is reached.

## Actor input object example

```json
{
  "publications": [
    "newsletter.pragmaticengineer.com",
    "astralcodexten.substack.com"
  ],
  "sinceDays": 0,
  "audience": "all",
  "includeBody": false,
  "maxBodiesPerPublication": 20,
  "maxPostsPerPublication": 50,
  "maxItems": 200
}
```

# Actor output Schema

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

All rows in the default 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": [
        "newsletter.pragmaticengineer.com",
        "astralcodexten.substack.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ailu_studio/substack-posts-api").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": [
        "newsletter.pragmaticengineer.com",
        "astralcodexten.substack.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("ailu_studio/substack-posts-api").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": [
    "newsletter.pragmaticengineer.com",
    "astralcodexten.substack.com"
  ]
}' |
apify call ailu_studio/substack-posts-api --silent --output-dataset

```

## MCP server setup

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

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/j6ltcges1gjjnxVtL/builds/ERpVsJHgtyRNpA4Y0/openapi.json
