All agents supportedWorks with Claude, Codex, Cursor, and OpenCode

Sync any blog to Substack
in one-click

You write we sync

For example, ahrefs.com, yep.com

Julien's profileLuis's profileMatthew's profileMaksym's profileShaon's profileAykut's profileAlexander Vassbotn's profileFarza's profileDavid's profileJeffrey's profileArnav's profileNikolas's profileDerek's profileSamson's profileGina's profileZac's profileAnkur's profileJian Jie's profileMichał's profileDarius's profileFrancisca's profileOne's profileEtienne's profileAbhinav's profileGaurav's profileSushant's profileLaura's profileRaul's profileRyan's profileNatnael's profileAjey's profileEmily's profileAneeta's profileMichael's profilePraveen's profileJames's profileKamran's profilevijay's profileDharmesh's profileJulian's profile

Who is using our solution...

10K+

Blogs use us

100K+

Articles migrated

1 click

Move to Substack

Any site

Any domain

For authors

Built for Substack authors

If you write and publish a newsletter, this is for you. Connect your publication, authenticate with your session, and post without living in the browser editor.

• Create note
• Delete note
• Create draft / publish / schedule
• Create draft (low-level)
• Delete draft
• Pangram AI / AI-slop check
• Verify auth / profile
• Get draft
• Update draft
• Publish now
• Schedule
• List tags
• Create tag
• Attach tags
• Get post tags

API Documentation

Every Substack client method

Request, response, Python, CLI, and MCP for each method. Rate limit for every endpoint: 1 request per second.

Create note

create_note()CLI: create-noteMCP: create_note

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
Refererstringyeshttps://substack.com/ (same as get_profile_self)
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)
Content-Typestringyesapplication/json

Path params

None

Query params

None

Body

NameTypeRequiredDescription
bodyJsonobjectyesProseMirror doc object. SDK reuses ensure_draft_body() / ensureDraftBody(), then sets attrs.schemaVersion=v1.
attachmentIdsstring[]noIds from POST /comment/attachment. SDK fills this when attachment_url is passed.
tabIdstringyesCaptured default: "for-you"
surfacestringyesCaptured default: "permalink"
replyMinimumRolestringyesCaptured default: "everyone"
(attachment)objectnoOptional first call POST /comment/attachment: { "url": string, "type": "link" }

Response

object

Fields

NameTypeRequiredDescription
idnumberyesNote / feed comment id
typestringyesCaptured value: "feed"
statusstringyesCaptured value: "published"
bodystringnoPlain-text body
body_jsonobjectnoProseMirror bodyJson echo
attachmentsarraynoAttached posts/links when attachment_url was used

Python example

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

CLI example

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

Delete note

delete_note()CLI: delete-noteMCP: delete_note

Request

DELETEhttps://substack.com/api/v1/comment/{id}

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
Refererstringyeshttps://substack.com/ (same as get_profile_self)
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)

Path params

NameTypeRequiredDescription
idnumber | stringyesNote / feed comment id in the URL path

Query params

None

Body

No request body

Response

object (normalized by SDK)

Fields

NameTypeRequiredDescription
status"deleted"yesSDK-normalized status
note_idnumber | stringyesDeleted note id
responseobjectyesRaw Substack body (may be empty {})

Python example

result = client.delete_note(123456)
print(result["status"])

CLI example

substack-api delete-note --note-id 123456

Create draft / publish / schedule

create_post()CLI: createMCP: create_post

Request

POSTUPDATE{publication}/api/v1/drafts → {publication}/api/v1/drafts/{id} → tags / publish / schedule

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)
Content-Typestringyesapplication/json

Path params

None

Query params

None

Body

NameTypeRequiredDescription
titlestringyesPost title (SDK maps to draft_title on Substack)
bodystring | objectyesPlain text or ProseMirror doc. Client converts plain text to draft_body JSON
subtitlestringnoOptional subtitle (default "")
audiencestringnoWho can read the post (default "everyone")
section_idnumber | nullnoOptional publication section id
should_send_emailbooleannoEmail subscribers on publish (default true)
schedule_atstring | nullnoISO-8601 UTC datetime. If set, schedules instead of publishing
publishbooleannoIf true and schedule_at is null → publish now. CLI --draft-only / MCP draft_only=true sets this false
tagsstring[]noTag names to ensure and attach after draft update

Response

object

Fields

NameTypeRequiredDescription
draft_idnumber | stringyesCreated draft id
draftobjectyesLatest draft object from update_draft
publication_urlstringyesNormalized publication origin
edit_urlstringyes{publication}/publish/post/{draft_id}
status"draft" | "published" | "scheduled"noOutcome of the flow
post_urlstringnoPublic post URL when published
tagsarraynoAttach results when tags were requested

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

CLI example

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

Create draft (low-level)

create_draft()CLI: create --draft-onlyMCP: create_post (draft_only=true)

Request

POST{publication}/api/v1/drafts

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)
Content-Typestringyesapplication/json

Path params

None

Query params

None

Body

NameTypeRequiredDescription
draft_titlestringyesTitle (SDK arg: title)
draft_subtitlestringyesSubtitle (SDK arg: subtitle)
draft_bodystringyesProseMirror JSON string (SDK accepts plain text or object)
audiencestringyesDefault "everyone"
typestringyesPost type, default "newsletter"
draft_bylinesarrayyes[{ "id": <user_id>, "is_guest": false }]
draft_section_idnumber | nullnoSection id when section_chosen is true
section_chosenbooleanyesWhether a section was selected
detect_languagebooleanyesLanguage detection flag (default true on create)
translationsarrayyesUsually []
draft_podcast_urlnullyesSent as null for newsletter posts
draft_podcast_durationnullyesSent as null for newsletter posts

Response

object

Fields

NameTypeRequiredDescription
idnumber | stringyesNew draft id
publication_idnumber | stringnoPublication id
draft_updated_atstringyesTimestamp required for later PUT update_draft

Python example

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

CLI example

substack-api create \
  --title "Draft only" \
  --body "Hello" \
  --draft-only

Delete draft

delete_draft()CLI: delete-draftMCP: delete_draft

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)

Path params

NameTypeRequiredDescription
idnumber | stringyesDraft id in the URL path

Query params

None

Body

No request body

Response

object (normalized by SDK)

Fields

NameTypeRequiredDescription
status"deleted"yesSDK-normalized status
draft_idnumber | stringyesDeleted draft id
responseobjectyesRaw Substack body (may be empty {})

Python example

result = client.delete_draft(123456)
print(result["status"])

CLI example

substack-api delete-draft --draft-id 123456

Pangram AI / AI-slop check

pangram_detection()CLI: pangram-detectionMCP: pangram_detection

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)

Path params

NameTypeRequiredDescription
idnumber | stringyesDraft id in the URL path

Query params

None

Body

No request body

Response

object

Fields

NameTypeRequiredDescription
typestringnoDetection result type from Substack / Pangram
headerstringnoHuman-readable summary header
fraction_ainumbernoEstimated AI-generated fraction
fraction_ai_assistednumbernoEstimated AI-assisted fraction
fraction_humannumbernoEstimated human-written fraction
detailsanynoAdditional Pangram detail payload when present
disclosureanynoDisclosure / availability metadata when present

Python example

result = client.pangram_detection(123456)
print(result.get("header"), result.get("fraction_ai"), result.get("fraction_human"))

CLI example

substack-api pangram-detection --draft-id 123456

Verify auth / profile

get_profile_self()CLI: profileMCP: test_connection

Request

GEThttps://substack.com/api/v1/user/profile/self

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
Originstringyeshttps://substack.com/ (global profile endpoint)
Refererstringyeshttps://substack.com/ (global profile endpoint)
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)

Path params

None

Query params

None

Body

No request body

Response

object

Fields

NameTypeRequiredDescription
idnumberyesSubstack user id (used in draft_bylines)
...anynoAdditional profile fields from Substack

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"])

CLI example

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

Get draft

get_draft()CLI: get-draftMCP: get_draft

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)

Path params

NameTypeRequiredDescription
idnumber | stringyesDraft id in the URL path

Query params

None

Body

No request body

Response

object

Fields

NameTypeRequiredDescription
idnumber | stringyesDraft id
draft_titlestringnoTitle
draft_subtitlestringnoSubtitle
draft_bodystringnoProseMirror JSON string
draft_updated_atstringnoOptimistic-concurrency stamp for PUT

Python example

draft = client.get_draft(123456)
print(draft["draft_title"], draft["draft_updated_at"])

CLI example

substack-api get-draft --draft-id 123456

Update draft

update_draft()CLI: update-draftMCP: update_draft

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)
Content-Typestringyesapplication/json

Path params

NameTypeRequiredDescription
idnumber | stringyesDraft id in the URL path

Query params

None

Body

NameTypeRequiredDescription
last_updated_atstringyesFrom latest get_draft/create_draft (client fetches it if omitted in SDK)
draft_bylinesarrayyes[{ "id": <user_id>, "is_guest": false }]
detect_languagebooleanyesUsually false on update
translationsarrayyesUsually []
draft_titlestringnoSDK arg: title
draft_subtitlestringnoSDK arg: subtitle
draft_bodystringnoProseMirror JSON. SDK arg: body (plain text or object)
audiencestringnoAudience string
draft_section_idnumber | nullnoSDK arg: section_id
section_chosenbooleannoSet true when draft_section_id is set
should_send_emailbooleannoSDK arg: should_send_email
write_comment_permissionsstringnoe.g. "everyone"
cover_imagestring | nullnoCover image URL only - not a file upload

Response

object

Fields

NameTypeRequiredDescription
idnumber | stringyesDraft id
draft_updated_atstringyesNew concurrency stamp
draft_titlestringnoUpdated title

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"])

CLI example

substack-api update-draft \
  --draft-id 123456 \
  --title "Updated title" \
  --body "New body"

Publish now

publish_now()CLI: publishMCP: publish_post

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)
Content-Typestringyesapplication/json

Path params

NameTypeRequiredDescription
idnumber | stringyesDraft id in the URL path

Query params

None

Body

NameTypeRequiredDescription
sendbooleanyesEmail subscribers (SDK arg: send_email, default true)
share_automaticallybooleanyesDefault false
audiencestringyesDefault "everyone"

Response

object

Fields

NameTypeRequiredDescription
idnumber | stringyesPost id
slugstringnoPublic slug
is_publishedbooleannoPublish flag

Python example

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

CLI example

substack-api publish --draft-id 123456

Schedule

schedule_release()CLI: scheduleMCP: schedule_post

Request

GETPOST{publication}/api/v1/drafts/{id}/prepublish → {publication}/api/v1/drafts/{id}/scheduled_release

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)
Content-Typestringyesapplication/json

Path params

NameTypeRequiredDescription
idnumber | stringyesDraft id in the URL path

Query params

NameTypeRequiredDescription
publish_datestringyesISO-8601 UTC - query param on GET .../prepublish (same value as trigger_at)

Body

NameTypeRequiredDescription
trigger_atstringyesISO-8601 UTC schedule time (POST scheduled_release body)
post_audiencestringyesDefault "everyone"
email_audiencestringyesDefault "everyone"

Response

any

Fields

NameTypeRequiredDescription
(body)object | emptynoSubstack often returns empty or a small object. Prepublish may include errors[]

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)

CLI example

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

List tags

list_post_tags()CLI: list-tagsMCP: list_tags

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)

Path params

None

Query params

None

Body

No request body

Response

array<object>

Fields

NameTypeRequiredDescription
idstringyesTag id
namestringyesTag display name
slugstringnoTag slug

Python example

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

CLI example

substack-api list-tags

Create tag

create_post_tag()CLI: create-tagMCP: create_tag

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)
Content-Typestringyesapplication/json

Path params

None

Query params

None

Body

NameTypeRequiredDescription
namestringyesNew tag name

Response

object

Fields

NameTypeRequiredDescription
idstringyesCreated tag id
namestringyesTag name
slugstringnoTag slug

Python example

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

CLI example

substack-api create-tag --name "product of the day"

Attach tags

set_post_tags()CLI: set-tagsMCP: set_tags

Request

GETPOST{publication}/api/v1/publication/post-tag → {publication}/api/v1/post/{id}/tag/{tag_id}

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)
Content-Typestringyesapplication/json

Path params

NameTypeRequiredDescription
idnumber | stringyesPost/draft id in attach URL
tag_idstringyesTag id in attach URL

Query params

None

Body

NameTypeRequiredDescription
(create tag)objectnoWhen creating missing tags: { "name": string }
(attach tag)objectyesEmpty JSON object {} on POST /post/{id}/tag/{tag_id}

Response

array<object> (SDK normalized)

Fields

NameTypeRequiredDescription
tagobjectyesResolved/created tag
status"attached" | "already_attached"yesAttach result
linkobjectnoRaw attach response when newly attached

Python example

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

CLI example

substack-api set-tags \
  --post-id 123456 \
  --tags "api-test,newsletter"

Get post tags

get_post_tags()CLI: get-post-tagsMCP: get_post_tags

Request

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

Rate limit: 1 request per second

Headers

NameTypeRequiredDescription
Cookiestringyessubstack.sid=<SUBSTACK_SID> (browser session cookie)
Acceptstringyesapplication/json
OriginstringyesPublication origin, e.g. https://yourname.substack.com
RefererstringyesUsually {publication}/publish or the draft editor URL
User-AgentstringyesBrowser User-Agent string (client sends a Chrome UA by default)

Path params

NameTypeRequiredDescription
idnumber | stringyesPost/draft id in the URL path

Query params

None

Body

No request body

Response

array<object>

Fields

NameTypeRequiredDescription
idstringnoTag or link id
post_tag_idstringnoAttached tag id (used by set_post_tags dedupe)
namestringnoTag name when present

Python example

tags = client.get_post_tags(123456)
print(tags)

CLI example

substack-api get-post-tags --post-id 123456

Products

Python client, CLI, and MCP

Three separate products for the same posting job. The Python client for scripts and apps. The CLI for the terminal. The MCP server for AI tools such as Cursor or Claude. Pick one product - they are not the same package.

• Python client: Standalone Python SDK for Substack authors who post from scripts and apps.
• CLI: Standalone command-line tool for draft, publish, and schedule from the terminal.
• MCP server: Standalone MCP product for posting from AI tools such as Cursor or Claude.
• Plain-text → ProseMirror draft_body conversion
• Universal publication_url (*.substack.com or custom domain)
• cover_image URL on update_draft (URL string only - no binary image upload in SDK)

Pricing

One simple plan for Substack authors. Try free for 7 days, then $9 per month. Unlimited API calls across MCP, CLI, Claude Code, Claude Cowork, Cursor, and ChatGPT-compatible clients.

API Substack

7 days free

Unlimited API calls

Posting

Drafting

Deletion

Creating tags

MCP compatible

CLI compatible

Claude Code / Cowork

Cursor compatible

ChatGPT compatible

7 days free — then $9 per month

Start free — $9/mo after

Very frequently asked questions

Who is this for?

Writers and newsletter operators who publish on Substack and want to post with the Python client, the CLI, or the MCP server — each a separate product. If your job is to get posts out of your head and into your publication — this product is for you.

What job does this close?

Help Substack authors post to their publication programmatically — draft, publish, and schedule without the browser UI. Supporting actions (update draft, delete draft, tags) exist to make that posting workflow complete.

How do I post with it?

Authenticate with your browser session cookie (substack.sid), set your publication URL, then create, publish, or schedule with one of the separate products: Python client, CLI, MCP server. Each is its own product — pick the one that fits your workflow.

Move any blog to Substack in one click

For example, ahrefs.com, yep.com

Partners

API Substack

The posting API for Substack authors — draft, publish, and schedule posts from code.

© 2026

API Substack