Skip to content

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_api v1.2.0 (★194) — read-only のみ(投稿不可)。本タスクの下書きアップロードには使えない。閲覧系のみ最も成熟。
    • JPres-Projects/Substack-API (★2) — マイナー、画像アップロード非対応。非推奨。
    • 自前実装 — python-substack のフォーク or 薄いラッパとして「危険箇所のみ自前で叩く」ハイブリッドが現実的。
  • 主要リスク:
    1. Substack TOS は "automated means" による scrape/crawl/spider を禁止(下書きアップロードがそれに直結するかはグレー)。
    2. 非公式エンドポイントは予告なく仕様変更される(primaryPublicationpublicationUsers 再編の前例)。
    3. Email+Password ログインは CAPTCHA に詰まりやすい。Cookie 方式運用が必須級

1. 既存ライブラリ比較

ライブラリLangForkLast pushReadDraft作成画像Up公開認証採用度
ma2za/python-substack v0.1.22Python ≥3.10150252026-05-08post_draftget_image(base64)publish_draftemail/pw, cookies.json, cookies_string★★★★★
NHagar/substack_api v1.2.0Python ≥3.12194312026-03-16×××optional cookies★★(読み取り用途のみ)
JPres-Projects/Substack-API v1.0.0Python212025-09-06○(markup記法)×env cookies
jakub-k-slys/substack-apiTypeScriptn/an/aactive部分cookies参考実装
conorbronsdon/substack-mcpMCP servern/an/aactive×意図的に非対応cookies参考実装

結論: 書き込み系を全部カバーする現役Python実装は ma2za/python-substack 一択。


2. 推奨実装方針

ハイブリッド構成

  1. 下地として python-substack を採用pip install python-substack)。ApiPost の責務(HTTP呼び出し・ProseMirror組み立て)を活用。
  2. 本リポジトリ独自層で次を実装:
    • 認証情報の Keychain ロード(security find-generic-password -s SUBSTACK_COOKIES_STRING -a "$USER" -w)。
    • リトライ・指数バックオフ・429ハンドリング(ライブラリは未実装)。
    • スキーマ変更検知(primaryPublicationpublicationUsers 系の差異吸収)。
    • 画像 CDN URL の internalRedirect 補完。
  3. ピン留め: python-substack==0.1.22 で固定。CI で「下書きを作って即削除する smoke test」を週次実行し回帰検知。
  4. 将来の脱却ルート: ライブラリが消えたときに自前実装に切替できるよう、エンドポイント呼び出しは1モジュールに隔離(substack_client.py)。

「自前実装オンリー」は非推奨: ライブラリの Post.add / from_markdown / captioned_image が実観測ベースのProseMirrorノード構築を約600行担っており、再実装コストが大きい。


3. 認証フロー

3.1 Cookie取得(推奨運用フロー)

  1. ブラウザで https://substack.com にログイン
  2. DevTools → Application → Cookies → https://substack.com
  3. 重要 Cookie:
    • substack.sid — メインのセッションID(Domain=.substack.com, Path=/, Secure, HttpOnly, 約3ヶ月有効)
    • substack.lli — ログイン補助
    • sid — 補助
  4. Cookie値を Keychain に保存:
    bash
    security add-generic-password -s "SUBSTACK_COOKIES_STRING" -a "$USER" \
      -w "substack.sid=...; substack.lli=...; sid=..." -U
  5. .zshrc に参照を追加:
    bash
    export 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-substackApi.__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

json
{
  "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 ユーザー情報

操作MethodPath
自分のプロフィール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/json

body 例:

json
{
  "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_bodyJSON文字列としてエスケープして送る(オブジェクトのままではない)。
  • draft_bylines[*].idint(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 下書き取得 / 一覧 / 削除

操作MethodPathパラメータ
一覧GET{publication_url}/api/v1/draftsfilter, offset, limit
単体取得GET{publication_url}/api/v1/drafts/{id}-
削除DELETE{publication_url}/api/v1/drafts/{id}-
公開済み一覧GET{publication_url}/api/v1/post_management/publishedoffset, 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-urlencoded

body:

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
    }
  • 取得した urlcaptionedImage ノードの 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

json
{
  "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

json
{"type": "heading", "attrs": {"level": 2}, "content": [{"type":"text","text":"見出し"}]}

level: 1〜6。

blockquote

json
{"type": "blockquote", "content": [
  {"type": "paragraph", "content": [{"type":"text","text":"引用"}]}
]}

horizontal_rule

json
{"type": "horizontal_rule"}

codeBlock

json
{"type": "codeBlock", "attrs": {"language": "python"},
 "content": [{"type":"text","text":"print(\"hi\")"}]}

captionedImage(必ず image2 を含む)

json
{"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

json
{"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)

markJSON
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-substackPost.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-substack issue tracker にも 429 報告は現状なし。
  • 暗黙的に rate limit はあると考え、429 / 5xx を捕捉して指数バックオフ(初期2秒, 最大60秒, 5回まで) をラッパに必ず実装。
  • 大量画像アップロード時は 画像1枚あたり 0.5〜1秒 sleep

6.3 仕様変更への耐性

過去観測された破壊的変更:

  • profile.primaryPublicationprofile.publicationUsers[].publication への再編
  • CAPTCHA 強化(パスワードレス magic link 移行)

対策:

  • エンドポイント呼び出しを単一モジュールに隔離(substack_client.py
  • 契約テスト(draft 作成→取得→削除)を CI で週次実行
  • ライブラリは requirements.txt== ピン

6.4 「公式API」は使えるか

  • 唯一の公式 = Substack Developer APIGET https://substack.com/profile/search/linkedin/<handle> のみ)。LinkedInハンドルで Substack プロフィールを引くだけで、下書き作成・投稿・画像Upはサポート外
  • Webhook / RSS / メール経由のドラフト作成も現状なし(RSSは出力のみ)。
  • → 本タスクは非公式エンドポイント以外に選択肢がない、と確定。

7. 実装ステップ提案(E-2〜E-6 具体化)

E-2 (#49): 認証フロー実装

  1. scripts/substack_login.py を作成: Playwright で対話的ログイン → Cookie を JSON出力 → security add-generic-password で Keychain 保存
  2. ランタイムは os.environ["SUBSTACK_COOKIES_STRING"] を読み、substack.Api(cookies_string=...) を初期化
  3. Cookie 失効検知: 任意APIで 401 が出たら「ログイン再実行を促す」例外を投げる

E-3 (#50): ライブラリ薄ラッパ作成

src/substack_client/__init__.py:

  • class SubstackClientpython-substackApi の上に作る
  • 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): 画像生成パイプライン連携

  1. 記事生成 → Markdown 出力
  2. 画像生成(Higgsfield / DALL·E)→ ローカル .tmp/*.png 保存
  3. Markdown 内の ![alt](local_path) を検出 → api.get_image(local_path) → CDN URL に rewrite
  4. 図(Excalidraw → PNG)も同経路でアップロード

E-5 (#52): 下書きアップロード本体

python
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. 出典

コア(採用ライブラリ)

代替ライブラリ・参考実装

公式情報

リバースエンジニアリング解説

ProseMirror


補足: 不確実情報のフラグ

情報確度補足
substack.sid Cookie名複数ソースで確認
Cookie 3ヶ月有効1ソース報告。実環境で検証要
画像エンドポイントが form-encoded base64python-substack ソース直接確認
internalRedirect 必須MCP server の実装で報告。空でも通る例もあり
Rate limit 具体数値公式・観測ともに非公開
TOS で「自パブリケーションへの自動下書き」が違反か明示条項なし。scrape禁止条項とのグレー領域
footnote/callout/link card ノード型不明python-substack 未実装。DevTools 実観測必要

substack-skill 内部設計書 — 公開非対象