Substack API 調査レポート
最終調査日: 2026-05-14 関連issue: #48 (E-1)
サマリ(採用判断)
- 採用するライブラリ:
python-substack(ma2za/python-substack) v0.1.22- 最終push: 2026-05-08(活発)/ stars 150 / forks 25 / open issues 0 / MIT
- 下書き作成・更新・画像アップロード・公開・タグ付け・セクション設定をフルサポートする唯一の現役メンテナンスPython実装。
- 認証は「Email+Password」「
cookies.json」「cookies_string」の3方式。Cookie方式が CAPTCHA回避でも推奨。
- 採用しない代替:
NHagar/substack_apiv1.2.0 (★194) — read-only のみ(投稿不可)。本タスクの下書きアップロードには使えない。閲覧系のみ最も成熟。JPres-Projects/Substack-API(★2) — マイナー、画像アップロード非対応。非推奨。- 自前実装 —
python-substackのフォーク or 薄いラッパとして「危険箇所のみ自前で叩く」ハイブリッドが現実的。
- 主要リスク:
- Substack TOS は "automated means" による scrape/crawl/spider を禁止(下書きアップロードがそれに直結するかはグレー)。
- 非公式エンドポイントは予告なく仕様変更される(
primaryPublication→publicationUsers再編の前例)。 - Email+Password ログインは CAPTCHA に詰まりやすい。Cookie 方式運用が必須級。
1. 既存ライブラリ比較
| ライブラリ | Lang | ★ | Fork | Last push | Read | Draft作成 | 画像Up | 公開 | 認証 | 採用度 |
|---|---|---|---|---|---|---|---|---|---|---|
| ma2za/python-substack v0.1.22 | Python ≥3.10 | 150 | 25 | 2026-05-08 | ○ | ○ post_draft | ○ get_image(base64) | ○ publish_draft | email/pw, cookies.json, cookies_string | ★★★★★ |
| NHagar/substack_api v1.2.0 | Python ≥3.12 | 194 | 31 | 2026-03-16 | ○ | × | × | × | optional cookies | ★★(読み取り用途のみ) |
| JPres-Projects/Substack-API v1.0.0 | Python | 2 | 1 | 2025-09-06 | ○ | ○(markup記法) | × | ○ | env cookies | ★ |
| jakub-k-slys/substack-api | TypeScript | n/a | n/a | active | ○ | ○ | 部分 | ○ | cookies | 参考実装 |
| conorbronsdon/substack-mcp | MCP server | n/a | n/a | active | ○ | ○ | × | 意図的に非対応 | cookies | 参考実装 |
結論: 書き込み系を全部カバーする現役Python実装は ma2za/python-substack 一択。
2. 推奨実装方針
ハイブリッド構成
- 下地として
python-substackを採用(pip install python-substack)。ApiとPostの責務(HTTP呼び出し・ProseMirror組み立て)を活用。 - 本リポジトリ独自層で次を実装:
- 認証情報の Keychain ロード(
security find-generic-password -s SUBSTACK_COOKIES_STRING -a "$USER" -w)。 - リトライ・指数バックオフ・429ハンドリング(ライブラリは未実装)。
- スキーマ変更検知(
primaryPublication⇄publicationUsers系の差異吸収)。 - 画像 CDN URL の
internalRedirect補完。
- 認証情報の Keychain ロード(
- ピン留め:
python-substack==0.1.22で固定。CI で「下書きを作って即削除する smoke test」を週次実行し回帰検知。 - 将来の脱却ルート: ライブラリが消えたときに自前実装に切替できるよう、エンドポイント呼び出しは1モジュールに隔離(
substack_client.py)。
「自前実装オンリー」は非推奨: ライブラリの Post.add / from_markdown / captioned_image が実観測ベースのProseMirrorノード構築を約600行担っており、再実装コストが大きい。
3. 認証フロー
3.1 Cookie取得(推奨運用フロー)
- ブラウザで https://substack.com にログイン
- DevTools → Application → Cookies →
https://substack.com - 重要 Cookie:
substack.sid— メインのセッションID(Domain=.substack.com,Path=/,Secure,HttpOnly, 約3ヶ月有効)substack.lli— ログイン補助sid— 補助
- Cookie値を Keychain に保存:bash
security add-generic-password -s "SUBSTACK_COOKIES_STRING" -a "$USER" \ -w "substack.sid=...; substack.lli=...; sid=..." -U .zshrcに参照を追加:bashexport SUBSTACK_COOKIES_STRING=$(security find-generic-password -s "SUBSTACK_COOKIES_STRING" -a "$USER" -w) export SUBSTACK_PUBLICATION_URL="https://yourpub.substack.com"
3.2 ライブラリ側の処理
python-substack の Api.__init__ は cookies_string を受け取り、内部の _parse_cookies_string がセミコロン区切りをdictに変換、requests.Session.cookies.update(...) でセッションに注入。CSRF token は使われていない(観測上、substack.sid 単独で書き込みAPIが通る)。
3.3 Email/Password 経由(フォールバック)
エンドポイント: POST https://substack.com/api/v1/login
{
"captcha_response": null,
"email": "...",
"for_pub": "",
"password": "...",
"redirect": "/"
}注意: Substackが新規アカウントをパスワードレス(magic link)に移行中。書き込み自動化を行うアカウントは事前に「Sign in with password → Set a new password」でパスワードを設定する必要がある。CAPTCHA を踏むと突破不能。本タスクでは Cookie 方式を主、Email/Password は緊急用フォールバック。
3.4 2FA 対応
python-substack には2FA専用フローなし。2FA有効アカウントは Cookie方式必須(ブラウザで一度ログインしてセッション Cookie を流用)。
3.5 Playwright自動Cookie取得(任意拡張)
本リポ独自に追加するなら Playwright で https://substack.com/sign-in → email/password入力 → context.cookies() を JSON 出力 → Keychain更新、というスクリプトを用意(CAPTCHA を踏んだら人手フォールバック)。
4. 主要エンドポイント仕様(python-substack 実観測ベース)
ベースURL:
- グローバル:
https://substack.com/api/v1 - パブリケーション固有:
https://<subdomain>.substack.com/api/v1(カスタムドメイン時はhttps://<custom_domain>/api/v1)
4.1 ユーザー情報
| 操作 | Method | Path |
|---|---|---|
| 自分のプロフィール | GET | /api/v1/user/profile/self |
| 設定 | GET | /api/v1/settings |
| パブリケーションのユーザー一覧 | GET | {publication_url}/api/v1/publication/users |
レスポンスの profile.publicationUsers[*].publication が所属パブリケーション。is_primary=true のものがプライマリ。旧形式 primaryPublication フィールドからの移行があったので両対応すること。
4.2 下書き作成
POST {publication_url}/api/v1/drafts
Cookie: substack.sid=...
Content-Type: application/jsonbody 例:
{
"draft_title": "How to publish a Substack post using the Python API",
"draft_subtitle": "This post was published using the Python API",
"draft_body": "{\"type\":\"doc\",\"content\":[ ...ProseMirrorノード配列... ]}",
"draft_bylines": [{"id": 12345678, "is_guest": false}],
"audience": "everyone",
"draft_section_id": null,
"section_chosen": true,
"write_comment_permissions": "everyone"
}重要点:
draft_bodyは JSON文字列としてエスケープして送る(オブジェクトのままではない)。draft_bylines[*].idはint(user_id)。audience:everyone | only_paid | founding | only_free。write_comment_permissions:none | only_paid | everyone。
レスポンス: 作成された draft オブジェクト。id が以降の更新/公開で必要。
4.3 下書き更新
PUT {publication_url}/api/v1/drafts/{draft_id}任意フィールド: draft_section_id, search_engine_title, search_engine_description, slug, draft_body, draft_title 等。kwargs で渡したフィールドだけが PUT body に乗る。
4.4 下書き取得 / 一覧 / 削除
| 操作 | Method | Path | パラメータ |
|---|---|---|---|
| 一覧 | GET | {publication_url}/api/v1/drafts | filter, offset, limit |
| 単体取得 | GET | {publication_url}/api/v1/drafts/{id} | - |
| 削除 | DELETE | {publication_url}/api/v1/drafts/{id} | - |
| 公開済み一覧 | GET | {publication_url}/api/v1/post_management/published | offset, limit, order_by=post_date, order_direction=desc |
4.5 プレ公開 / 公開 / スケジュール
GET {publication_url}/api/v1/drafts/{id}/prepublish
POST {publication_url}/api/v1/drafts/{id}/publish body: {"send": true, "share_automatically": false}
POST {publication_url}/api/v1/drafts/{id}/schedule body: {"post_date": "2026-05-15T09:00:00"} # null でスケジュール解除4.6 画像アップロード
POST {publication_url}/api/v1/image
Cookie: substack.sid=...
Content-Type: application/x-www-form-urlencodedbody:
image=data:image/jpeg;base64,<BASE64エンコードしたファイルバイト列>- multipart ではなく form-encoded で送る。
requests.post(..., data={"image": "data:image/jpeg;base64,..."}) - 外部URLを直接渡すこともできる(その場合
data:プレフィックスなしの URL 文字列)。 - レスポンス例:json
{ "url": "https://substackcdn.com/image/fetch/.../<encoded>", "id": "...", "imageUrl": "...", "imageHeight": 819, "imageWidth": 1456, "imageType": "image/jpeg", "imageBytes": 12345, "explicit": false } - 取得した
urlをcaptionedImageノードのsrcに流し込む。 - サイズ上限・形式は公式ドキュメントなし。実観測では PNG/JPEG/GIF が通る。Substackヘルプは「最適表示幅 1456px」推奨。アップロード前に画像を 1456px に縮小・10MB 以下 で送るのが推奨。
4.7 タグ・セクション・カテゴリ
GET {publication_url}/api/v1/publication/post-tag # 既存タグ取得
POST {publication_url}/api/v1/publication/post-tag # body: {"name": "..."}
POST {publication_url}/api/v1/post/{post_id}/tag/{tag_id} # タグを post に付与
GET {publication_url}/api/v1/subscriptions # section 一覧が混ざる
GET {publication_url}/api/v1/categories
GET {publication_url}/api/v1/category/public/{cid}/{ctype}?page=4.8 認証
POST https://substack.com/api/v1/login body: {captcha_response, email, for_pub, password, redirect}
GET https://substack.com/sign-in?redirect=%2F&for_pub={subdomain}5. ProseMirror JSON スキーマ要約
ドキュメントルートは {"type": "doc", "content": [ ... ]}。draft_body は これを json.dumps した文字列として /drafts に送る。
5.1 主要ブロック
paragraph
{
"type": "paragraph",
"content": [
{"type": "text", "text": "通常文字"},
{"type": "text", "text": "bold ", "marks": [{"type": "strong"}]},
{"type": "text", "text": "italic ", "marks": [{"type": "em"}]},
{"type": "text", "text": "リンク", "marks": [
{"type": "link", "attrs": {"href": "https://example.com"}}
]},
{"type": "text", "text": "code", "marks": [{"type": "code"}]},
{"type": "text", "text": "del", "marks": [{"type": "strikethrough"}]}
]
}heading
{"type": "heading", "attrs": {"level": 2}, "content": [{"type":"text","text":"見出し"}]}level: 1〜6。
blockquote
{"type": "blockquote", "content": [
{"type": "paragraph", "content": [{"type":"text","text":"引用"}]}
]}horizontal_rule
{"type": "horizontal_rule"}codeBlock
{"type": "codeBlock", "attrs": {"language": "python"},
"content": [{"type":"text","text":"print(\"hi\")"}]}captionedImage(必ず image2 を含む)
{"type": "captionedImage", "content": [{
"type": "image2",
"attrs": {
"src": "https://substackcdn.com/image/fetch/.../xxxx.jpg",
"fullscreen": false,
"imageSize": "normal",
"height": 819,
"width": 1456,
"resizeWidth": 728,
"bytes": null,
"alt": null,
"title": null,
"type": "image/jpeg",
"href": null,
"belowTheFold": false,
"internalRedirect": null
}
}]}注意(観測情報・確度中): internalRedirect を空のまま送るとエディタが正しく開けないケースが報告されている。get_image のレスポンスに含まれていれば最優先で使う。bytes, alt, title, type も埋めるのが安全。
paywall / subscribeWidget / youtube2 / embeddedPublication
{"type": "paywall"}
{"type": "subscribeWidget",
"attrs": {"url": "%%checkout_url%%", "text": "Subscribe", "language": "en"},
"content": [{"type":"ctaCaption","content":[{"type":"text","text":"Subscribe message"}]}]}
{"type": "youtube2", "attrs": {"videoId": "https://www.youtube.com/watch?v=..."}}
{"type": "embeddedPublication", "attrs": { ... publication_embed の返却 ... }}5.2 対応マーク(inline)
| mark | JSON |
|---|---|
| strong (bold) | {"type":"strong"} |
| em (italic) | {"type":"em"} |
| code (inline) | {"type":"code"} |
| link | {"type":"link","attrs":{"href":"..."}} |
| strikethrough | {"type":"strikethrough"} |
5.3 確認未取得(要追加調査)
- footnote — Substack エディタには存在するがノードタイプ名未確認
- callout — 同上
- link card (
linkPreview) — 未確認 - LaTeX math — Substackは
math-block/math-inline系を持つはずだが python-substack には未実装 - bulletList / orderedList / listItem — ProseMirror標準名がそのまま使えると推定(要DevTools実観測)
→ 本リポ実装時に DevTools でエディタを操作 → POST /drafts の payload を観察 → ノード型を確定 する手順を踏む。
5.4 Markdown → ProseMirror 変換
python-substackがPost.from_markdownを内蔵(heading, paragraph, image, link, code block, blockquote, lists, hr, inline marks 対応)。第一選択。- 汎用には
prosemirror-py(PyPI) があるが Substack 固有ノード(captionedImage,paywall等)には未対応。
6. リスクと運用ルール
6.1 利用規約とBANリスク
- Substack TOS は scrape/crawl/spider/コンテンツ大量保存を明示禁止。「自分のパブリケーションへの下書き投稿」は禁止条項に直接触れないが、
自動化のために作成された inauthentic アカウント取締の対象になりうる。 - 公式コミュニティでは「個人利用での自動下書き作成でBANされた」報告は現状ほとんど見当たらないが、過去事例ゼロではない。
- 防御策:
- 1日あたりの draft 作成は ヒューマンペース(≤ 10件/日 目安)。
- 短時間に連続リクエストしない(最低 2〜5秒 sleep、指数バックオフ)。
- User-Agent を実ブラウザに合わせる(
python-substackは requests デフォルト。必要ならsession.headers.update({"User-Agent": "Mozilla/5.0 ..."})で上書き)。 - 1パブリケーション内に限定し、複数を巡回しない。
6.2 レートリミット
- 公式の rate limit 数値は非公開。
python-substackissue tracker にも 429 報告は現状なし。 - 暗黙的に rate limit はあると考え、429 / 5xx を捕捉して指数バックオフ(初期2秒, 最大60秒, 5回まで) をラッパに必ず実装。
- 大量画像アップロード時は 画像1枚あたり 0.5〜1秒 sleep。
6.3 仕様変更への耐性
過去観測された破壊的変更:
profile.primaryPublication→profile.publicationUsers[].publicationへの再編- CAPTCHA 強化(パスワードレス magic link 移行)
対策:
- エンドポイント呼び出しを単一モジュールに隔離(
substack_client.py) - 契約テスト(draft 作成→取得→削除)を CI で週次実行
- ライブラリは
requirements.txtで==ピン
6.4 「公式API」は使えるか
- 唯一の公式 = Substack Developer API(
GET https://substack.com/profile/search/linkedin/<handle>のみ)。LinkedInハンドルで Substack プロフィールを引くだけで、下書き作成・投稿・画像Upはサポート外。 - Webhook / RSS / メール経由のドラフト作成も現状なし(RSSは出力のみ)。
- → 本タスクは非公式エンドポイント以外に選択肢がない、と確定。
7. 実装ステップ提案(E-2〜E-6 具体化)
E-2 (#49): 認証フロー実装
scripts/substack_login.pyを作成: Playwright で対話的ログイン → Cookie を JSON出力 →security add-generic-passwordで Keychain 保存- ランタイムは
os.environ["SUBSTACK_COOKIES_STRING"]を読み、substack.Api(cookies_string=...)を初期化 - Cookie 失効検知: 任意APIで 401 が出たら「ログイン再実行を促す」例外を投げる
E-3 (#50): ライブラリ薄ラッパ作成
src/substack_client/__init__.py:
class SubstackClientをpython-substackのApiの上に作るcreate_draft(title, subtitle, body_md, *, images=None, tags=None) -> draft_idを提供- 内部で
Post.from_markdownを呼び、画像は事前にapi.get_imageでアップロードしsrcを CDN URL に置換 - 指数バックオフ + 429 ハンドリングを全メソッドにデコレータで適用
E-4 (#51): 画像生成パイプライン連携
- 記事生成 → Markdown 出力
- 画像生成(Higgsfield / DALL·E)→ ローカル
.tmp/*.png保存 - Markdown 内の
を検出 →api.get_image(local_path)→ CDN URL に rewrite - 図(Excalidraw → PNG)も同経路でアップロード
E-5 (#52): 下書きアップロード本体
client = SubstackClient.from_keychain()
draft = client.create_draft(
title=meta["title"],
subtitle=meta["subtitle"],
body_md=md_text,
audience="everyone",
tags=meta["tags"],
slug=meta["slug"],
)
print(f"Draft URL: https://{client.subdomain}.substack.com/publish/post/{draft['id']}")公開はしない(人間レビュー後にエディタから手動公開)。
E-6 (#53): 運用・監視
- 週次 smoke test(下書き作成→即削除)を GitHub Actions で実行
- ライブラリの新バージョン PR を Renovate で自動
- 失敗時 Slack/Discord 通知
- レート制御:
ratelimitパッケージか自前トークンバケットで「10 reqs / 60s」程度に抑制
8. 出典
コア(採用ライブラリ)
- python-substack (ma2za): https://github.com/ma2za/python-substack
- python-substack PyPI: https://pypi.org/project/python-substack/
- python-substack README: https://github.com/ma2za/python-substack/blob/main/README.md
- python-substack
substack/api.py: https://github.com/ma2za/python-substack/blob/main/substack/api.py - python-substack
substack/post.py: https://github.com/ma2za/python-substack/blob/main/substack/post.py - examples/publish_post.py: https://github.com/ma2za/python-substack/blob/main/examples/publish_post.py
- Jason Moggridge "How to publish a Substack post using the Python API": https://jasonmoggridge.substack.com/p/how-to-publish-a-substack-post-using-199
- Saverio Mazza "Harnessing the Power of Substack with Python": https://medium.com/@saveriomazza/harnessing-the-power-of-substack-with-python-a-deep-dive-fd4658491b6f
代替ライブラリ・参考実装
- NHagar/substack_api: https://github.com/NHagar/substack_api
- NHagar substack-api docs: https://nhagar.github.io/substack_api/
- JPres-Projects/Substack-API: https://github.com/JPres-Projects/Substack-API
- jakub-k-slys/substack-api (TS): https://github.com/jakub-k-slys/substack-api
- conorbronsdon/substack-mcp: https://github.com/conorbronsdon/substack-mcp
公式情報
- Substack Developer API(LinkedIn handle 専用): https://support.substack.com/hc/en-us/articles/45099095296916
- Substack Terms of Use: https://substack.com/tos
- Substack API ToS: https://substack.com/api-tos
- Substack 画像埋め込みヘルプ: https://support.substack.com/hc/en-us/articles/360037832971
- Substack 画像最適サイズ: https://support.substack.com/hc/en-us/articles/4408381685268
リバースエンジニアリング解説
- iam.slys.dev "No official API? No problem!": https://iam.slys.dev/p/no-official-api-no-problem-how-i
- Charlie Guo "How to reverse engineer the Substack API": https://www.ignorance.ai/p/how-to-reverse-engineer-the-substack-api
- thirdbear "Syncing Substack drafts with a Chrome extension": https://thirdbear.substack.com/p/syncing-substack-drafts-with-a-chrome
- Simon Willison "Semi-automating a Substack newsletter": https://simonwillison.net/2023/Apr/4/substack-observable/
ProseMirror
- ProseMirror Guide: https://prosemirror.net/docs/guide/
- ProseMirror schema example: https://prosemirror.net/examples/schema/
補足: 不確実情報のフラグ
| 情報 | 確度 | 補足 |
|---|---|---|
substack.sid Cookie名 | 高 | 複数ソースで確認 |
| Cookie 3ヶ月有効 | 中 | 1ソース報告。実環境で検証要 |
| 画像エンドポイントが form-encoded base64 | 高 | python-substack ソース直接確認 |
internalRedirect 必須 | 中 | MCP server の実装で報告。空でも通る例もあり |
| Rate limit 具体数値 | 低 | 公式・観測ともに非公開 |
| TOS で「自パブリケーションへの自動下書き」が違反か | 低 | 明示条項なし。scrape禁止条項とのグレー領域 |
| footnote/callout/link card ノード型 | 不明 | python-substack 未実装。DevTools 実観測必要 |