Substack has no public API and issues no API keys. Your browser authenticates with a session cookie, and so does this tool. You copy that cookie once, and the tool sends it the same way your browser does.
There are two cookies. You need the first one. You only need the second if you want to schedule posts.
| Cookie | Where it lives | What it unlocks |
|---|---|---|
connect.sid |
Your publication domain | Everything except scheduling |
substack.sid |
substack.com |
schedule and unschedule |
Both last roughly two to four weeks. When one expires, every call returns 401 or 403 and the tool prints the exact refresh steps rather than a stack trace.
- Open your publication in a browser where you are signed in as the author. That is
https://yourname.substack.com, or your custom domain if you have one. - Press
F12to open DevTools. - Go to Application (Chrome, Edge, Brave) or Storage (Firefox).
- Expand Cookies in the left sidebar and click your publication's domain.
- Find the row named
connect.sidand copy its Value. It is a long string starting withs%3A.
Then run:
substack initPaste the URL, paste the cookie. The tool writes them to a config file with 0600
permissions, calls the API to discover your publication id and user id, caches those too,
and prints who you are signed in as.
Scheduling is the one operation that does not live on your publication domain. It runs on
substack.com, which uses a different cookie.
- Open
https://substack.comwhile signed in. F12> Application > Cookies >https://substack.com.- Copy the value of
substack.sid.
substack init --hub-tokenEverything else keeps working without it. substack doctor says plainly whether
scheduling is available.
Settings resolve from three places, highest priority first.
- Environment variables.
.substack.jsonin the current directory or any parent.- The per-user config file.
The per-user file is at ~/.config/substack-cli/config.json on Linux and macOS
($XDG_CONFIG_HOME is respected), and %APPDATA%\substack-cli\config.json on Windows.
{
"publication_url": "https://yourname.substack.com",
"session_token": "s%3A...",
"hub_session_token": "s%3A...",
"publication_id": 2433797,
"user_id": 85873917,
"template": "INIT"
}Only the first two lines are yours to fill in. The ids get discovered and written back on
first use. template is optional and names a saved post template to wrap every push in.
Use these in CI, in a container, or anywhere you would rather not write a file.
export SUBSTACK_PUBLICATION_URL="https://yourname.substack.com"
export SUBSTACK_SESSION_TOKEN="s%3A..."
export SUBSTACK_HUB_SESSION_TOKEN="s%3A..." # optional, scheduling only
export SUBSTACK_PUBLICATION_ID=2433797 # optional, saves one API call
export SUBSTACK_USER_ID=85873917 # optional, saves one API call
export SUBSTACK_TEMPLATE="INIT" # optionalA .substack.json beside your posts lets one machine drive several publications. The
tool walks up from the current directory to find it, so it works from any subfolder.
cd ~/writing/newsletter-a
substack init --localAdd .substack.json to your .gitignore. It holds a live session cookie.
A session cookie is a password. Treat it like one.
- The config file is written
0600on any OS that has file modes. - Nothing is ever printed back to the terminal, including by
doctorand--verbose. - In CI, use your platform's secret store and pass it as an environment variable. Never commit it, and never paste it into an issue.
- If you leak one, sign out of all sessions from Substack's account settings. That invalidates every cookie immediately.
2FA changes nothing here. You authenticate in the browser as usual, and the cookie you copy is already past that check.