Vexaloom Labs

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.

FieldTypeRequiredDescription
modestringoptionalWhat to scrape. substack.com-hosted inputs are reported as blocked_egress.
publicationsarrayoptional(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.
postUrlsarrayoptional(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
FieldTypeRequiredDescription
modestringoptionalWhat to scrape. substack.com-hosted inputs are reported as blocked_egress.
publicationsarrayoptional(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.
postUrlsarrayoptional(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.
sortBystringoptional(mode=publicationPosts) Newest first or all-time most popular first.
publishedAfterstringoptionalKeep posts published on or after this date (`YYYY-MM-DD`).
publishedBeforestringoptionalKeep posts published on or before this date (`YYYY-MM-DD`).
containsKeywordstringoptional(mode=publicationPosts) Keep posts whose title, teaser or text contains this keyword. Uses Substack's publication search when possible.
tagsAnyOfarrayoptionalKeep posts carrying at least one of these tags (case-insensitive).
audienceFilterstringoptionalKeep all posts, only free posts, or only paid posts.
postTypestringoptionalKeep all post types or a single type.
mediaFilterstringoptionalKeep all posts, audio posts, video posts, or text-only posts.
bodyFormatstringoptionalBody fields to include. `none` is fastest; `all` adds HTML, plain text and Markdown.
includePaidPreviewbooleanoptionalFor paid posts, include the public preview body and flag it as a preview. Turn off for faster metadata-only runs.
includeCommentsbooleanoptionalInclude public comment threads for free posts. Paid-post comments are members-only.
maxCommentsPerPostintegeroptionalCap on comments per post, including replies. Set to 0 for no limit.
includePublicationInfobooleanoptionalAdd publication details such as pricing plans, sections, language and payment status.
maxItemsintegeroptionalHard cap on emitted records.
maxPostsPerPublicationintegeroptionalOptional 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.

FieldTypeRequiredDescription
postsstringoptionalDataset 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.com and share links produce a blocked_egress row 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 link
  • publishedAt, publishedAtRaw, updatedAt — ISO date plus the original source value
  • author, authors[] — primary byline and every byline with name, handle, bio, photo and Twitter
  • publication, publicationUrl, publicationInfo — newsletter name/link, plus plans and sections when enabled
  • audience, isPaid, contentAccess, contentIsPreview, shouldSendFreePreview, freeUnlockRequired — access state
  • bodyHtml, bodyText, bodyMarkdown — body in the requested formats; paid bodies are previews only
  • summary, truncatedBodyText, description — plain excerpt, source teaser and source description
  • wordCount (words in the delivered body), articleWordCount (full article count from the source), readingTimeMinutes
  • language, postType — language code and newsletter / podcast / thread
  • coverImage, audioUrl, podcastUrl, podcastDurationSec — image and audio
  • videoUrl, videoThumbnailUrl, videoDurationSec, videoWidth, videoHeight, videoIsPreview — video details
  • tags, tagSlugs, categories, sectionId, sectionName — taxonomy; categories is kept for older integrations
  • reactionCount, reactions, commentCount, childCommentCount, restacks — engagement
  • comments[] — nested public comments when enabled, with author, date, reactions and replies
  • dataSource, 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.

Open the step-by-step Python guide →

Source build izi31evru0SmgKMn3. Documentation is regenerated from the default published build; private drafts are not included.