Substack Publication Post Exporter
Export posts from any Substack newsletter: title, author, publish date, body, categories and cover image, filtered by category, date or keyword. No login or proxy needed.
Input schema
Input fields, types, required flags, descriptions, defaults, and prefills come from the published definition. Prefill examples are distinct from defaults.
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | optional | What to scrape. substack.com-hosted inputs are reported as blocked_egress. |
publications | array | optional | (mode=publicationPosts) Custom-domain publication URLs or RSS-capable sites, e.g. `https://www.slowboring.com` or `https://platformer.news`. Bare subdomains and substack.com links are blocked from this actor's cloud network. |
postUrls | array | optional | (mode=postUrls) Custom-domain post URLs, e.g. `https://www.noahpinion.blog/p/the-internet-breaks`. substack.com share links are reported as blocked_egress. |
Show all input fields
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | optional | What to scrape. substack.com-hosted inputs are reported as blocked_egress. |
publications | array | optional | (mode=publicationPosts) Custom-domain publication URLs or RSS-capable sites, e.g. `https://www.slowboring.com` or `https://platformer.news`. Bare subdomains and substack.com links are blocked from this actor's cloud network. |
postUrls | array | optional | (mode=postUrls) Custom-domain post URLs, e.g. `https://www.noahpinion.blog/p/the-internet-breaks`. substack.com share links are reported as blocked_egress. |
sortBy | string | optional | (mode=publicationPosts) Newest first or all-time most popular first. |
publishedAfter | string | optional | Keep posts published on or after this date (`YYYY-MM-DD`). |
publishedBefore | string | optional | Keep posts published on or before this date (`YYYY-MM-DD`). |
containsKeyword | string | optional | (mode=publicationPosts) Keep posts whose title, teaser or text contains this keyword. Uses Substack's publication search when possible. |
tagsAnyOf | array | optional | Keep posts carrying at least one of these tags (case-insensitive). |
audienceFilter | string | optional | Keep all posts, only free posts, or only paid posts. |
postType | string | optional | Keep all post types or a single type. |
mediaFilter | string | optional | Keep all posts, audio posts, video posts, or text-only posts. |
bodyFormat | string | optional | Body fields to include. `none` is fastest; `all` adds HTML, plain text and Markdown. |
includePaidPreview | boolean | optional | For paid posts, include the public preview body and flag it as a preview. Turn off for faster metadata-only runs. |
includeComments | boolean | optional | Include public comment threads for free posts. Paid-post comments are members-only. |
maxCommentsPerPost | integer | optional | Cap on comments per post, including replies. Set to 0 for no limit. |
includePublicationInfo | boolean | optional | Add publication details such as pricing plans, sections, language and payment status. |
maxItems | integer | optional | Hard cap on emitted records. |
maxPostsPerPublication | integer | optional | Optional per-publication cap. Set to 0 for no per-publication limit. |
Download the published input schema
View complete JSON
{
"description": "Export custom-domain Substack publications and post URLs, with an RSS fallback for non-Substack sites.",
"properties": {
"audienceFilter": {
"default": "all",
"description": "Keep all posts, only free posts, or only paid posts.",
"editor": "select",
"enum": [
"all",
"free",
"paid"
],
"enumTitles": [
"All posts",
"Free posts only",
"Paid posts only"
],
"prefill": "all",
"title": "Free or paid",
"type": "string"
},
"bodyFormat": {
"default": "all",
"description": "Body fields to include. `none` is fastest; `all` adds HTML, plain text and Markdown.",
"editor": "select",
"enum": [
"none",
"html",
"text",
"markdown",
"all"
],
"enumTitles": [
"No body",
"HTML",
"Plain text",
"Markdown",
"HTML + text + Markdown"
],
"prefill": "none",
"title": "Post body",
"type": "string"
},
"containsKeyword": {
"description": "(mode=publicationPosts) Keep posts whose title, teaser or text contains this keyword. Uses Substack's publication search when possible.",
"editor": "textfield",
"title": "Contains keyword",
"type": "string"
},
"includeComments": {
"default": false,
"description": "Include public comment threads for free posts. Paid-post comments are members-only.",
"prefill": false,
"title": "Include comments",
"type": "boolean"
},
"includePaidPreview": {
"default": true,
"description": "For paid posts, include the public preview body and flag it as a preview. Turn off for faster metadata-only runs.",
"prefill": false,
"title": "Include paid post preview",
"type": "boolean"
},
"includePublicationInfo": {
"default": true,
"description": "Add publication details such as pricing plans, sections, language and payment status.",
"prefill": false,
"title": "Include publication info",
"type": "boolean"
},
"maxCommentsPerPost": {
"default": 20,
"description": "Cap on comments per post, including replies. Set to 0 for no limit.",
"maximum": 5000,
"minimum": 0,
"prefill": 20,
"title": "Max comments per post",
"type": "integer"
},
"maxItems": {
"default": 50,
"description": "Hard cap on emitted records.",
"maximum": 10000,
"minimum": 1,
"prefill": 3,
"title": "Max items",
"type": "integer"
},
"maxPostsPerPublication": {
"default": 0,
"description": "Optional per-publication cap. Set to 0 for no per-publication limit.",
"maximum": 10000,
"minimum": 0,
"prefill": 3,
"title": "Max posts per publication",
"type": "integer"
},
"mediaFilter": {
"default": "any",
"description": "Keep all posts, audio posts, video posts, or text-only posts.",
"editor": "select",
"enum": [
"any",
"audio",
"video",
"text"
],
"enumTitles": [
"Any media",
"With audio",
"With video",
"Text only"
],
"prefill": "any",
"title": "Media",
"type": "string"
},
"mode": {
"default": "publicationPosts",
"description": "What to scrape. substack.com-hosted inputs are reported as blocked_egress.",
"editor": "select",
"enum": [
"publicationPosts",
"postUrls"
],
"enumTitles": [
"Publication posts (archive)",
"Specific post URLs"
],
"prefill": "publicationPosts",
"title": "Mode",
"type": "string"
},
"postType": {
"default": "all",
"description": "Keep all post types or a single type.",
"editor": "select",
"enum": [
"all",
"newsletter",
"podcast",
"thread"
],
"enumTitles": [
"All types",
"Newsletter",
"Podcast",
"Thread"
],
"prefill": "all",
"title": "Post type",
"type": "string"
},
"postUrls": {
"default": [],
"description": "(mode=postUrls) Custom-domain post URLs, e.g. `https://www.noahpinion.blog/p/the-internet-breaks`. substack.com share links are reported as blocked_egress.",
"editor": "stringList",
"prefill": [],
"title": "Post URLs",
"type": "array"
},
"publications": {
"default": [
"platformer.news"
],
"description": "(mode=publicationPosts) Custom-domain publication URLs or RSS-capable sites, e.g. `https://www.slowboring.com` or `https://platformer.news`. Bare subdomains and substack.com links are blocked from this actor's cloud network.",
"editor": "stringList",
"prefill": [
"platformer.news"
],
"title": "Publications",
"type": "array"
},
"publishedAfter": {
"description": "Keep posts published on or after this date (`YYYY-MM-DD`).",
"editor": "textfield",
"title": "Published after",
"type": "string"
},
"publishedBefore": {
"description": "Keep posts published on or before this date (`YYYY-MM-DD`).",
"editor": "textfield",
"title": "Published before",
"type": "string"
},
"sortBy": {
"default": "newest",
"description": "(mode=publicationPosts) Newest first or all-time most popular first.",
"editor": "select",
"enum": [
"newest",
"top"
],
"enumTitles": [
"Newest first",
"Most popular"
],
"prefill": "newest",
"title": "Sort order",
"type": "string"
},
"tagsAnyOf": {
"default": [],
"description": "Keep posts carrying at least one of these tags (case-insensitive).",
"editor": "stringList",
"prefill": [],
"title": "Tags",
"type": "array"
}
},
"schemaVersion": 1,
"title": "Substack Scraper Input",
"type": "object"
}
Output schema
Published result links and storage locations. This schema is not an exhaustive contract for every field in a dataset record.
| Field | Type | Required | Description |
|---|---|---|---|
posts | string | optional | Dataset containing scraped Substack posts and author profiles. |
Download the published output schema
View complete JSON
{
"actorOutputSchemaVersion": 1,
"description": "One record per post (metadata, free body HTML/text/Markdown, flagged paid teaser, media, engagement, comments) or one blocked_egress status row per substack.com-hosted input.",
"properties": {
"posts": {
"description": "Dataset containing scraped Substack posts and author profiles.",
"template": "{{links.apiDefaultDatasetUrl}}/items",
"title": "Substack posts and author profiles",
"type": "string"
}
},
"title": "Substack Publication Post Exporter Output"
}
Dataset schema
Published dataset views and any declared field definitions. A view can select only a subset of the returned record fields.
Download the published dataset schema
View complete JSON
{
"actorSpecification": 1,
"views": {
"overview": {
"display": {
"component": "table",
"properties": {}
},
"title": "Substack posts",
"transformation": {
"fields": [
"title",
"recordType",
"publication",
"author",
"publishedAt",
"audience",
"commentCount",
"url"
]
}
}
}
}
Key-value schema
Published key-value store definitions, when provided by this build.
This definition is not present in the default published build. See the published README for available documentation.
Published README
Read the full published documentation
Substack Publication Post Exporter
Turn a Substack publication's public archive into a clean dataset, or fetch individual posts you already know. Custom-domain newsletters work directly; publications that still live on *.substack.com are reported as a clear blocked_egress status row instead of a misleading empty result. Free posts arrive with full HTML, readable text and Markdown; paid posts arrive with metadata and a clearly flagged public teaser. No account, login or proxy setup.
What this actor does
- Two modes: export a publication's archive (
publicationPosts) or fetch specific post URLs (postUrls). - Full archive depth: walk a custom-domain publication from its newest post back through its history, 50 posts at a time.
- Free posts in full: HTML, plain text and clean Markdown for every free post.
- Paid posts, honestly: title, subtitle, author, date, cover, word count, reactions, comments count and the public teaser. The public preview body is optional and flagged with
contentIsPreview. - Comments: nested public comment threads for free posts.
- Publication details: name, domain, language, payment status, subscription plans and sections.
- Filters: date range, keyword, tags, free/paid, post type and audio/video presence.
- RSS fallback: non-Substack sites such as Platformer are read through their public feed (up to 20 recent items).
- Blocked inputs are visible:
substack.com,*.substack.comand share links produce ablocked_egressrow that explains the limitation and the workaround. - Empty fields are omitted: a post without a subtitle, tag or video simply has no such key.
Output per post
id,slug,title,subtitle,url,guid— identity and canonical linkpublishedAt,publishedAtRaw,updatedAt— ISO date plus the original source valueauthor,authors[]— primary byline and every byline with name, handle, bio, photo and Twitterpublication,publicationUrl,publicationInfo— newsletter name/link, plus plans and sections when enabledaudience,isPaid,contentAccess,contentIsPreview,shouldSendFreePreview,freeUnlockRequired— access statebodyHtml,bodyText,bodyMarkdown— body in the requested formats; paid bodies are previews onlysummary,truncatedBodyText,description— plain excerpt, source teaser and source descriptionwordCount(words in the delivered body),articleWordCount(full article count from the source),readingTimeMinuteslanguage,postType— language code andnewsletter/podcast/threadcoverImage,audioUrl,podcastUrl,podcastDurationSec— image and audiovideoUrl,videoThumbnailUrl,videoDurationSec,videoWidth,videoHeight,videoIsPreview— video detailstags,tagSlugs,categories,sectionId,sectionName— taxonomy;categoriesis kept for older integrationsreactionCount,reactions,commentCount,childCommentCount,restacks— engagementcomments[]— nested public comments when enabled, with author, date, reactions and repliesdataSource,recordType(post),scrapedAt
Blocked input row
When an input is hosted on substack.com (a bare subdomain, a substack.com/@handle, or a share link), the actor emits one row with recordType: "blocked_egress", the original input, host, a plain-language message and a workaround. This is a limitation of this actor's cloud network, not an empty publication. Use the publication's custom domain or an RSS-capable site instead.
Input
| Field | Type | Default | Description |
|---|---|---|---|
mode |
string | publicationPosts |
publicationPosts or postUrls |
publications |
array | ["platformer.news"] |
Custom-domain publication URLs or RSS-capable sites (mode=publicationPosts) |
postUrls |
array | – | Custom-domain post URLs (mode=postUrls) |
sortBy |
string | newest |
newest or top for publication posts |
publishedAfter |
string | – | Keep posts on or after YYYY-MM-DD |
publishedBefore |
string | – | Keep posts on or before YYYY-MM-DD |
containsKeyword |
string | – | Keep posts whose title, teaser or text contains this keyword |
tagsAnyOf |
array | – | Keep posts with at least one matching tag |
audienceFilter |
string | all |
all, free or paid |
postType |
string | all |
all, newsletter, podcast or thread |
mediaFilter |
string | any |
any, audio, video or text |
bodyFormat |
string | all |
none, html, text, markdown or all |
includePaidPreview |
boolean | true |
Include the flagged public preview of paid posts |
includeComments |
boolean | false |
Include public comment threads for free posts |
maxCommentsPerPost |
integer | 20 |
Comment cap per post, replies included; 0 means no limit |
includePublicationInfo |
boolean | true |
Add plans, sections and publication metadata |
maxItems |
integer | 50 |
Hard cap on records (1–10,000) |
maxPostsPerPublication |
integer | 0 |
Per-publication cap; 0 means no per-publication limit |
Example: a custom-domain publication with full bodies
{
"mode": "publicationPosts",
"publications": ["https://www.slowboring.com"],
"maxItems": 25,
"bodyFormat": "all"
}
Example: paid and free posts in a date range
{
"mode": "publicationPosts",
"publications": ["https://www.noahpinion.blog"],
"publishedAfter": "2026-01-01",
"audienceFilter": "all",
"includePaidPreview": true,
"maxItems": 100
}
Example: specific custom-domain post URLs
{
"mode": "postUrls",
"postUrls": [
"https://www.noahpinion.blog/p/the-internet-breaks",
"https://www.slowboring.com/p/destroying-womens-sports-in-order"
],
"bodyFormat": "markdown",
"includeComments": true
}
Example: RSS fallback for a site that left Substack
{
"mode": "publicationPosts",
"publications": ["platformer.news"],
"maxItems": 10,
"bodyFormat": "text"
}
Use cases
- Newsletter monitoring — follow competitor or industry publications without checking each site.
- Research and NLP — export free-post text or Markdown for topic models, search indexes and RAG stores.
- Content archives — keep a searchable copy of a public archive and its free comments.
- Media analysis — track themes, posting cadence, reactions and discussion across newsletters.
- Feed consolidation — merge several custom-domain publications, or an RSS site, into one dataset.
FAQ
Do I need a Substack account, cookies or a proxy? No. The actor reads public custom-domain pages and posts. There is no login step and no proxy setup.
Which publications work?
Any publication served from its own custom domain, plus non-Substack sites with a public RSS feed. Bare *.substack.com addresses and substack.com share links do not work from this actor's cloud network.
What is a blocked_egress row?
It is a status record that tells you the input is hosted on substack.com and cannot be read from this actor's cloud network. It includes the original input, the host, a plain-language message and a workaround. It is not an empty publication.
Can I download paid posts in full?
No. Paid posts are protected. The actor returns their metadata, the public teaser and, when includePaidPreview is on, the public preview body with contentIsPreview: true. It never bypasses a paywall.
How far back does the archive go? As far as a custom-domain publication's public archive goes — not just the latest page. The RSS fallback returns up to 20 recent items.
Why are comments missing on some posts? Public comment threads are available for free posts. Comments on paid posts are members-only, so the actor returns the comment count but not the thread.
Can I get Markdown for AI or RAG pipelines?
Yes. Choose markdown or all in bodyFormat. Free posts get clean Markdown with subscribe buttons, share widgets and image wrappers removed. Paid posts get the flagged public preview only.
Why is a field absent from some records?
Empty values are omitted rather than written as null. A post without a subtitle, tag, video or public subscriber count simply does not include that key.
Do custom domains work the same as a .substack.com address?
Use the custom domain itself, such as https://www.slowboring.com or https://www.noahpinion.blog. The .substack.com address is reported as blocked_egress.
Is this affiliated with Substack? No. This is an independent third-party actor that reads publicly available newsletter data. It is not affiliated with or endorsed by Substack.
Python quickstart
Use a schema-derived input and the Python standard library to start a run. An Apify account and token are required for execution.
Source build izi31evru0SmgKMn3. Documentation is regenerated from the default published build; private drafts are not included.