# Substack Notes Scraper (`maximedupre/substack-notes`) Actor

Collect public Substack Notes from topic phrases or public source URLs. Save note text, stable IDs, public links, author details, times, engagement, media, and discovery data.

- **URL**: https://apify.com/maximedupre/substack-notes.md
- **Developed by:** [Maxime Dupré](https://apify.com/maximedupre) (community)
- **Categories:** Social media, Marketing, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.01 / 1,000 notes

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?

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

### 📝 Track public Substack Notes

Researchers, marketers, and newsletter teams can collect public Substack Notes without copying them by hand. This Actor returns one structured row per saved Note with text, stable identity, a public URL, source details, times, author and publication context, visible engagement, media, and links.

Use it to:

- Compare Note and post formats with **[Substack Notes Vs Posts](https://apify.com/maximedupre/substack-notes/examples/substack-notes-vs-posts)** when planning a Substack research workflow.
- Check **[Substack Notes Character Limit](https://apify.com/maximedupre/substack-notes/examples/substack-notes-character-limit)** examples when Note length matters.
- Explore public Note ranking questions with **[Substack Notes Algorithm](https://apify.com/maximedupre/substack-notes/examples/substack-notes-algorithm)**.
- Learn **[How To Use Substack Notes](https://apify.com/maximedupre/substack-notes/examples/how-to-use-substack-notes)** by collecting public Notes for a topic.
- Review **[How To Post A Note On Substack](https://apify.com/maximedupre/substack-notes/examples/how-to-post-a-note-on-substack)** ideas before a publishing session.
- Compare public social conversations with **[Substack Notes Vs Twitter](https://apify.com/maximedupre/substack-notes/examples/substack-notes-vs-twitter)** when researching a topic.
- Build a repeatable public Note workflow with **[Substack Notes Strategy](https://apify.com/maximedupre/substack-notes/examples/substack-notes-strategy)**.

#### 📦 Structured Note data

Each saved row describes one public Substack Note. It includes the visible Note text, stable Note ID, canonical URL, source type, publication and collection times, author and publication context, visible engagement, media, outbound links, and thread fields. If the same Note appears again through another phrase or submitted URL, the first eligible match is saved and later matches are ignored. `discoveredFrom` shows only the phrase or URL that first produced the row. Missing public values stay `null` when the source does not show them, and arrays can be empty.

#### ▶️ Run a public Notes collection

**Choose a target**

Select Topic search for phrases, or Public source URLs for public profile, feed, or Note pages.

**Set a limit**

`maxItems` is an optional positive limit for unique Notes. Leave it empty to return all available results until the source is exhausted.

**Filter dates**

`dateField` checks publication time or change time. `startDate` and `endDate` are inclusive UTC dates.

**Skip known Notes**

Add stable Note IDs in `excludeNoteIds` when you already have them.

Public search ranking, availability, rate limits, and source changes can affect what is returned. This Actor does not collect newsletter posts, full article bodies, or private or subscriber-only content.

#### ⚙️ Input

**Input fields**

| Field | Type | What it does |
| --- | --- | --- |
| `target` | string | Required. Choose `topicSearch` for topic phrases or `sourceUrls` for public source URLs. |
| `topicPhrases` | array of strings | Adds one or more phrases for Topic search. This field is ignored when `target` is `sourceUrls`. |
| `searchOrder` | string | Chooses `relevance` or `recent` order for Topic search. It is ignored when `target` is `sourceUrls`. |
| `sourceUrls` | array of objects | Adds public Substack profile, feed, or Note URLs. This field is ignored when `target` is `topicSearch`. |
| `sourceUrls[].url` | string | A public Substack profile, feed, or Note URL to read. |
| `dateField` | string | Chooses whether dates check `published` time or `changed` time. |
| `startDate` | string | Optional inclusive UTC start date in `YYYY-MM-DD` format. |
| `endDate` | string | Optional inclusive UTC end date in `YYYY-MM-DD` format. |
| `excludeNoteIds` | array of strings | Skips the listed stable Note IDs. |
| `maxItems` | integer | Optional positive limit for unique Notes. Leave it empty to return all available results until the source is exhausted. |

**Example input**

This is the public input from a successful current-beta default-input run:

```json
{
  "target": "topicSearch",
  "topicPhrases": [
    "technology"
  ],
  "searchOrder": "relevance",
  "dateField": "published",
  "maxItems": 100
}
```

#### 🧾 Output

Every saved row uses the same shape for topic, profile, feed, and Note sources. Values can be `null` when a public source does not show them. Arrays can be empty.

**Output fields**

| Field | Type | What it does |
| --- | --- | --- |
| `noteId` | string | Stable public ID for the Note. |
| `text` | string | Visible text of the public Note. |
| `noteUrl` | string | Canonical public URL for the Note. |
| `sourceType` | string | Says how the Note was found: `topicSearch`, `profile`, `feed`, or `note`. |
| `discoveredFrom` | object | Groups the first topic phrase or public source URL that produced the Note. |
| `discoveredFrom.type` | string | Says whether the first discovery value was a `topicPhrase` or `sourceUrl`. |
| `discoveredFrom.value` | string | The first topic phrase or public source URL that produced the Note. |
| `publishedAt` | string | UTC time when the Note was published. |
| `editedAt` | string or null | UTC time when the Note was last edited, when public edit time is available. |
| `collectedAt` | string | UTC time when the Actor collected the Note. |
| `author` | object | Groups public author identity and profile details. |
| `author.id` | string or null | Public author ID, when available. |
| `author.name` | string or null | Visible author name, when available. |
| `author.handle` | string or null | Visible author handle, when available. |
| `author.profileUrl` | string or null | Public author profile URL, when available. |
| `author.photoUrl` | string or null | Public author photo URL, when available. |
| `author.bio` | string or null | Visible author bio, when available. |
| `publication` | object or null | Groups the public publication linked to the Note, when available. |
| `publication.id` | string or null | Public publication ID, when available. |
| `publication.name` | string or null | Visible publication name, when available. |
| `publication.url` | string or null | Public publication URL, when available. |
| `engagement` | object | Groups visible counts and reaction details. |
| `engagement.likes` | number or null | Visible like or reaction count, when available. |
| `engagement.restacks` | number or null | Visible restack count, when available. |
| `engagement.replies` | number or null | Visible reply count, when available. |
| `engagement.total` | number or null | Total visible engagement count, when available. |
| `engagement.reactions` | array of objects or null | Lists visible reactions and their counts, when available. |
| `engagement.reactions[].emoji` | string | The visible reaction or emoji label. |
| `engagement.reactions[].count` | number | Visible count for that reaction. |
| `media` | array of objects | Lists media attached to the Note. It can be empty. |
| `media[].url` | string | Public URL for attached media. |
| `media[].type` | string or null | Media type, when exposed. |
| `media[].width` | number or null | Media width in pixels, when available. |
| `media[].height` | number or null | Media height in pixels, when available. |
| `outboundLinks` | array of strings | Public URLs found in the Note text. It can be empty. |
| `thread` | object | Groups reply status and public parent or root Note relationships. |
| `thread.isReply` | boolean or null | Says whether the Note is a reply, when available. |
| `thread.parentNoteId` | string or null | Public ID of the direct parent Note, when available. |
| `thread.parentUrl` | string or null | Public URL of the direct parent Note, when available. |
| `thread.rootNoteId` | string or null | Public ID of the root Note, when available. |
| `thread.rootUrl` | string or null | Public URL of the root Note, when available. |

**Example row**

This is a genuine, unshortened row from a successful current-beta profile run:

```json
{
  "noteId": "321255318",
  "text": "Also as we are going to kickstart our new training series - AI Avatar Training and Social Media Domination.\n\nWe are doing a limited time 50% off on our Substack plan.\n\nAnd you will be having full zoom access and replay and all our previous ai courses.\n\nDetails here:\n\nhttps://sifuyik.substack.com/p/24-hours-only-100-hours-of-ai-training",
  "noteUrl": "https://substack.com/@sifuyik/note/c-321255318",
  "sourceType": "profile",
  "discoveredFrom": {
    "type": "sourceUrl",
    "value": "https://substack.com/@sifuyik"
  },
  "publishedAt": "2026-08-24T01:32:51.983Z",
  "editedAt": null,
  "collectedAt": "2026-08-25T16:34:50.254Z",
  "author": {
    "id": "73134922",
    "name": "Sifu Yik Chan",
    "handle": "sifuyik",
    "profileUrl": "https://substack.com/@sifuyik",
    "photoUrl": "https://substack-post-media.s3.amazonaws.com/public/images/658267de-b7ce-4827-a4a4-bf4aec4d9ddc_866x866.jpeg",
    "bio": "AI Tips and News"
  },
  "publication": {
    "id": "7223942",
    "name": "Sifu Yik's Substack",
    "url": "https://sifuyik"
  },
  "engagement": {
    "likes": 2,
    "restacks": 0,
    "replies": 0,
    "total": 2,
    "reactions": [
      {
        "emoji": "❤",
        "count": 2
      }
    ]
  },
  "media": [
    {
      "url": "https://substack-post-media.s3.amazonaws.com/public/images/831fada1-9358-40eb-9a73-f000a1804f6e_2560x3200.png",
      "type": "image",
      "width": 2560,
      "height": 3200
    }
  ],
  "outboundLinks": [
    "https://sifuyik.substack.com/p/24-hours-only-100-hours-of-ai-training"
  ],
  "thread": {
    "isReply": false,
    "parentNoteId": null,
    "parentUrl": null,
    "rootNoteId": null,
    "rootUrl": null
  }
}
```

#### 💳 Pricing

Pay per event. The primary event is `Note` at $0.00001 for each public Note saved to your dataset. The total follows the number of Notes saved.

#### 🔌 Integrations

Start the Actor in Apify Console or through the standard Actor API. Read the dataset through its API link or export it after the run.

https://www.youtube.com/watch?v=bNACk1\_S\_6w\&list=PLObrtcm1Kw6MUrlLNDbK9QRg8VDJg0gOW\&index=4

#### ❓ FAQ

##### What does this Actor collect?

It collects public Substack Notes from topic searches and public profile, feed, or Note sources. It does not collect newsletter posts, full article bodies, or private and subscriber-only content.

##### Can I use more than one topic phrase?

Yes. Add several topic phrases to one Topic search run. They use the same search order, date filters, exclusions, and note limit.

##### Can I use a public profile, feed, or Note URL?

Yes. Choose Public source URLs and add one or more public Substack URLs. The URL can point to a profile, feed, or Note page.

##### What happens when the same Note matches twice?

The first eligible match is saved. Later matches from another phrase or submitted URL are ignored, and `discoveredFrom` keeps the first matching value.

##### Can I filter Notes by a date range?

Yes. Choose publication time or change time, then add an inclusive UTC start date, end date, or both.

##### Does this return every available Note?

No result set can promise full coverage. Public availability, search ranking, rate limits, and source changes can affect which Notes are returned.

##### What happens when a public field is missing?

The Actor keeps the missing value as `null` when the schema allows it. It does not invent a name, count, URL, or time.

##### Do I need a Substack account or API key?

No. The Actor reads anonymous public Substack surfaces and does not require authenticated access.

### 📝 Changelog

**0.0: Initial release**

### 🆘 Support

For issues, questions, or feature requests, [file a ticket](https://console.apify.com/actors/maximedupre~substack-notes/issues) and I'll fix or implement it in less than 24h 🫡

### 🔗 Related Actors

- [Substack Recommendations](https://apify.com/maximedupre/substack-recommendations) - Map public newsletter recommendation links from known publication pages.
- [Twitter (X) Scraper](https://apify.com/maximedupre/twitter-scraper) - Collect public X posts for social listening alongside Substack Notes.
- [Reddit Comments Search Scraper](https://apify.com/maximedupre/reddit-comments-search-scraper) - Search public Reddit comments to compare discussion around the same topic.
- [Substack Notes Search and Author Monitor](https://apify.com/automation-lab/substack-notes-search-author-monitor) - Search public Substack Notes and track author activity with stable IDs and engagement.
- [Substack Notes Scraper](https://apify.com/easyapi/substack-notes-scraper) - Extract Substack search results with author details, images, and engagement metrics.

**Made with ❤️ by Maxime Dupré**

# Actor input Schema

## `target` (type: `string`):

Choose where to collect public Substack Notes.

## `topicPhrases` (type: `array`):

Enter one or more phrases for a public Substack Notes search. Use this only with Topic search. It is ignored for Public source URLs.

## `searchOrder` (type: `string`):

Choose how Topic search results are ordered. Use this only with Topic search. It is ignored for Public source URLs.

## `sourceUrls` (type: `array`):

Enter one or more public Substack profile, feed, or Note URLs. The Actor collects Notes from these sources. Use this only with Public source URLs. It is ignored for Topic search.

## `dateField` (type: `string`):

Choose which Note time the date range checks: publication time or change time.

## `startDate` (type: `string`):

Optional inclusive start date in YYYY-MM-DD. Dates use UTC. Leave it empty to start at the first available result.

## `endDate` (type: `string`):

Optional inclusive end date in YYYY-MM-DD. Dates use UTC. Leave it empty to end at the last available result.

## `excludeNoteIds` (type: `array`):

Optional stable Note IDs to skip. Enter one or more IDs to exclude them from this run.

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

Optional positive number of unique Notes to collect. If you leave it empty, the Actor collects all available results until the public sources are exhausted.

## Actor input object example

```json
{
  "target": "topicSearch",
  "topicPhrases": [
    "technology"
  ],
  "searchOrder": "relevance",
  "dateField": "published",
  "maxItems": 100
}
```

# Actor output Schema

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

The collected public Substack 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 = {
    "target": "topicSearch",
    "topicPhrases": [
        "technology"
    ],
    "searchOrder": "relevance",
    "dateField": "published",
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("maximedupre/substack-notes").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 = {
    "target": "topicSearch",
    "topicPhrases": ["technology"],
    "searchOrder": "relevance",
    "dateField": "published",
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("maximedupre/substack-notes").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 '{
  "target": "topicSearch",
  "topicPhrases": [
    "technology"
  ],
  "searchOrder": "relevance",
  "dateField": "published",
  "maxItems": 100
}' |
apify call maximedupre/substack-notes --silent --output-dataset

```

## MCP server setup

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

```

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/RtZL7hFlj02VgZLb1/builds/FOeiy9uFedYuphQVd/openapi.json
