The most complete map of Substack's undocumented internal API.
129 verified endpoints, captured by driving the live API through 14 rounds of probing and write-side capture.
A practical, verified reference for Substack's undocumented internal API. Every endpoint here has been tested against the live API. Designed for humans and for AI agents (see SKILL.md).
β οΈ Unofficial.Substack doesn't publish or support this API. Endpoints can change without notice. Treat this as a working notebook, not a contract.
The Substack web app speaks to a JSON API at https://substack.com/api/v1/*
and per-publication subdomains at https://<sub>.substack.com/api/v1/*
. The web app uses it for everything: reading posts, creating drafts, publishing, managing subscribers, sending chat threads, configuring recommendations. With a session cookie, you can drive the same API from any script.
Existing community work covers parts of this surface:
NHagar/substack_apiβ Python, read-focusedma2za/python-substackβ Python, full CRUDjakub-k-slys/substack-apiβ TypeScript (archived)JPres-Projects/Substack-APIβ Python, draft + publish
This repo is intended to be the canonical endpoint reference these clients converge on. Submit a PR with anything new you find.
COOKIE='s%3A...your.connect.sid.value...'
curl -H "Cookie: connect.sid=$COOKIE; substack.sid=$COOKIE" \
https://substack.com/api/v1/user/profile/self | jq .handle
curl -X POST \
-H "Cookie: connect.sid=$COOKIE; substack.sid=$COOKIE" \
-H "Content-Type: application/json" \
-d '{"draft_title":"Hello","draft_subtitle":"From the API","draft_body":"<p>Hi.</p>","type":"newsletter"}' \
https://yourname.substack.com/api/v1/drafts
β every verified endpoint with body shapes, query params, and sample responsesENDPOINTS.md
βopenapi.yaml
OpenAPI 3.1 spec(125 operations, 51 schemas) β scaffold a typed client in any languageβ getting the cookie, format, rotation, sending it from server-side codeAUTH.md
β Claude Agent SDK skill manifestSKILL.md
β drop-in curl scriptsexamples/curl/
β minimal typed clientexamples/typescript/
Drop-in commands for popular generators:
npx openapi-typescript openapi.yaml -o substack-types.ts
npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ./sdk-ts
npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g python -o ./sdk-py
npx @openapitools/openapi-generator-cli generate -i openapi.yaml -g go -o ./sdk-go
Substack's endpoints split across two host families (account + per-pub) and the spec models both as servers
β set subdomain
when configuring the client.
Read side (passive β works with just a cookie):
- Authentication, account discovery, public profile, blocked users
- Publication CRUD (read), settings, sections, tags, recommendations, Stripe status, pledge tiers
- Drafts, scheduled posts, published posts, post management counts
- Per-post stats (31 engagement fields including open rate, CTR, daily breakdowns, referrers)
- Publication-wide analytics (subscribers, ARR, network attribution, growth sources/events timeseries)
- Subscribers list (filter/sort/paginate), DMs/messages inbox, activity feed
- Reader inbox (
/inbox/top
), Notes feed + tabs, global search, search modules - Categories, reactions catalog, comment moderation enum, per-post mute settings
Write side (captured by driving the UI through Chrome):
PUT /publication
β single-field saves (name
,hero_text
,language
,welcome_email_content
, β¦)PUT /publication_settings
β boolean toggles (high-res video, AI training opt-out, cross-posting, etc.)- Drafts: create, update, delete, publish, schedule, scheduled_release
- Notes: create (
POST /comment/feed
), delete, mark-seen - Reader comments: create, delete;
post reactions(literal emoji +surface
) Recommendations: add (PUT /recommendations
), remove (DELETE /recommendations/
β note trailing slash), search, suggestedAudio/podcast uploadβ full S3 presigned multipart sequence (init β S3 PUT β transcode β poll)** Substack Chat**: enable/disable (/publication_threads_settings
), send thread (client-generated UUID), delete thread- Co-author invites, subscriber add/remove, post-tag attach/detach, image upload
- Generic user setting (
PUT /user-setting
)
Known gates (documented but won't be cracked here):
POST /publication
β captcha-gated by design- Custom domain config β $50 one-time + DNS setup
- Magic-link verify β at
/sign-in?token=...
, not under/api/v1/*
- Moderator-delete-with-reason β needs another user's comment to surface
Each endpoint in ENDPOINTS.md is marked:
- β
Verifiedβ personally tested against the live API - π‘
Reportedβ documented by another client / blog post, not independently re-tested - β
Inferredβ pattern-matched from related endpoints, no successful call observed - β
Deadβ tested and confirmed 404, documented to save you the time - π
Gatedβ exists but returns 403 from a plain curl (often works from a real browser; see the "two-host trick" and "browser-vs-curl gap" notes in
ENDPOINTS.md
)
PRs welcome to upgrade β β π‘ β β .
This reference was built over 14 progressive rounds of capture, documented as Round N
commits. The methodology stack:
Curl probing for read endpoints + empirical-error field discovery (sending intentionally-invalid POSTs to surface required fields via Substack's error messages β that's howtrigger_at
was found, and dozens of others)Playwright headless captureβ driving Chromium with a session cookie through the admin SPA to log everyapi/v1
request that firesChrome extension live capture(Claude in Chrome) β monkey-patchingwindow.fetch
andXMLHttpRequest.send
from alocalStorage
-backed log so request bodies survive React-driven page reloads, then doing add-then-revert cycles on real publication actions to capture the write-side bodies that couldn't be reached passively
Why all three: passive observation gets you read endpoints and a lot of POST bodies "for free." Empirical probing fills the gaps where errors are descriptive (Substack's are). Live driving the UI catches the cases where the body shape is non-obvious (the literal-emoji reaction value, the client-generated UUID for thread sends, the stringified-ProseMirror welcome email content) and where endpoints 403 from curl but 200 from a real browser session (recommendations was the canonical example).
Found a new endpoint? Test with curl, then PR with:
- Method + path
- Required host (
substack.com
vs{sub}.substack.com
) - Required headers / query params
- Sample response (sanitize user data)
- Classification (β / π‘ / β)
- Date you verified it
Capture playbook for body shapes you can't get from curl:
-
Open Substack in Chrome β DevTools β Network β filter to
Fetch/XHR -
Perform the action in the UI
-
The matching request appears in Network β right-click β Copy as cURL - Strip the cookie + headers down to the minimum that still works
-
PR the result here
This reference was assembled by Anthony David Adams and the EarthPilot.ai Lab β a small applied-AI lab building Mission Support for Spaceship Earth: tools and protocols for sense-making, decision-making, and coordination at planetary scale. Working on Substack-adjacent infrastructure (newsletter platforms, AI editorial pipelines, growth tooling) pushed us to map this surface; sharing it back is the natural thing to do.
If you're working at the edge of AI Γ meaning Γ infrastructure β or you want to be β come hang out at the ** Singularity Playground**. It's our open community for builders, researchers, and writers working on what comes next. Free to join, no pitch deck required.
MIT. See LICENSE.