REST API reference

Substack API

14 endpoints for Substack. Each one is a POST with a JSON body, called with your API key.

Base URL
https://cotlet.dev/api/v1/substack
Connects with
A Substack account that can publish to the publication
Integration page
Substack
Substack Endpoints
Endpoint Access Description
list_posts read Posts of your publication, newest first, with status and audience.
get_post read Full post: title, subtitle, body as Markdown, audience, URL and dates.
get_post_stats read Recipients, opens, open rate, views, likes, restacks and subscribers gained.
get_publication_stats read Total, paid, new and churned subscribers and revenue for a period.
list_subscribers read Subscribers with plan, start date and activity rating.
export_subscribers read The full subscriber list as a CSV download link, valid for 1 hour.
list_comments read Comments on a post, with author, text and whether you replied.
create_draft write New draft from Markdown. Nothing is sent until you publish or schedule it.
update_draft write Change the title, subtitle, body or audience of a draft.
schedule_post write Publish a draft at a set time, on the web and by email.
publish_post write Publish a draft now.
publish_note write Short post in Substack Notes, now or at a set time, optionally with a link card to one of your posts.
reply_to_comment write Reply as you under a comment on your post.
add_subscriber write Add an email as a free subscriber or give a complimentary paid plan.

List posts

read

POST https://cotlet.dev/api/v1/substack/list_posts

Lists posts of your publication, newest first. Filter by status to see drafts or scheduled posts. Scheduled posts include their publish time. Pages hold up to 50 posts; pass next_cursor back as cursor for the next page.

Parameters

Parameters: list_posts
NameTypeDescription
status enum published, draft or scheduled. Default: published.
limit integer 1 to 50. Default: 20.
cursor string Next page cursor from the previous call.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/list_posts \
  -H "Authorization: Bearer $COTLET_API_KEY"
Python
result = client.call("substack", "list_posts")
Node.js
const result = await cotlet.call('substack', 'list_posts');
CLI
cotlet call substack list_posts

Example response

{
  "posts": [
    {
      "post_id": "148213977",
      "title": "The quiet launch",
      "status": "published",
      "audience": "everyone",
      "published_at": "2026-10-06T12:00:00Z",
      "url": "https://yourname.substack.com/p/the-quiet-launch"
    },
    {
      "post_id": "148420031",
      "title": "Notes on pricing",
      "status": "draft",
      "audience": "only_paid",
      "published_at": null,
      "url": null
    }
  ],
  "next_cursor": "eyJvZmZzZXQiOjIwfQ"
}

Get post

read

POST https://cotlet.dev/api/v1/substack/get_post

Returns one post with its full body as Markdown, including the paywalled part of your own posts. Use it to edit a published post or to quote it in a Note. Images come back as URLs.

Parameters

Parameters: get_post
NameTypeDescription
post_id required string Post ID from list_posts.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/get_post \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"post_id":"post_8c41d2"}'
Python
result = client.call("substack", "get_post", {"post_id":"post_8c41d2"})
Node.js
const result = await cotlet.call('substack', 'get_post', {"post_id":"post_8c41d2"});
CLI
cotlet call substack get_post --data '{"post_id":"post_8c41d2"}'

Example response

{
  "post_id": "148213977",
  "title": "The quiet launch",
  "subtitle": "What waiting taught me",
  "body_markdown": "Last month I shipped without telling anyone...",
  "audience": "everyone",
  "status": "published",
  "published_at": "2026-10-06T12:00:00Z",
  "url": "https://yourname.substack.com/p/the-quiet-launch"
}

POST https://cotlet.dev/api/v1/substack/get_post_stats

Returns the engagement numbers of one published post, the same as its stats page in the dashboard. Email numbers cover the send; views include the web. Numbers keep growing for days after a send, so check again later.

Parameters

Parameters: get_post_stats
NameTypeDescription
post_id required string Post ID from list_posts.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/get_post_stats \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"post_id":"post_8c41d2"}'
Python
result = client.call("substack", "get_post_stats", {"post_id":"post_8c41d2"})
Node.js
const result = await cotlet.call('substack', 'get_post_stats', {"post_id":"post_8c41d2"});
CLI
cotlet call substack get_post_stats --data '{"post_id":"post_8c41d2"}'

Example response

{
  "post_id": "148213977",
  "recipients": 4120,
  "opens": 2011,
  "open_rate": 0.488,
  "views": 3560,
  "likes": 142,
  "restacks": 18,
  "subscribers_gained": 37
}

POST https://cotlet.dev/api/v1/substack/get_publication_stats

Returns subscriber and revenue numbers for a period, the same figures as the publisher dashboard: total, paid and new subscribers, churned subscribers and revenue. Periods are 7, 30 or 90 days.

Parameters

Parameters: get_publication_stats
NameTypeDescription
period enum 7d, 30d or 90d. Default: 30d.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/get_publication_stats \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"period":"7d"}'
Python
result = client.call("substack", "get_publication_stats", {"period":"7d"})
Node.js
const result = await cotlet.call('substack', 'get_publication_stats', {"period":"7d"});
CLI
cotlet call substack get_publication_stats --data '{"period":"7d"}'

Example response

{
  "period": "7d",
  "subscribers_total": 4380,
  "subscribers_paid": 412,
  "subscribers_new": 96,
  "subscribers_churned": 11,
  "revenue_usd": 3150
}

POST https://cotlet.dev/api/v1/substack/list_subscribers

Lists subscribers, newest first. Filter by plan or by the date they joined. Activity rating is the 0 to 5 score Substack shows in the subscriber dashboard. Pages hold up to 500 subscribers; pass next_cursor back as cursor.

Parameters

Parameters: list_subscribers
NameTypeDescription
plan enum all, free, paid, comp or founding. Default: all.
since date Only subscribers who joined on or after this date.
limit integer 1 to 500. Default: 100.
cursor string Next page cursor from the previous call.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/list_subscribers \
  -H "Authorization: Bearer $COTLET_API_KEY"
Python
result = client.call("substack", "list_subscribers")
Node.js
const result = await cotlet.call('substack', 'list_subscribers');
CLI
cotlet call substack list_subscribers

Example response

{
  "subscribers": [
    {
      "email": "maya@example.com",
      "plan": "free",
      "joined_at": "2026-10-04T09:12:00Z",
      "activity_rating": 4
    },
    {
      "email": "jon@example.com",
      "plan": "paid",
      "joined_at": "2026-10-02T17:40:00Z",
      "activity_rating": 5
    }
  ],
  "next_cursor": null
}

POST https://cotlet.dev/api/v1/substack/export_subscribers

Builds the same CSV as Export in the subscriber dashboard and returns a link to it. Use it for the full list; list_subscribers is better for filtered pages. The link stops working after 1 hour.

Parameters

No parameters.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/export_subscribers \
  -H "Authorization: Bearer $COTLET_API_KEY"
Python
result = client.call("substack", "export_subscribers")
Node.js
const result = await cotlet.call('substack', 'export_subscribers');
CLI
cotlet call substack export_subscribers

Example response

{
  "download_url": "https://files.cotlet.example/exports/subscribers-2026-10-11.csv",
  "rows": 4380,
  "expires_at": "2026-10-11T13:00:00Z"
}

POST https://cotlet.dev/api/v1/substack/list_comments

Returns the comments of one post, oldest first, with replies nested under their parent. Set unanswered_only to skip threads you already replied to. Pass comment_id from an item to reply_to_comment.

Parameters

Parameters: list_comments
NameTypeDescription
post_id required string Post ID from list_posts.
unanswered_only boolean Skip threads you already replied to.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/list_comments \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"post_id":"post_8c41d2"}'
Python
result = client.call("substack", "list_comments", {"post_id":"post_8c41d2"})
Node.js
const result = await cotlet.call('substack', 'list_comments', {"post_id":"post_8c41d2"});
CLI
cotlet call substack list_comments --data '{"post_id":"post_8c41d2"}'

Example response

{
  "comments": [
    {
      "comment_id": "c_90417",
      "author": "Maya Chen",
      "body": "Does the next part cover pricing?",
      "created_at": "2026-10-07T08:20:00Z",
      "replied": false,
      "replies": []
    }
  ]
}

Create draft

write

POST https://cotlet.dev/api/v1/substack/create_draft

Creates a draft in your publication and returns its post_id. The draft is visible only to your team. Sending is a separate step: schedule_post or publish_post. Creating a draft reaches no one and update_draft or the Substack editor changes it, so it runs without asking.

Parameters

Parameters: create_draft
NameTypeDescription
title required string Post title.
body_markdown required string Post body. Images by URL.
subtitle string Shown under the title and in the email preview.
audience enum everyone, only_paid or founding. Default: everyone.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/create_draft \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"title","body_markdown":"body markdown"}'
Python
result = client.call("substack", "create_draft", {"title":"title","body_markdown":"body markdown"})
Node.js
const result = await cotlet.call('substack', 'create_draft', {"title":"title","body_markdown":"body markdown"});
CLI
cotlet call substack create_draft --data '{"title":"title","body_markdown":"body markdown"}'

Example response

{
  "post_id": "148420031",
  "status": "draft",
  "audience": "everyone",
  "edit_url": "https://yourname.substack.com/publish/post/148420031"
}

Update draft

write

POST https://cotlet.dev/api/v1/substack/update_draft

Changes fields of a draft and leaves the others as they are. It works on drafts only; a published post is not changed. Nothing reaches your subscribers, so it runs without asking.

Parameters

Parameters: update_draft
NameTypeDescription
post_id required string Post ID from list_posts.
fields required object Any of title, subtitle, body_markdown, audience.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/update_draft \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"post_id":"post_8c41d2","fields":"fields"}'
Python
result = client.call("substack", "update_draft", {"post_id":"post_8c41d2","fields":"fields"})
Node.js
const result = await cotlet.call('substack', 'update_draft', {"post_id":"post_8c41d2","fields":"fields"});
CLI
cotlet call substack update_draft --data '{"post_id":"post_8c41d2","fields":"fields"}'

Example response

{
  "post_id": "148420031",
  "status": "draft",
  "updated": [
    "title",
    "body_markdown"
  ]
}

Schedule post

write Confirm

POST https://cotlet.dev/api/v1/substack/schedule_post

Schedules a draft to go live at the time you set, on the web and, unless send_email is false, by email to your subscribers. The time must be in the future. A scheduled post stays editable until it goes out. By default you confirm each call; you can switch that off for this tool.

Parameters

Parameters: schedule_post
NameTypeDescription
post_id required string Post ID from list_posts.
publish_at required datetime ISO 8601 with time zone.
send_email boolean Email it to subscribers. Default: true.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/schedule_post \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"post_id":"148420031","publish_at":"2026-10-13T08:00:00-04:00","send_email":true}'
Python
result = client.call("substack", "schedule_post", {"post_id":"148420031","publish_at":"2026-10-13T08:00:00-04:00","send_email":True})
Node.js
const result = await cotlet.call('substack', 'schedule_post', {"post_id":"148420031","publish_at":"2026-10-13T08:00:00-04:00","send_email":true});
CLI
cotlet call substack schedule_post --data '{"post_id":"148420031","publish_at":"2026-10-13T08:00:00-04:00","send_email":true}'

Example response

{
  "post_id": "148420031",
  "status": "scheduled",
  "publish_at": "2026-10-13T08:00:00-04:00",
  "audience": "everyone",
  "url": "https://yourname.substack.com/p/notes-on-pricing"
}

Publish post

write Confirm

POST https://cotlet.dev/api/v1/substack/publish_post

Publishes a draft immediately, on the web and, unless send_email is false, by email to your subscribers. A sent email cannot be recalled. By default you confirm each call; you can switch that off for this tool.

Parameters

Parameters: publish_post
NameTypeDescription
post_id required string Post ID from list_posts.
send_email boolean Email it to subscribers. Default: true.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/publish_post \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"post_id":"post_8c41d2"}'
Python
result = client.call("substack", "publish_post", {"post_id":"post_8c41d2"})
Node.js
const result = await cotlet.call('substack', 'publish_post', {"post_id":"post_8c41d2"});
CLI
cotlet call substack publish_post --data '{"post_id":"post_8c41d2"}'

Example response

{
  "post_id": "148420031",
  "status": "published",
  "published_at": "2026-10-11T10:02:00Z",
  "emailed": true,
  "url": "https://yourname.substack.com/p/notes-on-pricing"
}

Publish Note

write Confirm

POST https://cotlet.dev/api/v1/substack/publish_note

Posts a Note under your name. Without publish_at it goes out now; with it, the Note waits in your scheduled Notes and goes out at that time. Text is up to 1,000 characters. By default you confirm each call; you can switch that off for this tool.

Parameters

Parameters: publish_note
NameTypeDescription
body required string Note text, up to 1,000 characters.
link_post_id string Attach one of your posts as a card.
publish_at datetime ISO 8601 with time zone. Omit to post now.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/publish_note \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"body"}'
Python
result = client.call("substack", "publish_note", {"body":"body"})
Node.js
const result = await cotlet.call('substack', 'publish_note', {"body":"body"});
CLI
cotlet call substack publish_note --data '{"body":"body"}'

Example response

{
  "note_id": "n_5521098",
  "status": "published",
  "published_at": "2026-10-11T10:05:00Z",
  "url": "https://substack.com/@yourname/note/c-5521098"
}

Reply to comment

write Confirm

POST https://cotlet.dev/api/v1/substack/reply_to_comment

Posts a reply under one comment, under your name, visible to everyone who can read the post. The author gets Substack's usual reply notification. By default you confirm each call; you can switch that off for this tool.

Parameters

Parameters: reply_to_comment
NameTypeDescription
comment_id required string Comment ID from list_comments.
body required string Reply text.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/reply_to_comment \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"comment_id":"c_90417","body":"Thank you, Maya. Pricing is the whole of part two, out on Thursday."}'
Python
result = client.call("substack", "reply_to_comment", {"comment_id":"c_90417","body":"Thank you, Maya. Pricing is the whole of part two, out on Thursday."})
Node.js
const result = await cotlet.call('substack', 'reply_to_comment', {"comment_id":"c_90417","body":"Thank you, Maya. Pricing is the whole of part two, out on Thursday."});
CLI
cotlet call substack reply_to_comment --data '{"comment_id":"c_90417","body":"Thank you, Maya. Pricing is the whole of part two, out on Thursday."}'

Example response

{
  "comment_id": "c_90432",
  "parent_id": "c_90417",
  "created_at": "2026-10-11T10:07:00Z"
}

Add subscriber

write Confirm

POST https://cotlet.dev/api/v1/substack/add_subscriber

Adds one email to your list. Substack sends the new subscriber its usual welcome email. With plan set to comp the person gets a complimentary paid plan until comp_until; for someone who already pays, the time is added after their current billing cycle. Add only people who agreed to hear from you. By default you confirm each call; you can switch that off for this tool.

Parameters

Parameters: add_subscriber
NameTypeDescription
email required string Subscriber email.
plan enum free or comp. Default: free.
comp_until date End of the complimentary plan.

Request

cURL
curl -X POST https://cotlet.dev/api/v1/substack/add_subscriber \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com"}'
Python
result = client.call("substack", "add_subscriber", {"email":"jane@example.com"})
Node.js
const result = await cotlet.call('substack', 'add_subscriber', {"email":"jane@example.com"});
CLI
cotlet call substack add_subscriber --data '{"email":"jane@example.com"}'

Example response

{
  "email": "maya@example.com",
  "plan": "free",
  "status": "added",
  "joined_at": "2026-10-11T10:09:00Z"
}