A Python wrapper around the Substack API.
Project description
Python Substack
Unofficial Python tools for publishing to Substack.
Features
- Create drafts and publish posts from Python.
- Convert Markdown into Substack's editor document format.
- Upload local images while rendering Markdown.
- Set audience, comment permissions, SEO title, SEO description, slug, sections, and tags.
- Publish now, schedule drafts, or keep drafts unpublished by default.
- Authenticate with email/password, cookies JSON, or a browser cookie string.
- Run a FastMCP server for AI-assisted publishing workflows.
Installation
pip install python-substack
Install the MCP server extra:
pip install "python-substack[mcp]"
Setup
Copy .env.example to .env and fill in one authentication method:
EMAIL=
PASSWORD=
PUBLICATION_URL=
COOKIES_PATH=
COOKIES_STRING=
Use either EMAIL and PASSWORD, or cookie-based authentication with COOKIES_PATH or COOKIES_STRING. Cookie authentication is usually the better option if Substack prompts for captcha or magic-link sign-in.
Newer Substack accounts may only have magic-link sign-in enabled. To set a password, sign out of Substack, choose "Sign in with password", then choose "Set a new password".
Quickstart
import os
from dotenv import load_dotenv
from substack import Api
load_dotenv()
api = Api(
email=os.getenv("EMAIL"),
password=os.getenv("PASSWORD"),
publication_url=os.getenv("PUBLICATION_URL"),
)
result = api.create_draft_from_markdown(
title="Shipping with Python",
subtitle="A short note from a script",
markdown="""
# Hello
This draft was created from **Markdown**.

""",
tags=["python", "automation"],
slug="shipping-with-python",
)
print(result["draft"]["id"])
create_draft_from_markdown creates a draft by default. It only publishes when publish=True is passed.
CLI
Check authentication without creating a draft:
substack-auth-check
With a cookies JSON file:
substack-auth-check --cookies cookies.json
Publish a Markdown file as a draft:
substack-publish-markdown post.md --title "My Post"
Create and publish:
substack-publish-markdown post.md --title "My Post" --publish
Publish from YAML:
substack-publish-yaml draft.yaml
Useful options:
substack-publish-markdown post.md \
--title "My Post" \
--subtitle "Optional subtitle" \
--tag python \
--tag substack \
--slug my-post \
--search-engine-title "SEO title" \
--search-engine-description "SEO description"
Cookie Authentication
Cookie authentication avoids logging in with email/password on every run and helps when Substack requires captcha or magic-link sign-in.
Use a cookies JSON file:
import os
from dotenv import load_dotenv
from substack import Api
load_dotenv()
api = Api(
cookies_path=os.getenv("COOKIES_PATH"),
publication_url=os.getenv("PUBLICATION_URL"),
)
Or paste a browser cookie header into COOKIES_STRING:
import os
from dotenv import load_dotenv
from substack import Api
load_dotenv()
api = Api(
cookies_string=os.getenv("COOKIES_STRING"),
publication_url=os.getenv("PUBLICATION_URL"),
)
To get a cookie string:
- Sign in to Substack in your browser.
- Open developer tools.
- Go to the network tab and refresh Substack.
- Select a request such as
subscription/unred/subscriptions. - Copy the full
cookierequest header value intoCOOKIES_STRING.
To export a working session to a cookies JSON file:
api.export_cookies("cookies.json")
Then set:
COOKIES_PATH=cookies.json
The CLI also accepts a cookie JSON path:
substack-publish-markdown post.md --cookies cookies.json
Low-Level Post Builder
import os
from dotenv import load_dotenv
from substack import Api
from substack.post import Post
load_dotenv()
api = Api(
email=os.getenv("EMAIL"),
password=os.getenv("PASSWORD"),
publication_url=os.getenv("PUBLICATION_URL"),
)
user_id = api.get_user_id()
post = Post(
title="How to publish a Substack post using Python",
subtitle="Created with python-substack",
user_id=user_id,
audience="everyone",
write_comment_permissions="everyone",
)
post.paragraph("This is a paragraph.")
post.add(
{
"type": "paragraph",
"content": [
{"content": "A link to "},
{
"content": "Substack",
"marks": [{"type": "link", "href": "https://substack.com"}],
},
],
}
)
post.add({"type": "paywall"})
post.add({"type": "captionedImage", "src": "https://example.com/image.png"})
draft = api.post_draft(post.get_draft())
api.prepublish_draft(draft.get("id"))
api.publish_draft(draft.get("id"))
Markdown Support
from substack.post import Post
post = Post("Title", "Subtitle", user_id=1)
post.from_markdown(
"""
# Heading
Paragraph with **bold**, *italic*, `code`, [links](https://example.com), and footnotes.[^1]
- Lists
- Images

[^1]: Footnote text.
"""
)
Supported Markdown includes headings, paragraphs, bold, italic, inline code, strikethrough, links, images, linked images, image captions, code blocks, blockquotes, ordered lists, unordered lists, horizontal rules, and footnotes.
When an Api instance is passed to from_markdown, local image paths are uploaded before the draft is created:
post.from_markdown(markdown_content, api=api)
YAML Drafts
title: "My Post Title"
subtitle: "My Post Subtitle"
audience: "everyone"
write_comment_permissions: "everyone"
search_engine_title: "SEO title"
search_engine_description: "SEO description"
slug: "my-post-title"
tags:
- python
- substack
markdown: |
# Introduction
This post body is Markdown.
The lower-level node format is also supported:
title: "My Post Title"
subtitle: "My Post Subtitle"
body:
0:
type: "heading"
level: 1
content: "Introduction"
1:
type: "paragraph"
content: "This is a paragraph."
2:
type: "captionedImage"
src: "local_image.jpg"
MCP Server
Install the MCP extra:
pip install "python-substack[mcp]"
Run the server over stdio:
substack-mcp
Equivalent Python entry point:
python -c "from substack_mcp.mcp_server import main; main()"
Available tools:
post_draft_from_markdown(...)put_draft(draft_id, update_payload)add_tags(draft_id, tags)prepublish_draft(draft_id)publish_draft(draft_id, send=True, share_automatically=False)
Development
pip install pre-commit
pre-commit install
pytest
Live Substack tests are opt-in. Set RUN_SUBSTACK_E2E=1 and configure credentials before running them.
Release changes are tracked in CHANGELOG.md.
Disclaimer
This project is not affiliated with Substack.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_substack-0.1.25.tar.gz.
File metadata
- Download URL: python_substack-0.1.25.tar.gz
- Upload date:
- Size: 23.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.4.1 CPython/3.11.0 Linux/6.17.0-1018-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b45e90820453b722219b45436585dec95fbc2126908d833da2b7267717285fd4
|
|
| MD5 |
5a0149893d3c7d6742defdf5d03b9e9d
|
|
| BLAKE2b-256 |
79f58e96a482271b5a444589d38d1670f3ae9f4b898d3e7a2ecfcc511a98da03
|
File details
Details for the file python_substack-0.1.25-py3-none-any.whl.
File metadata
- Download URL: python_substack-0.1.25-py3-none-any.whl
- Upload date:
- Size: 25.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.4.1 CPython/3.11.0 Linux/6.17.0-1018-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d4d7d45ee69333125451c5f37ef9ebfc6400d009f8384b62f0d3cb40a87778c
|
|
| MD5 |
245e6746c868a1ba03847cf50189523f
|
|
| BLAKE2b-256 |
e8e82b11fdce90b7fdf457c072804f4b17281e98f65a5b587a8aa15ff532c6ac
|