TL;DR: Substack has no public write API in 2026, but its TipTap editor accepts CDP automation reliably. Here's the 50-line Python script that publishes any markdown file to Substack as a fully formatted post in 45 seconds. Saves 5+ minutes per post for a publishing batch of 30+.
Why CDP, not API?
Substack offers a public read API (RSS, API for subscriber list) but no public write API. Their internal API (/api/v1/posts) requires session cookies and CSRF tokens that change per session.
CDP attaches to a real Chrome you've already logged into. No reverse-engineering, no broken sessions — your browser does what it always does.
Setup (one-time, 60 sec)
chrome.exe ^
--remote-debugging-port=9222 ^
--remote-allow-origins=* ^
--user-data-dir="C:\Users\you\AppData\Local\Google\Chrome\User Data\AutomationProfile"
Open this profile, log into Substack manually once. The profile stays logged in indefinitely.
The 50-line Python publisher
from playwright.sync_api import sync_playwright
import re, time, pyperclip
from pathlib import Path
def publish(md_path, target_substack_publish_url):
raw = Path(md_path).read_text(encoding='utf-8-sig')
# Strip YAML frontmatter
raw = re.sub(r'^---\s*\n.*?\n---\s*\n', '', raw, flags=re.DOTALL)
title_match = re.search(r'^# (.+)$', raw, re.MULTILINE)
title = title_match.group(1) if title_match else 'Untitled'
body = re.sub(r'^# .+\n', '', raw, count=1, flags=re.MULTILINE).strip()
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp("http://127.0.0.1:9222")
ctx = browser.contexts[0]
page = ctx.new_page()
page.goto(f"{target_substack_publish_url}/publish/posts?type=newsletter")
page.wait_for_load_state('networkidle')
page.click("text=新規投稿") # or "New post" in EN
page.wait_for_selector('textarea[aria-label="title"]', timeout=10_000)
page.fill('textarea[aria-label="title"]', title)
time.sleep(1)
editor = page.locator("div.ProseMirror").first
editor.click()
time.sleep(0.5)
pyperclip.copy(body)
page.keyboard.press("Control+V")
time.sleep(2)
# Wait for autosave indicator
page.wait_for_selector('text=下書き保存しました', timeout=15_000)
page.click("text=続ける")
page.wait_for_selector("text=今すぐ全員に送信", timeout=10_000)
page.click("text=今すぐ全員に送信")
page.wait_for_url(re.compile(r'.*/p/[a-z0-9-]+'), timeout=20_000)
published_url = page.url
return published_url
# Usage
url = publish("reports/substack-issue-23.md", "https://autoappnotes.substack.com")
print(f"Published: {url}")
What this saves you
Per issue manual flow:
- Open Substack publish UI: 5 sec
- Copy title manually: 5 sec
- Open editor, paste body: 30 sec
- Wait for autosave: 10 sec
- Click 続ける: 2 sec
- Click 今すぐ全員に送信: 3 sec
- Confirm: 5 sec
Total manual: ~60 sec per issue, but with attention required throughout.
CDP automated: 45 sec per issue, fully unattended — you can batch 10 issues from CSV in 8 minutes.
Common errors and fixes
"下書き保存しました" never appears
- Substack lost focus on the editor. Click into ProseMirror again first.
Title accepted but body cut off
- Body has frontmatter leak. Strip with
re.sub(r'^---\s*\n.*?\n---\s*\n', '', body, flags=re.DOTALL).
Ctrl+V pastes nothing
- Editor not focused. Use
editor.click()beforepyperclip.copy().
"今すぐ全員に送信" button missing
- Substack has multiple publish modal variants. Use
page.click('button:has-text("今すぐ"):visible')for fallback.
Markdown not rendering as headings/lists
-
TipTap doesn't auto-parse markdown on paste. Either:
- (a) Convert markdown → HTML first, paste HTML
- (b) Convert markdown → ProseMirror JSON, dispatch via
editor.evaluate() - For most cases, (a) with
markdown-itworks:
from markdown_it import MarkdownIt md = MarkdownIt() html = md.render(body) pyperclip.copy(html) # TipTap accepts HTML clipboard via Ctrl+V
Why this scales
Once the script works, you can drive your full content backlog:
for f in reports/substack-issue-*.md; do
python publish.py "$f"
sleep 60 # rate limit
done
In one evening I went from "writing in advance" to "publishing 4 weeks of content in 30 minutes."
Source
Full Substack TipTap publish script + ProseMirror JSON converter (for richer formatting):
AutoApp Dashboard ($39) includes:
-
substack_publish.py(50 lines, this article) -
substack_publish_with_html.py(markdown→HTML→TipTap) -
substack_publish_batch.py(CSV-driven batch) -
substack_subscriber_export.py(read API helper)
If you're publishing to Substack regularly and not automating, you're spending 5 hours per month on copy-paste. CDP solves it in 50 lines.
Top comments (0)