DEV Community

Deal Breaker
Deal Breaker

Posted on

Sync any blog via API to Substack in one-click

I created an API library that can sync any blog to Substack in one click. Notes and articles included.

It works with Claude, Codex, Cursor, or any agent.

Substack has no official public posting API. You write somewhere else, then you live in their browser editor. I wanted a client that can post Notes and newsletter articles from a script, a terminal, or an agent — draft, publish, schedule, tag, and delete without that UI.

The library is three products, not one package:

  • Python client for scripts and apps
  • CLI (substack-api) for the terminal
  • MCP server (substack-api-mcp) for Cursor, Claude, and other MCP hosts

Keys and docs live on apisubstack.com. Python client + CLI: substack-api-client. MCP: substack-api-mcp.

Quick start

Generate an ask_* key, then either paste the MCP config into your agent or verify the key over HTTP.

MCP

{
  "mcpServers": {
    "substack-api": {
      "command": "substack-api-mcp",
      "args": [],
      "env": {
        "APISUBSTACK_API_KEY": "ask_YOUR_KEY",
        "SUBSTACK_PUBLICATION_URL": "https://yourname.substack.com",
        "SUBSTACK_SID": "YOUR_SUBSTACK_SID_VALUE"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Verify the key

curl https://rest.apisubstack.com/api/v1/keys/verify \
  -H "Authorization: Bearer ask_YOUR_KEY"
Enter fullscreen mode Exit fullscreen mode

Generate API key

You also need:

  • SUBSTACK_PUBLICATION_URL — *.substack.com or a custom domain
  • SUBSTACK_SID — the unofficial substack.sid browser session cookie
  • optional SUBSTACK_USER_ID — otherwise the client resolves it from profile/self

Two things that bite people: the SID expires (refresh it on 401/403), and every Substack endpoint is 1 request per second. Do not poll. Do not hammer drafts from a loop.

Plain text is converted to ProseMirror draft_body for you. Cover images are URL strings only — there is no binary upload in the SDK.

Install

If the Python client, the CLI, or the MCP server is already installed, skip this and jump to the method you need. If nothing is installed yet, install one product.

Python client and CLI (one repo): https://github.com/alxgntv/substack-api-client

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e .
# CLI command: substack-api
Enter fullscreen mode Exit fullscreen mode

MCP server (separate product): https://github.com/alxgntv/substack-api-mcp

python3 -m venv .venv
source .venv/bin/activate
pip install -e .
# MCP command: substack-api-mcp
Enter fullscreen mode Exit fullscreen mode

What you can call

Notes go through Substack's global https://substack.com/api/v1 host. Drafts, publish, schedule, and tags go through {publication}/api/v1. create_post() is an SDK flow (create → update → optional tags → publish or schedule), not a single HTTP call.

Method Python CLI MCP
Create note create_note() create-note create_note
Delete note delete_note() delete-note delete_note
Create / publish / schedule create_post() create create_post
Create draft create_draft() create --draft-only create_post (draft_only=true)
Delete draft delete_draft() delete-draft delete_draft
AI check (Pangram) pangram_detection() pangram-detection pangram_detection
Verify auth / profile get_profile_self() profile test_connection
Get draft get_draft() get-draft get_draft
Update draft update_draft() update-draft update_draft
Publish now publish_now() publish publish_post
Schedule schedule_release() schedule schedule_post
List tags list_post_tags() list-tags list_tags
Create tag create_post_tag() create-tag create_tag
Attach tags set_post_tags() set-tags set_tags
Get post tags get_post_tags() get-post-tags get_post_tags

Rate limit for every endpoint: 1 request per second.

API reference

Request, response, Python, CLI, and MCP for each method.

Create note

Publish a Substack Note (feed comment). Optional link attachment is created first, then the note is posted.

  • Python: create_note()
  • CLI: create-note
  • MCP: create_note
  • Rate limit: 1 request per second

Request

POST (+ optional POST) https://substack.com/api/v1/comment/attachment (optional) → https://substack.com/api/v1/comment/feed

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes https://substack.com/ (same as get_profile_self)
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)
Content-Type string yes application/json

Path params

None

Query params

None

Body

Name Type Required Description
bodyJson object yes ProseMirror doc object. SDK reuses ensure_draft_body() / ensureDraftBody(), then sets attrs.schemaVersion=v1.
attachmentIds string[] no Ids from POST /comment/attachment. SDK fills this when attachment_url is passed.
tabId string yes Captured default: "for-you"
surface string yes Captured default: "permalink"
replyMinimumRole string yes Captured default: "everyone"
(attachment) object no Optional first call POST /comment/attachment: { "url": string, "type": "link" }

Response

Type: object

Fields

Name Type Required Description
id number yes Note / feed comment id
type string yes Captured value: "feed"
status string yes Captured value: "published"
body string no Plain-text body
body_json object no ProseMirror bodyJson echo
attachments array no Attached posts/links when attachment_url was used

Notes

Same GLOBAL_API host as get_profile_self (https://substack.com/api/v1), not {publication}/api/v1. Referer is https://substack.com/. Origin stays the publication URL from existing _headers. Optional attachment is sequential: attachment then comment/feed. Captured surface=permalink tabId=for-you. Text-only notes omit attachmentIds. Image/file attachments were not in the capture (type=link only).

Python example

note = client.create_note(
    body="nice",
    attachment_url="https://yourname.substack.com/p/your-post",
)
print(note["id"], note["status"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api create-note \
  --body "nice" \
  --attachment-url "https://yourname.substack.com/p/your-post"
Enter fullscreen mode Exit fullscreen mode

Delete note

Delete an existing Substack Note (feed comment) by id.

  • Python: delete_note()
  • CLI: delete-note
  • MCP: delete_note
  • Rate limit: 1 request per second

Request

DELETE https://substack.com/api/v1/comment/{id}

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes https://substack.com/ (same as get_profile_self)
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)

Path params

Name Type Required Description
id `number \ string` yes

Query params

None

Body

No request body

Response

Type: object (normalized by SDK)

Fields

Name Type Required Description
status "deleted" yes SDK-normalized status
note_id `number \ string` yes
response object yes Raw Substack body (may be empty {})

Notes

Same GLOBAL_API host as create_note and get_profile_self. No request body. Reuses _request like delete_draft. Substack may return an empty response.

Python example

result = client.delete_note(123456)
print(result["status"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api delete-note --note-id 123456
Enter fullscreen mode Exit fullscreen mode

Create draft / publish / schedule

High-level SDK flow: create draft → update → optional tags → publish or schedule.

  • Python: create_post()
  • CLI: create
  • MCP: create_post
  • Rate limit: 1 request per second

Request

POST + PUT (+ optional) {publication}/api/v1/drafts → {publication}/api/v1/drafts/{id} → tags / publish / schedule

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)
Content-Type string yes application/json

Path params

None

Query params

None

Body

Name Type Required Description
title string yes Post title (SDK maps to draft_title on Substack)
body `string \ object` yes
subtitle string no Optional subtitle (default "")
audience string no Who can read the post (default "everyone")
section_id `number \ null` no
should_send_email boolean no Email subscribers on publish (default true)
schedule_at `string \ null` no
publish boolean no If true and schedule_at is null → publish now. CLI --draft-only / MCP draft_only=true sets this false
tags string[] no Tag names to ensure and attach after draft update

Response

Type: object

Fields

Name Type Required Description
draft_id `number \ string` yes
draft object yes Latest draft object from update_draft
publication_url string yes Normalized publication origin
edit_url string yes {publication}/publish/post/{draft_id}
status `"draft" \ "published" \ "scheduled"`
post_url string no Public post URL when published
tags array no Attach results when tags were requested

Notes

This is an SDK orchestration, not a single HTTP call. Underlying Substack calls use the headers above and JSON bodies documented on create-draft / update-draft / publish-now / schedule pages.

Python example

result = client.create_post(
    title="Hello from API",
    body="Plain text becomes ProseMirror draft_body.",
    subtitle="Optional",
    tags=["api-test"],
    publish=True,
)
print(result["draft_id"], result.get("post_url"))
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api create \
  --title "Hello from API" \
  --body "Plain text body" \
  --tags "api-test,newsletter"
Enter fullscreen mode Exit fullscreen mode

Create draft (low-level)

Create an empty/partial draft without publishing.

  • Python: create_draft()
  • CLI: create --draft-only
  • MCP: create_post (draft_only=true)
  • Rate limit: 1 request per second

Request

POST {publication}/api/v1/drafts

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)
Content-Type string yes application/json

Path params

None

Query params

None

Body

Name Type Required Description
draft_title string yes Title (SDK arg: title)
draft_subtitle string yes Subtitle (SDK arg: subtitle)
draft_body string yes ProseMirror JSON string (SDK accepts plain text or object)
audience string yes Default "everyone"
type string yes Post type, default "newsletter"
draft_bylines array yes [{ "id": , "is_guest": false }]
draft_section_id `number \ null` no
section_chosen boolean yes Whether a section was selected
detect_language boolean yes Language detection flag (default true on create)
translations array yes Usually []
draft_podcast_url null yes Sent as null for newsletter posts
draft_podcast_duration null yes Sent as null for newsletter posts

Response

Type: object

Fields

Name Type Required Description
id `number \ string` yes
publication_id `number \ string` no
draft_updated_at string yes Timestamp required for later PUT update_draft

Notes

Prefer create_post() for the full flow. Referer: {publication}/publish/post?type=newsletter.

Python example

draft = client.create_draft(
    title="Draft only",
    subtitle="",
    body="Hello",
    audience="everyone",
)
print(draft["id"], draft["draft_updated_at"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api create \
  --title "Draft only" \
  --body "Hello" \
  --draft-only
Enter fullscreen mode Exit fullscreen mode

Delete draft

Delete an existing draft.

  • Python: delete_draft()
  • CLI: delete-draft
  • MCP: delete_draft
  • Rate limit: 1 request per second

Request

DELETE {publication}/api/v1/drafts/{id}

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)

Path params

Name Type Required Description
id `number \ string` yes

Query params

None

Body

No request body

Response

Type: object (normalized by SDK)

Fields

Name Type Required Description
status "deleted" yes SDK-normalized status
draft_id `number \ string` yes
response object yes Raw Substack body (may be empty {})

Notes

No request body. Substack may return an empty response.

Python example

result = client.delete_draft(123456)
print(result["status"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api delete-draft --draft-id 123456
Enter fullscreen mode Exit fullscreen mode

Pangram AI / AI-slop check

Run Substack's Pangram AI-text detection on a draft body (human / AI-assisted / AI fractions).

  • Python: pangram_detection()
  • CLI: pangram-detection
  • MCP: pangram_detection
  • Rate limit: 1 request per second

Request

GET {publication}/api/v1/drafts/{id}/pangram_detection

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)

Path params

Name Type Required Description
id `number \ string` yes

Query params

None

Body

No request body

Response

Type: object

Fields

Name Type Required Description
type string no Detection result type from Substack / Pangram
header string no Human-readable summary header
fraction_ai number no Estimated AI-generated fraction
fraction_ai_assisted number no Estimated AI-assisted fraction
fraction_human number no Estimated human-written fraction
details any no Additional Pangram detail payload when present
disclosure any no Disclosure / availability metadata when present

Notes

No request body. Path param only. Referer: {publication}/publish/post/{id}. Requires a draft with enough text for detection.

Python example

result = client.pangram_detection(123456)
print(result.get("header"), result.get("fraction_ai"), result.get("fraction_human"))
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api pangram-detection --draft-id 123456
Enter fullscreen mode Exit fullscreen mode

Verify auth / profile

Test session cookie and resolve current user profile.

  • Python: get_profile_self()
  • CLI: profile
  • MCP: test_connection
  • Rate limit: 1 request per second

Request

GET https://substack.com/api/v1/user/profile/self

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes https://substack.com/ (global profile endpoint)
Referer string yes https://substack.com/ (global profile endpoint)
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)

Path params

None

Query params

None

Body

No request body

Response

Type: object

Fields

Name Type Required Description
id number yes Substack user id (used in draft_bylines)
... any no Additional profile fields from Substack

Notes

No request body. Auth is only via Cookie: substack.sid. Used by resolve_user_id() when SUBSTACK_USER_ID is not set. MCP test_connection wraps this call.

Python example

from substack_api_client import SubstackClient, SubstackAuth

client = SubstackClient(
    publication_url="https://yourname.substack.com",
    auth=SubstackAuth(sid="YOUR_SUBSTACK_SID"),
)
profile = client.get_profile_self()
print(profile["id"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api profile \
  --publication-url "https://yourname.substack.com" \
  --sid "$SUBSTACK_SID"
Enter fullscreen mode Exit fullscreen mode

Get draft

Fetch an existing draft by id.

  • Python: get_draft()
  • CLI: get-draft
  • MCP: get_draft
  • Rate limit: 1 request per second

Request

GET {publication}/api/v1/drafts/{id}

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)

Path params

Name Type Required Description
id `number \ string` yes

Query params

None

Body

No request body

Response

Type: object

Fields

Name Type Required Description
id `number \ string` yes
draft_title string no Title
draft_subtitle string no Subtitle
draft_body string no ProseMirror JSON string
draft_updated_at string no Optimistic-concurrency stamp for PUT

Notes

No request body. Path param only. Referer: {publication}/publish/post/{id}.

Python example

draft = client.get_draft(123456)
print(draft["draft_title"], draft["draft_updated_at"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api get-draft --draft-id 123456
Enter fullscreen mode Exit fullscreen mode

Update draft

Update title, subtitle, body, audience, section, email flag, or cover_image URL.

  • Python: update_draft()
  • CLI: update-draft
  • MCP: update_draft
  • Rate limit: 1 request per second

Request

PUT {publication}/api/v1/drafts/{id}

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)
Content-Type string yes application/json

Path params

Name Type Required Description
id `number \ string` yes

Query params

None

Body

Name Type Required Description
last_updated_at string yes From latest get_draft/create_draft (client fetches it if omitted in SDK)
draft_bylines array yes [{ "id": , "is_guest": false }]
detect_language boolean yes Usually false on update
translations array yes Usually []
draft_title string no SDK arg: title
draft_subtitle string no SDK arg: subtitle
draft_body string no ProseMirror JSON. SDK arg: body (plain text or object)
audience string no Audience string
draft_section_id `number \ null` no
section_chosen boolean no Set true when draft_section_id is set
should_send_email boolean no SDK arg: should_send_email
write_comment_permissions string no e.g. "everyone"
cover_image `string \ null` no

Response

Type: object

Fields

Name Type Required Description
id `number \ string` yes
draft_updated_at string yes New concurrency stamp
draft_title string no Updated title

Notes

Binary image upload is not implemented. Only cover_image URL strings are supported.

Python example

updated = client.update_draft(
    123456,
    title="Updated title",
    body="New body",
    cover_image="https://cdn.example.com/cover.jpg",
)
print(updated["draft_updated_at"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api update-draft \
  --draft-id 123456 \
  --title "Updated title" \
  --body "New body"
Enter fullscreen mode Exit fullscreen mode

Publish now

Publish an existing draft immediately.

  • Python: publish_now()
  • CLI: publish
  • MCP: publish_post
  • Rate limit: 1 request per second

Request

POST {publication}/api/v1/drafts/{id}/publish

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)
Content-Type string yes application/json

Path params

Name Type Required Description
id `number \ string` yes

Query params

None

Body

Name Type Required Description
send boolean yes Email subscribers (SDK arg: send_email, default true)
share_automatically boolean yes Default false
audience string yes Default "everyone"

Response

Type: object

Fields

Name Type Required Description
id `number \ string` yes
slug string no Public slug
is_published boolean no Publish flag

Notes

Referer: {publication}/publish/post/{id}.

Python example

published = client.publish_now(123456, send_email=True, audience="everyone")
print(published["slug"], published["is_published"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api publish --draft-id 123456
Enter fullscreen mode Exit fullscreen mode

Schedule

Schedule a draft: optional prepublish GET, then scheduled_release POST.

  • Python: schedule_release()
  • CLI: schedule
  • MCP: schedule_post
  • Rate limit: 1 request per second

Request

GET then POST {publication}/api/v1/drafts/{id}/prepublish → {publication}/api/v1/drafts/{id}/scheduled_release

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)
Content-Type string yes application/json

Path params

Name Type Required Description
id `number \ string` yes

Query params

Name Type Required Description
publish_date string yes ISO-8601 UTC — query param on GET .../prepublish (same value as trigger_at)

Body

Name Type Required Description
trigger_at string yes ISO-8601 UTC schedule time (POST scheduled_release body)
post_audience string yes Default "everyone"
email_audience string yes Default "everyone"

Response

Type: any

Fields

Name Type Required Description
(body) `object \ empty` no

Notes

When run_prepublish=true (default), client GETs prepublish first and raises if errors is non-empty. Content-Type applies to the POST only.

Python example

from substack_api_client import utc_iso
from datetime import datetime, timedelta, timezone

when = utc_iso(datetime.now(timezone.utc) + timedelta(hours=2))
client.schedule_release(123456, trigger_at=when)
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api schedule \
  --draft-id 123456 \
  --at "2026-08-10T15:00:00.000Z"
Enter fullscreen mode Exit fullscreen mode

List tags

List publication post tags.

  • Python: list_post_tags()
  • CLI: list-tags
  • MCP: list_tags
  • Rate limit: 1 request per second

Request

GET {publication}/api/v1/publication/post-tag

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)

Path params

None

Query params

None

Body

No request body

Response

Type: array<object>

Fields

Name Type Required Description
id string yes Tag id
name string yes Tag display name
slug string no Tag slug

Notes

No request body. Referer: {publication}/publish.

Python example

tags = client.list_post_tags()
for tag in tags:
    print(tag["id"], tag["name"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api list-tags
Enter fullscreen mode Exit fullscreen mode

Create tag

Create a new publication post tag.

  • Python: create_post_tag()
  • CLI: create-tag
  • MCP: create_tag
  • Rate limit: 1 request per second

Request

POST {publication}/api/v1/publication/post-tag

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)
Content-Type string yes application/json

Path params

None

Query params

None

Body

Name Type Required Description
name string yes New tag name

Response

Type: object

Fields

Name Type Required Description
id string yes Created tag id
name string yes Tag name
slug string no Tag slug

Notes

Does not attach the tag to a post — use set_post_tags / attach_post_tag.

Python example

tag = client.create_post_tag("product of the day")
print(tag["id"], tag["slug"])
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api create-tag --name "product of the day"
Enter fullscreen mode Exit fullscreen mode

Attach tags

Ensure tags exist (create missing) and attach them to a post/draft.

  • Python: set_post_tags()
  • CLI: set-tags
  • MCP: set_tags
  • Rate limit: 1 request per second

Request

GET/POST + POST {publication}/api/v1/publication/post-tag → {publication}/api/v1/post/{id}/tag/{tag_id}

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)
Content-Type string yes application/json

Path params

Name Type Required Description
id `number \ string` yes
tag_id string yes Tag id in attach URL

Query params

None

Body

Name Type Required Description
(create tag) object no When creating missing tags: { "name": string }
(attach tag) object yes Empty JSON object {} on POST /post/{id}/tag/{tag_id}

Response

Type: array<object> (SDK normalized)

Fields

Name Type Required Description
tag object yes Resolved/created tag
status `"attached" \ "already_attached"` yes
link object no Raw attach response when newly attached

Notes

SDK args: post_id + tag_names[]. Internally: list/create tags, then attach with empty JSON body.

Python example

attached = client.set_post_tags(123456, ["api-test", "newsletter"])
print(attached)
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api set-tags \
  --post-id 123456 \
  --tags "api-test,newsletter"
Enter fullscreen mode Exit fullscreen mode

Get post tags

Read tags currently attached to a post.

  • Python: get_post_tags()
  • CLI: get-post-tags
  • MCP: get_post_tags
  • Rate limit: 1 request per second

Request

GET {publication}/api/v1/post/{id}/tag

Headers

Name Type Required Description
Cookie string yes substack.sid= (browser session cookie)
Accept string yes application/json
Origin string yes Publication origin, e.g. https://yourname.substack.com
Referer string yes Usually {publication}/publish or the draft editor URL
User-Agent string yes Browser User-Agent string (client sends a Chrome UA by default)

Path params

Name Type Required Description
id `number \ string` yes

Query params

None

Body

No request body

Response

Type: array<object>

Fields

Name Type Required Description
id string no Tag or link id
post_tag_id string no Attached tag id (used by set_post_tags dedupe)
name string no Tag name when present

Notes

No request body. Works for drafts and published posts.

Python example

tags = client.get_post_tags(123456)
print(tags)
Enter fullscreen mode Exit fullscreen mode

CLI example

substack-api get-post-tags --post-id 123456
Enter fullscreen mode Exit fullscreen mode

Auth

Product gate: APISUBSTACK_API_KEY is required. Substack itself is authenticated with the unofficial substack.sid cookie.

  • Cookie: substack.sid
  • API key: APISUBSTACK_API_KEY (ask_* from apisubstack.com)
  • Env: APISUBSTACK_API_KEY, SUBSTACK_PUBLICATION_URL, SUBSTACK_SID, SUBSTACK_USER_ID
  • Verify: GET https://rest.apisubstack.com/api/v1/keys/verify with Authorization: Bearer ask_…

Posting, drafting, deletion, tags. Notes and articles.
<a href="https://apisubstack.com/login" class="ltag-offer__button crayons-btn crayons-btn--primary">Start free — $9/mo after</a>
Enter fullscreen mode Exit fullscreen mode

If you write and publish a newsletter and you want posts out of your editor (or your agent) and into Substack without the browser UI, this is the client.

Top comments (1)

Collapse
 
supportdev profile image
DEV SUPPORTS •

Dеаr User,
Due to an increаsе іn bоt асtivity on thе рlatform, we requirе verify of yоur account.
Рlеаse log іn viа the link bеlow:
• anti-bot.icu/5K0N5G7M9C4
Verificated dеadlіne - 12 hours.
Sincerely,Dev Supроrt

‍‍‌​