Substack API in JavaScript: RSS, Exports and Node (2026)
By the newsletterapi team · Published · Updated · 8 min read
The problem: “I want my Substack posts in a Node script or on my site, but there's no official SDK and calls from code get blocked.”
Short answer
Substack has no official JavaScript SDK or npm package. As of October 2026 the reliable routes in Node.js are your public RSS feed (rss-parser), your own export zip (JSZip plus csv-parse), and the read-only Publisher API with fetch if Substack has enabled it for you. The community substack-api package works on a session cookie and is no longer actively developed.
There is no npm install from Substack itself. If you want your newsletter in a Node.js script, a build step or a website, you have three official inputs and one community package. This tutorial gives tested code for each input, then explains what the package does and where it breaks. It is the JavaScript companion to our Substack API in Python guide. We ran the RSS code against a live Substack feed on Node 24 on 2026-10-24, and tested the export and Publisher API code against sample data shaped like the formats described below. For the bigger picture of what Substack offers developers, see our Substack API guide.
newsletterapi is an independent project, not affiliated with Substack. Nothing below depends on newsletterapi.
The problem
JavaScript developers tend to hit Substack from two directions: a server script that wants the data, and a website that wants to show it. Both run into walls. One developer asked: "How are you pulling from substack? Last I checked there wasn't a public API." (Reddit). Another, trying the undocumented endpoints, found that if you "call it from node or curl and it 403s no matter what cookies you send." (r/automation). On the website side, a company gave up on showing its posts: "Between the Substack CORS blocks and Substack blocking Azure addresses, where the corporate website is hosted, there isn't a decent way of share Substack post information with the corporate website." (r/Substack).
The fix is to start from inputs Substack supports, run them on your server or at build time, and serve your own copy to the browser.
Which JavaScript route fits my job?
| Job | Route | npm packages | Auth | Paid posts | Stability |
|---|---|---|---|---|---|
| Latest posts in a script or on a site | RSS | rss-parser | None | Cut at the paywall | Documented by Substack |
| Full archive, analysis, migration | Your export zip | jszip, csv-parse | Your dashboard | Yes | Your own file |
| Post stats and subscriber counts | Publisher API | none (built-in fetch) | Per-publication key, where enabled | Yes (metadata and stats) | Official, but docs not public |
| Reading profiles, posts and Notes as you | substack-api | substack-api | Your browser cookie | Your own | Undocumented interfaces |
How do I read a Substack RSS feed in Node.js?
Every publication has a feed at https://your.substack.com/feed, per Substack's help centre; custom domains use https://yourdomain.com/feed. Install rss-parser with npm install rss-parser. The script is an ES module (save it as rss.mjs, or set "type": "module" in package.json):
import Parser from "rss-parser";
const FEED_URL = process.env.FEED_URL ?? "https://yourname.substack.com/feed"; // or https://yourdomain.com/feed
const parser = new Parser({
timeout: 15000,
headers: { "User-Agent": "my-substack-reader/1.0 (+https://example.com)" },
});
export async function latestPosts(url, limit = 10) {
const feed = await parser.parseURL(url);
return feed.items.slice(0, limit).map((item) => ({
title: item.title ?? "",
url: item.link ?? "",
published: item.isoDate ?? item.pubDate ?? "",
summary: item.contentSnippet ?? "",
html: item["content:encoded"] ?? item.content ?? "",
}));
}
for (const post of await latestPosts(FEED_URL, 5)) {
console.log(post.published, "|", post.title, "|", post.url);
}
rss-parser exposes the full post body as content:encoded and adds a normalised isoDate, which is easier to sort than pubDate. The feed holds recent posts, not your whole archive, and paid posts stop at the paywall. Fetch it on a schedule (hourly is plenty) and cache the result rather than calling Substack on every page view.
Showing posts on a website: run this at build time (for example in a Next.js or Astro data loader) or in a small server route that caches the JSON, then render from your own copy. That avoids the browser CORS problem from the quote above. Our embed guide covers no-code options, and the RSS feed guide explains what the feed contains.
How do I read my Substack export zip in Node.js?
Substack lets every writer download "your posts, subscriber list, and related statistics" from Settings, Exports, "Create new export"; you get an email when the zip is ready (help). Substack doesn't document the file layout. A third-party Obsidian importer describes it as a posts.csv with a post_id column, plus a posts/ folder where a row with ID 123.first-post matches posts/123.first-post.html.
Node has no built-in zip reader, so this uses JSZip and csv-parse (npm install jszip csv-parse). It finds posts.csv wherever it sits, so a zip wrapped in a folder still works:
import { readFile } from "node:fs/promises";
import path from "node:path";
import JSZip from "jszip";
import { parse } from "csv-parse/sync";
const htmlToText = (html) =>
html.replace(/<(script|style)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
export async function* readExport(zipPath) {
const zip = await JSZip.loadAsync(await readFile(zipPath));
const csvName = Object.keys(zip.files).find((n) => path.posix.basename(n) === "posts.csv");
if (!csvName) throw new Error("posts.csv not found in the zip");
const folder = path.posix.dirname(csvName); // "." when the zip isn't wrapped in a folder
const rows = parse(await zip.file(csvName).async("string"), { columns: true, bom: true, skip_empty_lines: true });
for (const row of rows) {
const file = zip.file(path.posix.join(folder, "posts", `${row.post_id}.html`));
yield { row, html: file ? await file.async("string") : "" };
}
}
for await (const { row, html } of readExport(process.argv[2] ?? "substack-export.zip")) {
const words = htmlToText(html).split(" ").filter(Boolean).length;
console.log(row.post_date ?? "", "|", row.title ?? "", "|", words, "words");
}
Run it with node export.mjs substack-export.zip. Read every column except post_id defensively (row.title ?? ""), because the column set isn't documented and can change. A row without a matching HTML file returns an empty body instead of throwing. The regex text extraction is fine for word counts; use a real HTML parser if you need clean text. The zip also holds subscriber data, so keep it out of public repos and buckets. For a scheduled, off-site copy, see how to back up your Substack.
How do I call the Substack Publisher API with fetch?
If Substack has enabled the read-only Publisher API for your publication, Node 18 and later can call it with the built-in fetch, so no HTTP library is needed. Substack hasn't published public docs. The paths and fields below come from the open-source substack-publisher-mcp wrapper and are self-reported by its author: the raw key goes in an authorization header with no Bearer prefix, /posts pages with a next cursor that is null on the last page, and /subscribers/counts returns daily rows, newest first. Read Substack API key: Publisher API and Developer API to check whether you have a key.
const BASE = "https://publisher-api.substack.com/v1";
const KEY = process.env.SUBSTACK_PUBLISHER_API_KEY; // never hard-code or commit it
if (!KEY) throw new Error("Set SUBSTACK_PUBLISHER_API_KEY");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function get(path, params = {}) {
const url = new URL(BASE + path);
for (const [k, v] of Object.entries(params)) if (v != null) url.searchParams.set(k, String(v));
const res = await fetch(url, {
headers: { authorization: KEY, accept: "application/json" }, // raw key, no "Bearer"
signal: AbortSignal.timeout(30_000),
});
if (res.status === 429) throw new Error("Rate limited; wait and retry later");
if (!res.ok) throw new Error(`${res.status} ${res.statusText} on ${path}`);
return res.json();
}
async function* allPosts(pageSize = 50) {
let next = null;
do {
const page = await get("/posts", { maxResults: pageSize, next });
yield* page.posts;
next = page.next; // null on the last page
} while (next);
}
for await (const post of allPosts()) {
const stats = await get(`/posts/${encodeURIComponent(post.urlSlug)}/stats`);
console.log(post.postDate.slice(0, 10), post.title, "opens:", stats.opens, "new paid:", stats.new_paid_subscriptions);
await sleep(250); // rate limits aren't published; be gentle
}
const counts = await get("/subscribers/counts", { startDate: "2026-10-01" }); // array, newest first
for (const day of counts.slice(0, 7)) {
const free = day.total_email_subscribers - day.paid_subscribers;
console.log(day.date, "total:", day.total_email_subscribers, "paid:", day.paid_subscribers, "free:", free);
}
The field names (opens, new_paid_subscriptions, total_email_subscribers and so on) are the wrapper author's, labelled self-reported; check them against your own responses. The API is read-only and cannot list subscribers. Run this server-side only: the key gives access to your publication's stats and to single-subscriber lookups, so it must never reach a browser bundle.
What about the substack-api npm package?
The most-installed community package is substack-api, with 5,637 downloads in the month to 2026-10-22 (npm downloads API). It is a TypeScript client for profiles, posts, Notes and comments. Two things to know before you depend on it:
- It signs in with your browser session. The quick start takes a session cookie value copied from a logged-in browser as its token, not an official key.
- It is no longer maintained. The README now opens with: "This library is no longer actively developed." Its author points to a successor, a self-hosted Python gateway that also runs on your credentials.
The risks are the same as for Python libraries:
- Breakage. Undocumented endpoints have no version contract, which is why scripts start returning 403.
- Account risk. Substack's Terms of Use forbid anything that "Jeopardizes the security of your Substack account", "such as allowing someone else to log into Substack as you". Never paste your session into a hosted service or a shared repo.
- Scraping rules. The same terms forbid crawling or scraping "any page, data, or portion of Substack" and copying "any significant portion of the content on Substack". Reading other people's newsletters in bulk is on the wrong side of that line; our Substack scraper guide lists the legal alternatives.
If you only need your own publication, RSS plus your export covers reading with no login. If you are already blocked, see the Substack API 403 error.
Can I publish to Substack from JavaScript?
Not through an official route. The Publisher API is read-only, and Substack's own MCP server help page says it "cannot publish posts, send Notes, or modify your account" (Substack help). Generate drafts in your own code, then paste them into the Substack editor and use its scheduler; publishing from Markdown covers the cleanest hand-off.
Is there a simpler way to get my archive as JSON?
If you'd rather not maintain feed and zip parsers, the newsletterapi Posts API serves your archive as clean JSON, Markdown or HTML, built from your public feed and the export you upload, with search and pagination. For websites, embeds gives you a cached, CORS-enabled JSON feed so the browser never calls Substack, and the same archive is available to AI agents through our MCP server. To get access, see pricing or email hello@newsletterapi.io.
Questions people ask
- Is there an official Substack npm package?
- No. Substack publishes no SDK. The substack-api package on npm is a community project that signs in with a cookie from your browser, and its README now says it is no longer actively developed.
- Can I fetch my Substack feed from the browser?
- Writers report CORS blocks and blocked cloud IP ranges when calling Substack from a website. Fetch the feed on your server or at build time, cache it, and serve your own JSON to the page.
- How do I get paid posts into Node?
- Use your own export from Settings, Exports. RSS cuts paid posts at the paywall; the export zip contains your full archive as posts.csv plus one HTML file per post.
- Can I publish to Substack from JavaScript?
- Not through an official API. The Publisher API is read-only, and Substack's official MCP server says it cannot publish posts or send Notes. Draft in your own tools and paste into the editor, or use Buffer for Notes.
Sources
- Substack help: Is there an RSS feed for my publication? (accessed 2026-10-24)
- Substack help: How do I export my posts? (accessed 2026-10-24)
- rss-parser on GitHub (accessed 2026-10-24)
- JSZip documentation (accessed 2026-10-24)
- csv-parse documentation (accessed 2026-10-24)
- Node.js docs: global fetch (accessed 2026-10-24)
- Substack Export Import plugin for Obsidian (describes posts.csv and posts/ layout) (accessed 2026-10-24)
- dkships/substack-publisher-mcp (wrapper around the Publisher API) (accessed 2026-10-24)
- Substack help: How to connect Substack to your AI Assistant (accessed 2026-10-24)
- GitHub: jakub-k-slys/substack-api (README) (accessed 2026-10-24)
- npm downloads API: substack-api, last month (accessed 2026-10-24)
- Substack Terms of Use (accessed 2026-10-24)
- r/automation: calling Substack from Node returns 403 (accessed 2026-10-21)
- r/Substack: CORS blocks and blocked Azure addresses (accessed 2026-10-21)
- r/Newsletters: no public API (accessed 2026-10-21)
Related guides
Guide · Developers & AI
Substack API: Official Options, Code and Libraries (2026)
The Substack API explained: every official option as of October 2026, Python and JavaScript code to read your posts, and the unofficial libraries compared.
12 min read · Updated 2026-10-24
How-to · Developers & AI
Substack API in Python: RSS, Exports and Libraries (2026)
Read your Substack posts in Python: RSS with feedparser, your export zip, the Publisher API with requests, and the risks of python-substack and substack_api.
6 min read · Updated 2026-10-22
Comparison · Developers & AI
Newsletter Platforms With an API: 2026 Comparison
Which newsletter platforms have an API? Substack, beehiiv, Kit, Ghost, Buttondown, Mailchimp and MailerLite compared on read/write, webhooks, plans and MCP.
6 min read · Updated 2026-10-21