An unofficial Python client library for interacting with Substack newsletters and content.
This library provides Python interfaces for interacting with Substack's unofficial API, allowing you to:
- Retrieve newsletter posts, podcasts, and recommendations
- Get user profile information and subscriptions
- Fetch post content and metadata
- Search for posts within newsletters
- Access publication subscriber chats and threads
- Access paywalled content that you have written or paid for with user-provided authentication
# Using pip
pip install substack-api
# Using uv
uv add substack-apiThe library includes a CLI for quick access from the terminal. All commands output JSON by default.
# Get the 5 newest posts from a newsletter
substack newsletter posts https://example.substack.com --limit 5
# Search for posts
substack newsletter search https://example.substack.com "machine learning"# Get metadata for a specific post
substack post metadata https://example.substack.com/p/my-post --pretty
# Look up a user's subscriptions
substack user subscriptions username
# Browse categories
substack categories
substack category newsletters --name Technology
# Run the quickstart guide for a full command reference
substack quickstartUse --pretty for human-readable output and --cookies <path> for authenticated access to paywalled content.
fromsubstack_apiimportNewsletter# Initialize a newsletter by its URLnewsletter=Newsletter("https://example.substack.com")
# Get recent posts (returns Post objects)recent_posts=newsletter.get_posts(limit=5)
# Get posts sorted by popularitytop_posts=newsletter.get_posts(sorting="top", limit=10)
# Search for postssearch_results=newsletter.search_posts("machine learning", limit=3)
# Get podcast episodespodcasts=newsletter.get_podcasts(limit=5)
# Get recommended newslettersrecommendations=newsletter.get_recommendations()
# Get newsletter authorsauthors=newsletter.get_authors()fromsubstack_apiimportPost# Initialize a post by its URLpost=Post("https://example.substack.com/p/post-slug")
# Get post metadatametadata=post.get_metadata()
# Get the post's HTML contentcontent=post.get_content()To access paywalled content, you need to provide your own session cookies from a logged-in Substack session:
fromsubstack_apiimportNewsletter, Post, SubstackAuth# Set up authentication with your cookiesauth=SubstackAuth(cookies_path="path/to/your/cookies.json")
# Use authentication with newslettersnewsletter=Newsletter("https://example.substack.com", auth=auth)
posts=newsletter.get_posts(limit=5) # Can now access paywalled posts# Use authentication with individual postspost=Post("https://example.substack.com/p/paywalled-post", auth=auth)
content=post.get_content() # Can now access paywalled content# Check if a post is paywalledifpost.is_paywalled():
print("This post requires a subscription")To access paywalled content, you need to export your browser cookies from a logged-in Substack session. The cookies should be in JSON format with the following structure:
[
{
"name": "substack.sid",
"value": "your_session_id",
"domain": ".substack.com",
"path": "/",
"secure": true
},
{
"name": "substack.lli", "value": "your_lli_value",
"domain": ".substack.com",
"path": "/",
"secure": true
},
...
]Important: Only use your own cookies from your own authenticated session. This feature is intended for users to access their own subscribed or authored content programmatically.
fromsubstack_apiimportUser# Initialize a user by their usernameuser=User("username")
# Get user profile informationprofile_data=user.get_raw_data()
# Get user ID and nameuser_id=user.idname=user.name# Get user's subscriptionssubscriptions=user.get_subscriptions()Substack allows users to change their handle (username) at any time. When this happens, the old API endpoints return 404 errors. This library automatically handles these redirects by default.
fromsubstack_apiimportUser# This will automatically follow redirects if the handle has changeduser=User("oldhandle") # Will find the user even if they renamed to "newhandle"# Check if a redirect occurredifuser.was_redirected:
print(f"User was renamed from {user.original_username} to {user.username}")If you prefer to handle 404s yourself:
# Disable automatic redirect followinguser=User("oldhandle", follow_redirects=False)You can also manually resolve handle redirects:
fromsubstack_apiimportresolve_handle_redirectnew_handle=resolve_handle_redirect("oldhandle")
ifnew_handle:
print(f"Handle was renamed to: {new_handle}")Access publication subscriber chats (requires authentication):
fromsubstack_apiimportChat, SubstackAuth# Set up authentication (required for chat access)auth=SubstackAuth(cookies_path="cookies.json")
# Access a publication's chat using its publication IDchat=Chat(4906951, auth=auth)
# Get recent threadsthreads=chat.get_threads(limit=5)
forthreadinthreads:
print(f"Thread: {thread.body[:80]}...")
print(f" By: {thread.author['name']} on {thread.created_at}")
print(f" {thread.comment_count} messages")
# Get messages in threadformsginthread.get_messages():
print(f" [{msg.created_at}] {msg.author['name']}: {msg.body[:60]}...")- This is an unofficial library and not endorsed by Substack
- APIs may change without notice, potentially breaking functionality
- Rate limiting may be enforced by Substack
- Authentication requires users to provide their own session cookies
- Users are responsible for complying with Substack's terms of service when using authentication features
# Install dev dependencies
pip install -e ".[dev]"# Run tests
pytestContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
This package is not affiliated with, endorsed by, or connected to Substack in any way. It is an independent project created to make Substack content more accessible through Python.