{"id":37071104,"url":"https://github.com/ma2za/python-substack","last_synced_at":"2026-04-02T13:30:57.807Z","repository":{"id":45247390,"uuid":"507708304","full_name":"ma2za/python-substack","owner":"ma2za","description":"Substack API python implementation","archived":false,"fork":false,"pushed_at":"2026-03-25T17:41:16.000Z","size":414,"stargazers_count":144,"open_issues_count":2,"forks_count":24,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-26T17:00:11.189Z","etag":null,"topics":["newsletter","python","python-substack","substack"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/python-substack/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ma2za.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-06-26T23:36:44.000Z","updated_at":"2026-03-26T15:42:46.000Z","dependencies_parsed_at":"2023-12-03T10:24:29.986Z","dependency_job_id":"e2f36fb0-59a2-4406-9b94-a6776023eb5a","html_url":"https://github.com/ma2za/python-substack","commit_stats":null,"previous_names":["mazza8/python-substack"],"tags_count":14,"template":false,"template_full_name":null,"purl":"pkg:github/ma2za/python-substack","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ma2za%2Fpython-substack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ma2za%2Fpython-substack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ma2za%2Fpython-substack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ma2za%2Fpython-substack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ma2za","download_url":"https://codeload.github.com/ma2za/python-substack/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ma2za%2Fpython-substack/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31307129,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["newsletter","python","python-substack","substack"],"created_at":"2026-01-14T08:18:16.706Z","updated_at":"2026-04-02T13:30:57.775Z","avatar_url":"https://github.com/ma2za.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Python Substack\n\nThis is an unofficial library providing a Python interface for [Substack](https://substack.com/).\nI am in no way affiliated with Substack.\n\n[![Downloads](https://static.pepy.tech/badge/python-substack/month)](https://pepy.tech/project/python-substack)\n![Release Build](https://github.com/ma2za/python-substack/actions/workflows/ci_publish.yml/badge.svg)\n---\n\n# Installation\n\nYou can install python-substack using:\n\n    $ pip install python-substack\n\nFor the MCP server tools, install the extra dependency set:\n\n    $ poetry install --with mcp\n\n\u003e NOTE: We had to upgrade the package requirements to support Python 3.10 because 3.9 is basically vintage now. If you still run 3.9, please join us in the future (or bring snacks).\n\n---\n\n# Setup\n\nSet the following environment variables by creating a **.env** file:\n\n    EMAIL=\n    PASSWORD=\n    PUBLICATION_URL=  # Optional: your publication URL\n    COOKIES_PATH=     # Optional: path to cookies JSON file\n    COOKIES_STRING=   # Optional: cookie string for authentication\n\n## If you don't have a password\n\nRecently Substack has been setting up new accounts without a password. If you sign out and sign back in, it just uses\nyour email address with a \"magic\" link.\n\nSet a password:\n\n- Sign out of Substack\n- At the sign-in page, click \"Sign in with password\" under the `Email` text box\n- Then choose, \"Set a new password\"\n\nThe .env file will be ignored by git but always be careful.\n\n---\n\n# Usage\n\nCheck out the examples folder for some examples 😃 🚀\n\n## Basic Authentication\n\n```python\nimport os\nfrom dotenv import load_dotenv\n\nfrom substack import Api\nfrom substack.post import Post\n\nload_dotenv()\n\n# Authenticate with email and password\napi = Api(\n    email=os.getenv(\"EMAIL\"),\n    password=os.getenv(\"PASSWORD\"),\n    publication_url=os.getenv(\"PUBLICATION_URL\"),\n)\n```\n\n## Cookie-based Authentication\n\nYou can also authenticate using cookies instead of email/password:\n\n```python\nimport os\nfrom dotenv import load_dotenv\n\nfrom substack import Api\n\nload_dotenv()\n\n# Authenticate with cookies (alternative to email/password)\napi = Api(\n    cookies_path=os.getenv(\"COOKIES_PATH\"),  # Path to cookies JSON file\n    # OR\n    cookies_string=os.getenv(\"COOKIES_STRING\"),  # Cookie string\n    publication_url=os.getenv(\"PUBLICATION_URL\"),\n)\n```\n\n## Creating and Publishing Posts\n\n```python\nuser_id = api.get_user_id()\n\n# Switch Publications - The library defaults to your user's primary publication. You can retrieve all your publications and change which one you want to use.\n\n# primary publication\nuser_publication = api.get_user_primary_publication()\n# all publications\nuser_publications = api.get_user_publications()\n\n# This step is only necessary if you are not using your primary publication\n# api.change_publication(user_publication)\n\n# Create a post with basic settings\npost = Post(\n    title=\"How to publish a Substack post using the Python API\",\n    subtitle=\"This post was published using the Python API\",\n    user_id=user_id\n)\n\n# Create a post with audience and comment permissions\npost = Post(\n    title=\"My Post Title\",\n    subtitle=\"My Post Subtitle\",\n    user_id=user_id,\n    audience=\"everyone\",  # Options: \"everyone\", \"only_paid\", \"founding\", \"only_free\"\n    write_comment_permissions=\"everyone\"  # Options: \"none\", \"only_paid\", \"everyone\"\n)\n\npost.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'})\n\n# bolden text\npost.add({'type': \"paragraph\",\n          'content': [{'content': \"This is how you \"}, {'content': \"bolden \", 'marks': [{'type': \"strong\"}]},\n                      {'content': \"a word.\"}]})\n\n# add hyperlink to text\npost.add({'type': 'paragraph', 'content': [\n    {'content': \"View Link\", 'marks': [{'type': \"link\", 'href': 'https://whoraised.substack.com/'}]}]})\n\n# set paywall boundary\npost.add({'type': 'paywall'})\n\n# add image\npost.add({'type': 'captionedImage', 'src': \"https://media.tenor.com/7B4jMa-a7bsAAAAC/i-am-batman.gif\"})\n\n# add local image\nimage = api.get_image('image.png')\npost.add({\"type\": \"captionedImage\", \"src\": image.get(\"url\")})\n\n# embed publication\nembedded = api.publication_embed(\"https://jackio.substack.com/\")\npost.add({\"type\": \"embeddedPublication\", \"url\": embedded})\n\n# create post from Markdown\nmarkdown_content = \"\"\"\n# My Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n![Image Alt](https://example.com/image.jpg)\n\"\"\"\npost.from_markdown(markdown_content, api=api)\n\ndraft = api.post_draft(post.get_draft())\n\n# set section (can only be done after first posting the draft)\n# post.set_section(\"rick rolling\", api.get_sections())\n# api.put_draft(draft.get(\"id\"), draft_section_id=post.draft_section_id)\n\napi.prepublish_draft(draft.get(\"id\"))\n\napi.publish_draft(draft.get(\"id\"))\n```\n\n## Loading Posts from YAML Files\n\nYou can define your posts in YAML files for easier management:\n\n```python\nimport yaml\nimport os\nfrom dotenv import load_dotenv\n\nfrom substack import Api\nfrom substack.post import Post\n\nload_dotenv()\n\n# Load post data from YAML file\nwith open(\"draft.yaml\", \"r\") as fp:\n    post_data = yaml.safe_load(fp)\n\n# Authenticate (using cookies or email/password)\ncookies_path = os.getenv(\"COOKIES_PATH\")\ncookies_string = os.getenv(\"COOKIES_STRING\")\n\napi = Api(\n    email=os.getenv(\"EMAIL\") if not cookies_path and not cookies_string else None,\n    password=os.getenv(\"PASSWORD\") if not cookies_path and not cookies_string else None,\n    cookies_path=cookies_path,\n    cookies_string=cookies_string,\n    publication_url=os.getenv(\"PUBLICATION_URL\"),\n)\n\nuser_id = api.get_user_id()\n\n# Create post from YAML data\npost = Post(\n    post_data.get(\"title\"),\n    post_data.get(\"subtitle\", \"\"),\n    user_id,\n    audience=post_data.get(\"audience\", \"everyone\"),\n    write_comment_permissions=post_data.get(\"write_comment_permissions\", \"everyone\"),\n)\n\n# Add body content from YAML\nbody = post_data.get(\"body\", {})\nfor _, item in body.items():\n    # Handle local images - upload them first\n    if item.get(\"type\") == \"captionedImage\" and not item.get(\"src\").startswith(\"http\"):\n        image = api.get_image(item.get(\"src\"))\n        item.update({\"src\": image.get(\"url\")})\n    post.add(item)\n\ndraft = api.post_draft(post.get_draft())\nput_draft_kwargs = {\n    \"draft_section_id\": post.draft_section_id,\n    \"search_engine_title\": post_data.get(\"search_engine_title\"),\n    \"search_engine_description\": post_data.get(\"search_engine_description\"),\n    \"slug\": post_data.get(\"slug\"),\n}\nput_draft_kwargs = {k: v for k, v in put_draft_kwargs.items() if v is not None}\napi.put_draft(draft.get(\"id\"), **put_draft_kwargs)\n\n# Publish the draft\napi.prepublish_draft(draft.get(\"id\"))\napi.publish_draft(draft.get(\"id\"))\n```\n\nExample YAML structure:\n\n```yaml\ntitle: \"My Post Title\"\nsubtitle: \"My Post Subtitle\"\naudience: \"everyone\"  # everyone, only_paid, founding, only_free\nwrite_comment_permissions: \"everyone\"  # none, only_paid, everyone\nsection: \"my-section\"\nbody:\n  0:\n    type: \"heading\"\n    level: 1\n    content: \"Introduction\"\n  1:\n    type: \"paragraph\"\n    content: \"This is a paragraph.\"\n  2:\n    type: \"captionedImage\"\n    src: \"local_image.jpg\"  # Local images will be uploaded automatically\n```\n\n## MCP FastMCP server\n\nThis package now includes a FastMCP server in `substack/mcp_fastmcp.py` with the following tools:\n\n- `post_draft_from_markdown(...)`: create draft from markdown, optional tag/add/prepublish/publish, and control send/share_automatically.\n- `put_draft(draft_id, update_payload)`: update draft fields.\n- `add_tags(draft_id, tags)`: add tags to a draft/post.\n- `prepublish_draft(draft_id)`: prepublish a draft.\n- `publish_draft(draft_id, send=True, share_automatically=False)`: publish a draft.\n\nUse via stdio transport:\n\n```bash\npython -c \"from substack.mcp_fastmcp import main; main()\"\n```\n\n# Contributing\n\nInstall pre-commit:\n\n```shell\npip install pre-commit\n```\n\nSet up pre-commit\n\n```shell\npre-commit install\n```\n\n## Cookie Help\n\nTo get a cookie string, after login, go to dev tools (F12), network tab, refresh and find one of the requests like subscription/unred/subscriptions, right click and copy as fetch (Node.js), paste somewhere and get the entire cookie string assigned to the cookie header and put it in the env variables as COOKIES_STRING, et voila!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fma2za%2Fpython-substack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fma2za%2Fpython-substack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fma2za%2Fpython-substack/lists"}