Automating Substack Sync from a Static Blog

How I built a system to automatically sync markdown posts to Substack as drafts

Published December 28, 2025 ET

I want the best of both worlds: own my content in plain markdown files, but use Substack's distribution and monetization. The problem? Substack doesn't offer a plugin or API for external content. You have to put your content on their platform.

So I built a bridge.


The Hybrid Approach

Instead of choosing between my blog and Substack, I automated the sync:

  1. Write posts in markdown on my static blog
  2. Add substack: true to frontmatter for posts I want on Substack
  3. Push to main
  4. A GitHub Action syncs tagged posts to Substack as drafts
  5. I review and publish from Substack's dashboard

This gives me ownership (source lives in my repo) plus distribution (Substack's ecosystem).


How It Works

The Trigger

Any post with substack: true in its frontmatter gets synced:

---
title: "My Post"
description: "A great read"
datePublished: "2025-01-15"
tags:
  - writing
substack: true
---

The Script

A Python script (scripts/substack-sync.py) does the heavy lifting:

  1. Scans all markdown files in content/
  2. Filters for substack: true
  3. Converts markdown to Substack's format
  4. Creates new drafts or updates existing ones
  5. Writes the Substack ID back to frontmatter

After the first sync, the frontmatter gets updated:

substack: true
substackId: "123456"  # Added automatically

The substackId lets the script know this post already exists on Substack, so it updates instead of creating a duplicate.

The GitHub Action

A separate workflow (.github/workflows/substack-sync.yml) runs on every push to main:

on:
  push:
    branches: ["main"]
    paths:
      - 'content/**/*.md'

It only triggers when content files change, keeping things efficient. After syncing, it commits any frontmatter updates back to the repo.


The Unofficial API

Here's the catch: Substack has no official API.

The script uses Substack's internal endpoints, authenticated with a session cookie from your browser. It's the same API their web app uses, just called programmatically.

To set it up, you need three GitHub secrets:

  • SUBSTACK_PUBLICATION_URL - Your Substack subdomain (e.g., yourname.substack.com)
  • SUBSTACK_SID - Session cookie from browser (DevTools > Application > Cookies > substack.sid)
  • SUBSTACK_USER_ID - Your user ID (visit https://substack.com/api/v1/user/profile/self while logged in and copy the id field)

The session cookie typically stays valid for months, so you don't need to refresh it often.


Limitations

This approach has some constraints:

  1. Drafts only - Posts sync as drafts. You manually publish from Substack's dashboard. This is intentional; I want a human review step before anything goes live.

  2. No images - The script syncs text content only. Images would need to be uploaded to Substack's CDN separately.

  3. Unofficial API - If Substack changes their internal API, the script might break. But it's simple enough to fix.

  4. One-way sync - Changes flow from my blog to Substack, not the reverse. If I edit on Substack, those changes don't come back.


Why Drafts?

I deliberately chose to sync as drafts rather than auto-publish. Reasons:

  • Quality control - I can review formatting before publishing
  • Timing - Substack has optimal send times; I want to control when posts go out
  • Paid content - Some posts might need paywall settings configured
  • Safety net - If something breaks, it's just a draft

What's Next

This is version 1. Future improvements might include:

  • substack: publish to auto-publish certain posts
  • Image syncing via Substack's CDN
  • Two-way sync (probably not worth the complexity)
  • Better markdown-to-Substack formatting

For now, this simple bridge lets me write where I want while tapping into Substack's ecosystem. The best of both worlds.