Substack API in Python: RSS, Exports and Libraries (2026)
By the newsletterapi team · Published · Updated · 5 min read
The problem: “I want to pull my Substack posts into a Python script, but there's no official SDK and the libraries keep breaking.”
Short answer
Substack has no official Python SDK. As of October 2026 the reliable routes are your public RSS feed (feedparser), your own export zip (the standard library's zipfile and csv), and the read-only Publisher API with requests if it's enabled for you. Unofficial libraries such as python-substack and substack_api work, but rely on undocumented endpoints.
There is no pip install substack from Substack itself. If you want your newsletter in a Python script, you have three official inputs and a handful of community libraries. This tutorial gives tested code for each official input, then explains what the libraries do and where they break. We ran the RSS code against a live Substack feed on 2026-10-22, 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
Developers go looking for an SDK and find questions instead. One builder in r/n8n wrote: "Problem: Substack doesn't provide a public API on my plan." (Reddit). Another asked a fellow developer: "How are you pulling from substack? Last I checked there wasn't a public API." (Reddit). The libraries that fill the gap sometimes fail at sign-in, as in this python-substack report: "APIError(code=401): Please complete the captcha to continue" (issue #27).
The fix is to start from inputs Substack actually supports, then add libraries only where you accept the risk.
Which Python route fits my job?
| Job | Route | Auth | Paid posts | Stability |
|---|---|---|---|---|
| Latest posts on a website or in a script | RSS + feedparser | None | Cut at the paywall | Documented by Substack |
| Full archive, analysis, migration | Your export zip + standard library | Your dashboard | Yes | Your own file |
| Post stats and subscriber counts | Publisher API + requests | Per-publication key, where enabled | Yes (metadata and stats) | Official, but docs not public |
| Drafting and publishing | python-substack | Your login | n/a | Undocumented interfaces |
| Reading public newsletters | substack_api | None (cookies for paywalled) | Only your own or paid-for | Undocumented interfaces |
How do I read my Substack RSS feed in Python?
Every publication has a feed at https://your.substack.com/feed, per Substack's help centre; custom domains use https://yourdomain.com/feed. Install feedparser with pip install feedparser:
import feedparser
FEED_URL = "https://yourname.substack.com/feed" # or https://yourdomain.com/feed
def latest_posts(url, limit=10):
feed = feedparser.parse(url)
if feed.bozo and not feed.entries:
raise RuntimeError(f"Could not read feed: {feed.bozo_exception}")
posts = []
for entry in feed.entries[:limit]:
content = entry.get("content", [{}])[0].get("value", "")
posts.append({
"title": entry.get("title", ""),
"url": entry.get("link", ""),
"published": entry.get("published", ""),
"summary": entry.get("summary", ""),
"html": content,
})
return posts
for post in latest_posts(FEED_URL, limit=5):
print(post["published"], "|", post["title"], "|", post["url"])
The feed holds recent posts, not your whole archive, and paid posts stop at the paywall. Fetch it on a schedule (hourly is plenty) rather than in a tight loop. More detail is in our Substack RSS feed guide.
How do I read my Substack export zip in Python?
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. The script below follows that layout using only the standard library, and tolerates the zip being wrapped in a folder:
import csv
import io
import zipfile
from html.parser import HTMLParser
from pathlib import PurePosixPath
class _Text(HTMLParser):
def __init__(self):
super().__init__()
self.parts = []
def handle_data(self, data):
self.parts.append(data)
def html_to_text(html):
parser = _Text()
parser.feed(html)
return " ".join(" ".join(parser.parts).split())
def read_export(zip_path):
"""Yield (row, html) for each post in a Substack export zip."""
with zipfile.ZipFile(zip_path) as z:
names = set(z.namelist())
csv_name = next(n for n in names if PurePosixPath(n).name == "posts.csv")
folder = PurePosixPath(csv_name).parent
with z.open(csv_name) as f:
rows = list(csv.DictReader(io.TextIOWrapper(f, encoding="utf-8-sig")))
for row in rows:
html_name = str(folder / "posts" / f"{row['post_id']}.html")
html = z.read(html_name).decode("utf-8") if html_name in names else ""
yield row, html
for row, html in read_export("substack-export.zip"):
words = len(html_to_text(html).split())
print(row.get("post_date", ""), "|", row.get("title", ""), "|", words, "words")
Use row.get() for every column except post_id, because the column set isn't documented and can change. If a row has no matching HTML file, the script returns an empty body instead of failing. The zip also contains subscriber data, so keep it out of public repos. For a scheduled, off-site copy, see how to back up your Substack.
How do I call the Publisher API from Python?
If Substack has enabled the read-only Publisher API for your publication, you can call it with requests. Substack hasn't published public docs; the paths and fields below come from the open-source substack-publisher-mcp wrapper (self-reported by its author). The key goes in a plain authorization header. Read Substack API key: Publisher API and Developer API for how to check whether you have one.
import os
import requests
BASE = "https://publisher-api.substack.com/v1"
KEY = os.environ["SUBSTACK_PUBLISHER_API_KEY"] # never hard-code or commit it
session = requests.Session()
session.headers.update({"authorization": KEY, "accept": "application/json"})
def get(path, **params):
r = session.get(f"{BASE}{path}", params=params, timeout=30)
if r.status_code == 429:
raise RuntimeError("Rate limited; wait and retry later")
r.raise_for_status()
return r.json()
def all_posts(page_size=50):
cursor = None
while True:
params = {"maxResults": page_size}
if cursor:
params["next"] = cursor
page = get("/posts", **params)
yield from page["posts"]
cursor = page.get("next")
if not cursor:
break
for post in all_posts():
stats = get(f"/posts/{post['urlSlug']}/stats")
print(post["postDate"][:10], post["title"],
"opens:", stats.get("opens"),
"new paid:", stats.get("new_paid_subscriptions"))
The API is read-only and cannot list subscribers; /subscribers/counts gives daily totals by type and /subscribers/{email} checks one address. Rate limits aren't public, so add a pause between stats calls for large archives.
What about python-substack and substack_api?
Two community libraries dominate PyPI. Figures checked on 2026-10-22:
| Library | Downloads last month | What it does | How it signs in |
|---|---|---|---|
| python-substack | 8,994 (pypistats) | Markdown to drafts, scheduling and publishing; CLI and MCP | Your email and password, or browser cookies |
substack_api (substack-api on PyPI) |
3,111 (pypistats) | Reads newsletters, posts, authors and recommendations | None; your cookies for paywalled posts |
Both are honest about their limits. python-substack's README says it "uses undocumented Substack interfaces that may change without notice". substack_api's README warns that "APIs may change without notice", that "Rate limiting may be enforced by Substack", and that "Users are responsible for complying with Substack's terms of service".
The risks to weigh before you depend on them:
- Breakage. Undocumented endpoints have no version contract.
- Account risk. Substack's Terms of Use forbid uses that jeopardise your account, "such as allowing someone else to log into Substack as you". Never put your password or session into a hosted service.
- Scraping rules. The same terms forbid using automated means to crawl or scrape "any page, data, or portion of Substack", and copying or storing "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 explains the legal alternatives.
If you only need to read your own publication, RSS plus your export covers it with no login. If you get blocked anyway, see the Substack API 403 error.
Is there a simpler way to get my archive as JSON?
If you'd rather not maintain 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. 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 Python library?
- No. Substack publishes no SDK. python-substack and substack_api are community projects, and both say in their READMEs that they are not affiliated with Substack.
- How do I get paid posts into Python?
- Use your own export from Settings, Exports. The RSS feed cuts paid posts at the paywall; the export zip contains your full archive.
- Can I publish to Substack from Python?
- Not through an official API. python-substack can create drafts and publish by signing in as you, which relies on undocumented interfaces and carries terms-of-service and account risk.
- Why does my Python script get 403 or captcha errors?
- Substack's web endpoints are not an API contract. Automated sign-ins can hit captchas and requests from servers can be blocked. Use RSS, your export or an official key instead.
Sources
- Substack help: Is there an RSS feed for my publication? (accessed 2026-10-22)
- Substack help: How do I export my posts? (accessed 2026-10-22)
- feedparser documentation (accessed 2026-10-22)
- Substack Export Import plugin for Obsidian (describes posts.csv and posts/ layout) (accessed 2026-10-22)
- dkships/substack-publisher-mcp (wrapper around the Publisher API) (accessed 2026-10-22)
- GitHub: ma2za/python-substack (accessed 2026-10-22)
- GitHub: NHagar/substack_api (accessed 2026-10-22)
- pypistats: python-substack recent downloads (accessed 2026-10-22)
- pypistats: substack-api recent downloads (accessed 2026-10-22)
- python-substack issue #27: captcha error (accessed 2026-10-22)
- Substack Terms of Use (accessed 2026-10-22)
- Reddit r/n8n: Substack doesn't provide a public API (accessed 2026-10-21)
- Reddit r/Newsletters: no public API (accessed 2026-10-21)
Related guides
Explainer · Developers & AI
Substack Scraper: Why It Breaks, the Rules, and Alternatives
Thinking of a Substack scraper? What Substack's terms say about scraping, why scrapers keep breaking, and the legal ways to get posts and stats instead.
5 min read · Updated 2026-10-22
How-to · Developers & AI
Substack API Key: Publisher API and Developer API (2026)
How to get a Substack API key in 2026: what the Publisher API returns, how to check if it's enabled, what the Developer API does, and the terms and limits.
5 min read · Updated 2026-10-22
Comparison · Developers & AI
Substack MCP Server: Official vs Open Source (2026)
Compare Substack's official MCP server, open-source Substack MCP servers and hosted ones: who can use each, what they read or write, and the risks.
6 min read · Updated 2026-10-21