How to use it: 3 steps
- Make a free Apify accountApify is the cloud platform where our tools run. The free plan gives you $5 of usage every month, no credit card needed. You pay Apify, not us.
- Paste your input and press StartNo code needed: you fill in a form (the input). It already has a working example. For example:
https://on.substack.com/feedin Full public feed URLs. Each start is one run. - Download the resultsWhen the run ends, the results are in a table (the dataset). Open Output, press Export and pick Excel or CSV. JSON and the API are there too.
This tool is an Apify Actor: a small program that runs on Apify. You can also run it on a schedule, for example every Monday. New to it? Read how it works, step by step and the 8 words you need first.
What it does
Get structured post metadata from known public Substack RSS or Atom feeds. This Substack newsletter scraper saves one stable Dataset row per unique feed item and never opens article pages.
- monitoring recent posts from a known list of Substack publications;
- importing newsletter post metadata into a database or workflow;
- comparing publication cadence using public feed dates;
- building a lightweight feed index where stable item identity matters.
Who uses it
Newsletter readers
Follow new posts from a list of Substack writers in one table.
Media analysts
Compare how often newsletters publish.
Developers
Feed post data into a database or workflow.
Price
Pay per event: you pay for each result the tool saves, plus a small start fee for each run. The price includes Apify platform costs. Apify bills you, not us. Prices below are for the Apify free plan.
| What you pay for | Price | When |
|---|---|---|
| Saved post metadata | $0.001 | One unique metadata row saved to the Dataset. |
| Start fee | $0.00071 | Charged once per run. This tool uses 1 GB of memory or less by default; more memory costs more. |
Examples
- One post: about $0.0017.
- 1,000 posts: about $1.00.
- Every day 100 posts, for 30 days: about $3.02 a month.
Free to try: the Apify free plan gives you $5 of usage every month, no credit card needed. That is enough for about 4,900 posts.
Prices checked on 24 October 2026. See the current price on Apify.
Questions
Can I add a newsletter home page?
No. Add the feed URL that ends with /feed.
Does it work with custom domains?
No. Only feeds on substack.com addresses work.
Do I get the article text?
No. You get titles, dates, authors and links, not the text.
Can I get the full archive?
No. It reads one feed response, up to 20 posts per feed.
Can I give the Actor a publication homepage?
No. Supply the explicit public feed URL, for example https://on.substack.com/feed. Homepages, post URLs, /archive, and bare slugs are outside this contract.
Does it support custom-domain newsletters?
No. Inputs must be *.substack.com/feed, and redirects to custom domains are not followed. This keeps the network and SSRF boundary explicit.
What happens with paid posts?
The Actor never bypasses a paywall. If the public feed exposes an audience/paywall signal, it can save allowed metadata with PAYWALLED_METADATA_ONLY. It does not request protected article content.
Why did a valid run return zero rows?
Check RUN_SUMMARY. A zero-row result can mean NO_POSTS for the chosen date window, NOT_FOUND, BLOCKED, RATE_LIMITED, parser drift, or another explicit source outcome.
Are duplicate feeds or posts charged twice?
Normalized duplicate input feeds are processed once. Duplicate item identities are first-wins and do not create another Dataset row or post-saved event.
Details
Tips for better results
- Add the full feed link, for example name.substack.com/feed.
- Use the date filters to get posts from one period only.
- Run it on a schedule to watch for new posts.
Limits and good to know
This Actor reads one finite feed response per accepted URL. It does not paginate an archive and does not claim full historical coverage.
- Up to 20 input feeds per run
- Up to 20 saved items per feed
- Up to 200 saved items total
- One direct HTTPS feed request per successful source, plus at most two bounded retries for retryable failures
- Internal run deadline is 60 seconds even if the Apify platform timeout is larger
- Redirects are not followed to custom domains or other hosts
- Response wire size, decompressed size, compression ratio, concurrency, retries, and DNS targets are bounded
Recent updates
Update
- M09 final repair: fixed escaped schema patterns and UTC fractional-date parity, removed false truncation when the finite response exactly meets the cap, and bounded production resource handling without changing the 60-second deadline.
- Preserved the v1 metadata fields, IDs, limits and explicit feed input. Added standard platform start pricing ($0.00071) with the row rate unchanged.
- M09 maintenance refresh (2026-10-09): added a small no-input UI prefill while preserving runtime defaults, refreshed Store/SEO copy and the README hero, upgraded safe transitive dependency resolutions and pinned the remaining
basic-ftpfix through an exact override. Local acceptance is 38/38 with zero productionnpm auditfindings; Cloud candidate/canary evidence is recorded separately in the M09 workflow closeout.
For developers
Sample result
One result from a real test run, in JSON. Real values change from run to run. Some values are changed for privacy. In Excel or CSV each field is a column.
{
"schemaVersion": "v1",
"recordType": "post",
"postKey": "v1:sha256:54fa1135afab659a504413c570559306127df98abd78cbba50fbd817cb9b5c4f",
"feedUrl": "https://on.substack.com/feed",
"publicationKey": "on.substack.com/feed",
"publicationName": "On Substack",
"publicationUrl": "https://on.substack.com/",
"sourceGuid": "https://on.substack.com/p/a-new-vision-for-sponsorships",
"canonicalUrl": "https://on.substack.com/p/a-new-vision-for-sponsorships",
"title": "A new vision for sponsorships",
"subtitle": null,
"publishedAt": "2026-10-08T13:14:23.000Z",
"updatedAt": null,
"author": "Substack team",
"categories": [],
"audience": "unknown",
"contentMode": "metadata_only",
"contentAvailability": "unknown",
"retrievedAt": "2026-10-09T11:24:18.452Z",
"sourceStatus": "POST_SAVED"
}Use it from code
Actor ID: cliqtomedia/substack-newsletter-scraper. You need your Apify API token from Apify Console → Settings → API & Integrations.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("cliqtomedia/substack-newsletter-scraper").call(run_input={
"feedUrls": [
"https://on.substack.com/feed"
],
"maxPostsPerFeed": 5
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item)curl -X POST "https://api.apify.com/v2/acts/cliqtomedia~substack-newsletter-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"feedUrls": ["https://on.substack.com/feed"], "maxPostsPerFeed": 5}'# pip install langchain-apify
# set APIFY_TOKEN in your environment
from langchain_apify import ApifyActorsTool
tool = ApifyActorsTool("cliqtomedia/substack-newsletter-scraper")
result = tool.invoke({"run_input": {"feedUrls": ["https://on.substack.com/feed"], "maxPostsPerFeed": 5}})
print(result)The cURL call waits for the results, up to 300 seconds. For longer runs, start a run (/runs) and get the results with a webhook or by checking the run. Apify API docs. LlamaIndex, MCP for AI agents and the JavaScript client: AI agents & MCP.
Data fields (21)
| Field | Type |
|---|---|
schemaVersion | string |
recordType | string |
postKey | string |
feedUrl | string |
publicationKey | string |
publicationName | string |
publicationUrl | string |
sourceGuid | string |
canonicalUrl | string |
title | string |
subtitle | string |
publishedAt | string |
updatedAt | string |
author | string |
categories | array |
audience | string |
contentMode | string |
contentAvailability | string |
enclosure | object |
retrievedAt | string |
sourceStatus | string |
Input settings (5)
The main fields of the input form. With the API you send them as JSON.
| Setting | Name in the API | Type | Example value |
|---|---|---|---|
| Full public feed URLs required | feedUrls | array | ["https://on.substack.com/feed"] |
| Posts per feed | maxPostsPerFeed | integer | 5 |
| Total unique rows | maxTotalPosts | integer | 5 |
| Published from (inclusive) | publishedFrom | string | |
| Published to (exclusive) | publishedTo | string |
Use cases with this tool
Each guide shows all the steps and the other tools.