Substack Scraper (public newsletter posts)
Extract posts from public Substack newsletters as clean JSON: title, date, author byline, likes, comments, word count and tags.
Extract posts from public Substack newsletters as clean JSON: title, date, author byline, likes, comments, word count and tags. Keyword search, date window and optional full text of free posts. No login, no API key. Pay per result, built for monitoring and AI pipelines.
$3.00 / 1,000 · Recently published - building its public record.
Run it on Apify → Free to try - runs bill per result on your own account.
What you give it
Every field is optional unless marked; sensible defaults do the rest.
| Field | Type | What it does | Default |
|---|---|---|---|
publications |
string | One or more Substack publications, separated by commas or spaces. Use the name ("astralcodexten"), the address ("astralcodexten.substack.com"), a custom domain ("https://www.lennysnewsletter.com") or an author link ("https://substack.com/@lenny"). A link to a single post works too: the whole publication is read, not only that post. | astralcodexten |
searchQuery |
string | Optional. Only return posts that match this term, using the publication's own archive search. Results come most relevant first, not by date, and the sort option below is ignored while a search is set. Leave empty to read the whole archive. | - |
sort |
string | new walks the archive newest-first. top lists the most popular posts first, which is a quick way to the best of a publication. A search term always lists by relevance. | new |
audience |
string | Keep all posts, only the free ones, or only the paid ones. Paid posts always come back as metadata and the public preview; their text is never read. | all |
postedAfter |
string | Only include posts published on or after this date. A newest-first walk stops as soon as it reaches older posts. Format: YYYY-MM-DD (for example 2026-09-08) or a full ISO timestamp such as 2026-09-08T14:30:00Z. Slash dates like 08/09/2026 are refused on purpose: they are read as September in Europe and August in the United States, and this actor will not guess which you meant. |
- |
postedBefore |
string | Only include posts published on or before this date. Format: YYYY-MM-DD (for example 2026-09-08) or a full ISO timestamp such as 2026-09-08T14:30:00Z. Slash dates like 08/09/2026 are refused on purpose: they are read as September in Europe and August in the United States, and this actor will not guess which you meant. |
- |
includeBody |
boolean | Adds a bodyText field with the full text of each free post. It needs one extra request per post, so a run takes noticeably longer. Paid posts never get a body, whatever this setting says. |
False |
maxItems |
integer | Stop after this many posts in total. Free-plan runs are capped at 5 items. The form starts at 25 for a cheap first run - that is the form's prefill, not a cap on your plan; clear it to use the default of 500. | 500 |
reportFailures |
boolean | When a run fails, send the developer the actor name and build, an error category, the error message (truncated) and a one-way hash of your input - so a bug can be fixed without anyone asking you to reproduce it. Your input itself, your results and your identity are never sent, and successful runs send nothing at all. Switch this off and the actor behaves exactly the same. | True |
What you get back
One JSON object per result, with exactly these fields - no seller identities, no contact data, ever.
| Field | Type | Meaning |
|---|---|---|
id |
integer | Substack's numeric id of the post. Stable, unique across publications, and the key a run uses to drop duplicates. |
title |
['string', 'null'] | Post title as published. |
subtitle |
['string', 'null'] | The subtitle printed under the title, when the author set one. |
slug |
string | The post's slug inside its publication. |
url |
string | Link to the post on the publication's own address (its custom domain when it has one). |
canonicalUrl |
['string', 'null'] | The canonical link Substack publishes for the post. Equals `url` unless the post was moved or imported. |
publicationName |
string | The publication's public name - the byline of the post. |
publicationUrl |
string | Address of the publication. |
authors |
array | Author names exactly as the byline prints them under the title (guest authors included). Names only: no handles, profile links, avatars or bios. |
postDate |
['string', 'null'] | When the post was published (ISO timestamp, UTC). |
audience |
['string', 'null'] | Who can read the whole post: `everyone` for free posts, `only_paid` for posts behind the paywall (other values pass through as Substack names them). |
type |
['string', 'null'] | Post type as Substack labels it: `newsletter`, `podcast`, `thread`, `video`. |
section |
['string', 'null'] | The section of the publication the post is filed under, when it uses sections. |
reactionCount |
['integer', 'null'] | Likes at scrape time. |
commentCount |
['integer', 'null'] | Number of comments at scrape time. The comments themselves are not collected. |
restackCount |
['integer', 'null'] | Times the post was restacked (shared inside Substack) at scrape time. |
wordCount |
['integer', 'null'] | Length of the full post in words, as Substack reports it. Also filled for paid posts, whose text you do not get. |
coverImage |
['string', 'null'] | Link to the post's cover image. |
tags |
array | Tags the author put on the post. |
excerpt |
['string', 'null'] | The short public preview Substack shows for the post (about 200 characters). Present for free and paid posts alike. |
bodyText |
['string', 'null'] | Full text of the post, one paragraph per block. Only present when `includeBody` is on, and only filled for free posts; paid posts come back with null. |
Health
Recently published - building its public record. Live record, failures included: status page.
Full documentation, examples and the AI-assistant prompt live on the store page.
Questions
Is scraping Substack legal?
This is not legal advice. The actor reads only what a logged-out visitor sees: a publication's public archive and, if you switch it on, the full text of posts that are free for everyone. It never logs in, never reads comments, notes, subscriber lists or email addresses, and never opens a paid post's text. Substack's terms of use contain a clause on automated access, so check them against your own use. Whether your use is allowed is yours to assess.
Which Substack publications can it read?
Any public Substack publication, whether it lives on a substack.com address or on a custom domain. Give the name (astralcodexten), a link to the publication or one of its posts, or an author's substack.com/@handle link. A name that is not a Substack publication is reported as such in the run status, charges nothing, and the rest of the run is delivered.
Can I get the text of paid posts?
No. A paid post comes back with its metadata (title, subtitle, date, authors, likes, comment count, word count, tags, cover image) and the short public preview Substack shows to everyone, never its text. The full text is only collected for posts that are free for everyone, and only when includeBody is on.
How much do 10,000 Substack posts cost?
$30.00, billed per delivered post at $3.00 per 1,000. That price covers the run's platform usage, and a run that fails or returns nothing bills nothing per result. Turning includeBody on does not change the price per post.
Does it collect commenters, subscribers or reader data?
No. Each row has the publication's name and the author names printed on the post as its byline, plus the post itself. Comments, commenter names, subscriber and reader data, author handles, bios and avatars are never read, and a test on every build fails if one ever appears in the output.
Can I search inside a publication or limit it to a date range?
Yes. Set searchQuery to get the posts that match a term, most relevant first, and postedAfter or postedBefore to keep a date window. A plain newest-first run with postedAfter stops by itself as soon as it reaches older posts, so a daily watch only reads what is new.