# Substack Publication, Post & Comment Monitor (`luminar/substack-publication-post-comment-monitor`) Actor

Discover public Substack publications and posts, export publication, author, post and public-comment rows, and monitor compatible snapshots without AI interpretation.

- **URL**: https://apify.com/luminar/substack-publication-post-comment-monitor.md
- **Developed by:** [Luka](https://apify.com/luminar) (community)
- **Categories:** Automation, News, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.53 / 1,000 post 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

<p><img src="https://substack.com/img/substack.png" width="64" height="64" alt="Official Substack source icon"></p>

Discover public Substack publications and posts by topic, export publication archives, collect exact public posts, or retrieve public comments and replies. Repeat a compatible run in Changes mode to receive factual before-and-after events without sentiment scoring, inferred subscriber data, login, or paywall bypass.

### 🚀 Start in 60 seconds

Choose the smallest workflow that matches your job. This copy-paste example discovers public publications and posts for one topic and stops after 25 posts:

```json
{
  "workflow": "discover_topics",
  "keywords": ["independent publishing"],
  "maxPosts": 25,
  "outputMode": "current"
}
```

Run it once, open the Dataset, and check the final `coverage` row. For monitoring, keep the same workflow, targets, filters, limits, and included data families, then switch `outputMode` to `changes` on later sequential runs.

### 🎯 Choose the right workflow

| Workflow | Use it for | Required target | Main rows |
|---|---|---|---|
| Discover by topic | Find public publications and posts around a subject | `keywords` | publication, author, post, coverage |
| Publication archive | Export public publication histories by Substack URL, subdomain, or verified custom domain | `publicationUrls` | publication, author, post, optional comment, coverage |
| Exact posts | Open known public post URLs and record their public access state | `postUrls` | publication, author, post, optional comment, coverage |
| Public comments | Build a public comment-and-reply table for known posts | `postUrls` | publication, author, post, comment, coverage |

Exact-post targets automatically request the public post body or preview. Public comments are returned only where Substack's public endpoint actually exposes them.

### 📦 What you get

Every Dataset row has a stable `recordType` and target-scoped identity:

| Record type | Buyer-useful fields |
|---|---|
| `publication` | public name, domain, description, logo, language/category, visible subscriber and pricing fields when exposed |
| `author` | public byline identity, handle, profile URL, avatar and bio when exposed |
| `post` | canonical URL, title, subtitle, type, access state, public body or preview, dates, tags, media and public counts |
| `comment` | comment/reply ID, post ID, public author ID, text, timestamp, reactions, parent ID and depth |
| `change` | stable change ID, factual change type, changed fields and compatible before/after values |
| `coverage` | requested, observed and delivered counts, source shape, caps, duplicates, pages, bytes and field coverage |

![Substack publication post comment and coverage rows in the Actor Dataset](https://api.apify.com/v2/key-value-stores/DJgPR6wwS2tIaLDQW/records/substack_publication_change_monitor--d4bd39ccce0b2b5f-substack-public-data.png)

The screenshot shows a representative fixture rendered from the Actor's delivered output shape. Its identifiers and publication names are synthetic; no private account or paid content appears in it.

Representative fixture output:

```json
{
  "recordType": "post",
  "stableId": "substack:post:fixture-9001",
  "targetKey": "post:https://publication.example.com/p/example-public-post",
  "canonicalUrl": "https://publication.example.com/p/example-public-post",
  "title": "Representative public post",
  "contentAccess": "full_public",
  "publishedAt": "2026-08-29T10:00:00.000Z",
  "reactionsCount": 10,
  "commentsCount": 2,
  "coverageState": "COMPLETE"
}
```

### 🎛️ Input guide

#### Start here

- `workflow` selects topic discovery, publication archive, exact posts, or public comments.
- `keywords` accepts up to 10 topic phrases for public discovery.
- `publicationUrls` accepts up to 1,000 public publication roots, Substack subdomains, or custom domains that prove Substack shape.
- `postUrls` accepts up to 10,000 concrete public `/p/` URLs.
- `maxPosts` caps delivered post rows at 10,000. Start with 25 for discovery or 50 for one archive.
- `maxBuyerChargeUsd` stops before source work if the projected event vector exceeds your chosen ceiling.

#### Filters and public access

- `dateFrom` and `dateTo` apply inclusive `YYYY-MM-DD` publication-date filters.
- `contentTypes` keeps selected public article, podcast, video, thread, or note records.
- `contentAccessFilter` keeps one factual public-access class.
- `minimumEngagement` filters by the sum of source-exposed reactions, comments, and restacks. It is not a quality score.

`contentAccess` is always one of `full_public`, `free_preview`, `truncated`, or `paywalled_unavailable`. The Actor never signs in, sends subscriber cookies, or reconstructs paid/private body text.

#### Optional output families

- `includeBody` adds public post HTML/text where the official response exposes it.
- `includePublication` and `includeAuthors` add linked public context rows.
- `includeComments` requests public comments and replies for delivered posts.
- `includeRawSource` adds a bounded raw object for debugging; leave it off for cleaner JSON, CSV, and Excel exports.
- `maxCommentsPerPost` caps one post at 250 comments/replies, while `maxTotalComments` caps the run at 20,000.

#### Baseline and Changes

The first compatible Changes run seeds a baseline without producing a flood of false `NEW` events. Later runs can emit `NEW`, `UPDATED`, `CONTENT_ACCESS_CHANGED`, `ENGAGEMENT_CHANGED`, `RANK_CHANGED`, and optional free `UNCHANGED` audit rows.

`REMOVED` is deliberately strict: it requires two consecutive compatible `EMPTY_CONFIRMED` observations for the same exact-post target. Missing rows from topic search, archive windows, filters, caps, partial responses, or blocked responses are never treated as deletion proof.

Use `monitorId` to separate intentional monitor scopes. `maxTrackedItems` can cap persisted publication, post, and comment identities at 40,000. Run the same monitor scope sequentially so one baseline cannot race another.

### 💰 Pricing and cost control

The run page shows the applicable pricing before execution. A run can include a start event plus delivered publication, post, comment, monitored-target, or factual-change events; coverage and contextual author rows are bundled.

The one-time useful-run event is **$0.00425**. On the Free plan, delivered publication, post, public comment/reply, Changes-scope, and factual-change events cost **$0.000100480769**, **$0.001004807692**, **$0.000452163462**, **$0.0004**, and **$0.001004807692** respectively. Paid Apify membership tiers receive the lower prices shown on the run page; the Actor validates the exact active tier vector before source work.

Keep the first run inexpensive:

- start with one target and a small row cap;
- leave body and comments off unless the buyer job requires them;
- use archive metadata for broad lists and request full public bodies deliberately;
- inspect coverage before scheduling Changes runs;
- set `maxBuyerChargeUsd` to a ceiling you accept.

The Actor stops before source work when the projected buyer charge exceeds that ceiling. Source, state, byte, request, and runtime safeguards keep deliberately large jobs bounded.

### ✅ Coverage and reliability

| State | Meaning |
|---|---|
| `COMPLETE` | The requested official public source scope was exhausted. |
| `PARTIAL` | Useful rows were returned, but a requested source component was not complete. |
| `FILTERED` | Source rows existed, but your filters removed every delivered result. |
| `CAPPED` | A row, request, byte, runtime, or state guard stopped the target before exhaustion. |
| `BLOCKED` | The expected official public shape was not reached because access was blocked or challenged. |
| `EMPTY_CONFIRMED` | The expected official public shape was reached and proved no result for that target. |

The Actor uses first-party Substack web surfaces only: public search pages, publication archive and RSS, same-origin public archive/post JSON, exact public post pages and embedded public preload data, and same-origin public comments endpoints. Custom domains must first prove Substack shape; subsequent JSON calls remain same-origin. HTTP 401, 403, 429, and challenge pages become `BLOCKED`, never false empty results.

Stable IDs, target-scoped deduplication, bounded retries, explicit source-shape checks, and state commit only after confirmed delivery protect repeat workflows. A malformed response, origin mismatch, or unknown shape stops the run instead of inventing records.

### 🔌 API and automation

Use the same JSON input through Apify Console, API, client libraries, or your own automation. Read rows from the default Dataset and the run summary from the default key-value store output.

For scheduled monitoring:

1. verify one Current run and its coverage row;
2. repeat the identical scope with `outputMode: "changes"`;
3. keep same-scope executions sequential;
4. consume `change` rows using their stable IDs;
5. alert separately on `PARTIAL`, `CAPPED`, or `BLOCKED` coverage.

No public Task or schedule is required; you control where and when automation runs.

### ❓ FAQ and support

#### Does it bypass Substack paywalls?

No. It records only what official public responses expose. Restricted posts receive `free_preview`, `truncated`, or `paywalled_unavailable` instead of reconstructed paid text.

#### Why did a run return coverage but few content rows?

Open the target's `coverage` row. It distinguishes filters, caps, blocks, confirmed empty results, and incomplete public components.

#### Can I monitor a custom publication domain?

Yes, when the public root proves Substack shape. The Actor keeps later requests same-origin and fails closed if the domain no longer matches that contract.

#### How do I report a reproducible issue?

Include the workflow, a public URL, the coverage row, and the run ID. Never send subscriber cookies, paid article text, passwords, or private publication access.

Substack is a trademark of its owner. This independent public-data tool is not affiliated with or endorsed by Substack. The source icon is the unchanged official asset from `https://substack.com/brand` and `https://substack.com/img/substack.png`.

# Actor input Schema

## `workflow` (type: `string`):

Choose the buyer job first. It controls which target field is required, which source pages are opened, and which result types can be returned.

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

Required only for topic discovery. Enter up to 10 topics; each creates a separate target and its own coverage receipt. Ignored by the other workflows.

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

Required only for Publication archive. Paste up to 1,000 Substack URLs, subdomains, or public custom domains. Each becomes a canonical HTTPS target with its own coverage receipt.

## `postUrls` (type: `array`):

Required for Exact posts and Public comments. Paste up to 10,000 canonical public `/p/` post URLs and keep maxPosts at least as high as the target count. Comment collection opens only public comment data linked to these posts.

## `maxPublications` (type: `integer`):

Global ceiling for publication rows, including contextual publication rows linked to exact posts. Raise it with large exact-post batches; reaching it produces CAPPED coverage, never false completeness.

## `maxPosts` (type: `integer`):

Global ceiling for post rows across all targets. For Exact posts or Public comments it must cover every supplied post URL. A small value is the fastest useful first run; reaching the ceiling produces CAPPED coverage when more posts may exist.

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

Used only when comments are requested. Limits public comments and replies for each post; a visible next page or cursor makes that comment target CAPPED.

## `maxTotalComments` (type: `integer`):

Global comment and reply ceiling across the run. Comment rows add source work and use their own result event when pricing is active.

## `dateFrom` (type: `string`):

Optional inclusive YYYY-MM-DD filter for post publication time. This can turn source-complete traversal into FILTERED coverage for the requested result set.

## `dateTo` (type: `string`):

Optional inclusive YYYY-MM-DD filter. It must not be earlier than Published on or after.

## `contentTypes` (type: `array`):

Leave empty for every source-exposed type. Selecting values filters normalized posts; an unknown source type remains explicit rather than guessed.

## `contentAccessFilter` (type: `string`):

Filters by what the public source actually exposed. Paywalled unavailable means metadata or preview may be public while the paid body remains unavailable.

## `minimumEngagement` (type: `integer`):

Keeps posts whose source-exposed reactions + comments + restacks meet this total. It is a deterministic numeric filter, not a quality or trend score.

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

Current returns normalized rows. Changes seeds the first compatible run without a NEW flood, then emits deterministic title, body-access, rank, and public-metric changes on sequential repeats.

## `monitorId` (type: `string`):

Optional human label included in the state scope. Use a stable label when you intentionally maintain separate monitors over otherwise identical inputs.

## `includeUnchanged` (type: `boolean`):

Used only in Changes mode. Returns free UNCHANGED change rows for auditing; they do not become paid change events.

## `maxTrackedItems` (type: `integer`):

Used only for Changes. Leave empty to match the requested publication + post + comment scope automatically, or set an explicit ceiling up to 40,000. Contextual author rows do not create disappearance events.

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

Opens or parses post detail where needed and returns clean public HTML/text. Exact posts enable this automatically. Paid or private body text is never requested or reconstructed.

## `includePublication` (type: `boolean`):

Returns normalized publication metadata linked to delivered posts when the public source exposes it.

## `includeAuthors` (type: `boolean`):

Returns public author/byline rows. It does not infer private email addresses or subscriber identities.

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

Requests public comment/reply rows for delivered posts. Public comments workflow enables this automatically; unavailable or unverified comment access is PARTIAL, not empty.

## `includeRawSource` (type: `boolean`):

Adds a bounded rawSource object for debugging. Leave off for clean JSON/CSV/Excel rows; internal upload/transcoding keys are not promoted into the default schema.

## `maxBuyerChargeUsd` (type: `number`):

Fail before source work when the projected current pricing vector could exceed this ceiling. This is the buyer-charge guard, not an infrastructure-cost estimate.

## `maxRequests` (type: `integer`):

Internal source-request guard sized for the declared exact-target workload. This is a ceiling, not a request target; small row defaults still stop early.

## `maxSourceBytes` (type: `integer`):

Internal cumulative response-byte guard for deliberate bulk work. This ceiling does not make a small run fetch extra data.

## `maxStateBytes` (type: `integer`):

Internal serialized-state guard sized for the declared 40,000-entity maximum.

## `maxRuntimeSecs` (type: `integer`):

Internal source-work runtime guard for deliberate bulk runs; ordinary small runs return as soon as their requested scope is complete.

## `requestTimeoutSecs` (type: `integer`):

Internal per-request timeout.

## `retryAttempts` (type: `integer`):

Internal bounded retry policy.

## `requestIntervalMs` (type: `integer`):

Internal fair-access pacing in milliseconds.

## `_luminarQaFaultStage` (type: `string`):

Hidden fail-closed QA hook. Accepted only for the exact registered private QA Actor and immutable build tag.

## Actor input object example

```json
{
  "workflow": "discover_topics",
  "keywords": [
    "independent publishing"
  ],
  "maxPublications": 10,
  "maxPosts": 25,
  "maxCommentsPerPost": 50,
  "maxTotalComments": 250,
  "contentAccessFilter": "any",
  "minimumEngagement": 0,
  "outputMode": "current",
  "includeUnchanged": false,
  "includeBody": false,
  "includePublication": true,
  "includeAuthors": true,
  "includeComments": false,
  "includeRawSource": false,
  "maxBuyerChargeUsd": 5,
  "maxRequests": 50000,
  "maxSourceBytes": 2000000000,
  "maxStateBytes": 64000000,
  "maxRuntimeSecs": 10800,
  "requestTimeoutSecs": 30,
  "retryAttempts": 2,
  "requestIntervalMs": 250
}
```

# Actor output Schema

## `dataset` (type: `string`):

Use recordType to distinguish publications, authors, posts, comments, changes and coverage evidence.

## `overview` (type: `string`):

Buyer-focused columns across the intentionally mixed normalized dataset.

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

Before-and-after fields for compatible repeat runs; recordType remains visible because the dataset is mixed.

## `coverage` (type: `string`):

Requested, observed, duplicate, capped, partial, blocked and confirmed-empty evidence.

## `runSummary` (type: `string`):

Coverage, delivery, state and resource reconciliation for the run.

# 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 = {
    "workflow": "discover_topics",
    "keywords": [
        "independent publishing"
    ],
    "maxPublications": 10,
    "maxPosts": 25,
    "maxBuyerChargeUsd": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("luminar/substack-publication-post-comment-monitor").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 = {
    "workflow": "discover_topics",
    "keywords": ["independent publishing"],
    "maxPublications": 10,
    "maxPosts": 25,
    "maxBuyerChargeUsd": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("luminar/substack-publication-post-comment-monitor").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 '{
  "workflow": "discover_topics",
  "keywords": [
    "independent publishing"
  ],
  "maxPublications": 10,
  "maxPosts": 25,
  "maxBuyerChargeUsd": 5
}' |
apify call luminar/substack-publication-post-comment-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,luminar/substack-publication-post-comment-monitor"
        }
    }
}

```

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/mjYVJLz1ZV7VnkWtk/builds/xe9o4Xpk8qkeByydl/openapi.json
