# Substack API

> Every Substack endpoint on Cotlet: inputs, JSON Schema, cURL, Python and Node.js examples and an example response for each call.

Source: https://cotlet.dev/docs/api/substack

Docs sections: [Get started](https://cotlet.dev/docs.md) · [REST API](https://cotlet.dev/docs/api.md) · [MCP](https://cotlet.dev/docs/mcp.md) · [SDKs & CLI](https://cotlet.dev/docs/sdks.md) · [Integrations](https://cotlet.dev/docs/integrations.md)

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: https://cotlet.dev/integrations/substack-api-mcp-server

## 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

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

Access: read.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `status` | enum | no | published, draft or scheduled. Default: published. |
| `limit` | integer | no | 1 to 50. Default: 20. |
| `cursor` | string | no | Next page cursor from the previous call. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/substack/list_posts \
  -H "Authorization: Bearer $COTLET_API_KEY"
```

**Python**

```python
result = client.call("substack", "list_posts")
```

**Node.js**

```js
const result = await cotlet.call('substack', 'list_posts');
```

**CLI**

```bash
cotlet call substack list_posts
```

### Example response

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

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

Access: read.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `post_id` | string | yes | Post ID from list_posts. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "get_post", {"post_id":"post_8c41d2"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'get_post', {"post_id":"post_8c41d2"});
```

**CLI**

```bash
cotlet call substack get_post --data '{"post_id":"post_8c41d2"}'
```

### Example response

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

## Get post stats

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

Access: read.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `post_id` | string | yes | Post ID from list_posts. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "get_post_stats", {"post_id":"post_8c41d2"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'get_post_stats', {"post_id":"post_8c41d2"});
```

**CLI**

```bash
cotlet call substack get_post_stats --data '{"post_id":"post_8c41d2"}'
```

### Example response

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

## Get publication stats

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

Access: read.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `period` | enum | no | 7d, 30d or 90d. Default: 30d. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "get_publication_stats", {"period":"7d"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'get_publication_stats', {"period":"7d"});
```

**CLI**

```bash
cotlet call substack get_publication_stats --data '{"period":"7d"}'
```

### Example response

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

## List subscribers

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

Access: read.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `plan` | enum | no | all, free, paid, comp or founding. Default: all. |
| `since` | date | no | Only subscribers who joined on or after this date. |
| `limit` | integer | no | 1 to 500. Default: 100. |
| `cursor` | string | no | Next page cursor from the previous call. |

### Request

**cURL**

```bash
curl -X POST https://cotlet.dev/api/v1/substack/list_subscribers \
  -H "Authorization: Bearer $COTLET_API_KEY"
```

**Python**

```python
result = client.call("substack", "list_subscribers")
```

**Node.js**

```js
const result = await cotlet.call('substack', 'list_subscribers');
```

**CLI**

```bash
cotlet call substack list_subscribers
```

### Example response

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

## Export subscribers

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

Access: read.

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**

```bash
curl -X POST https://cotlet.dev/api/v1/substack/export_subscribers \
  -H "Authorization: Bearer $COTLET_API_KEY"
```

**Python**

```python
result = client.call("substack", "export_subscribers")
```

**Node.js**

```js
const result = await cotlet.call('substack', 'export_subscribers');
```

**CLI**

```bash
cotlet call substack export_subscribers
```

### Example response

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

## List comments

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

Access: read.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `post_id` | string | yes | Post ID from list_posts. |
| `unanswered_only` | boolean | no | Skip threads you already replied to. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "list_comments", {"post_id":"post_8c41d2"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'list_comments', {"post_id":"post_8c41d2"});
```

**CLI**

```bash
cotlet call substack list_comments --data '{"post_id":"post_8c41d2"}'
```

### Example response

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

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

Access: write.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `title` | string | yes | Post title. |
| `body_markdown` | string | yes | Post body. Images by URL. |
| `subtitle` | string | no | Shown under the title and in the email preview. |
| `audience` | enum | no | everyone, only_paid or founding. Default: everyone. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "create_draft", {"title":"title","body_markdown":"body markdown"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'create_draft', {"title":"title","body_markdown":"body markdown"});
```

**CLI**

```bash
cotlet call substack create_draft --data '{"title":"title","body_markdown":"body markdown"}'
```

### Example response

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

## Update draft

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

Access: write.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `post_id` | string | yes | Post ID from list_posts. |
| `fields` | object | yes | Any of title, subtitle, body_markdown, audience. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "update_draft", {"post_id":"post_8c41d2","fields":"fields"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'update_draft', {"post_id":"post_8c41d2","fields":"fields"});
```

**CLI**

```bash
cotlet call substack update_draft --data '{"post_id":"post_8c41d2","fields":"fields"}'
```

### Example response

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

## Schedule post

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

Access: write. Confirm: waits for the user's OK before it runs.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `post_id` | string | yes | Post ID from list_posts. |
| `publish_at` | datetime | yes | ISO 8601 with time zone. |
| `send_email` | boolean | no | Email it to subscribers. Default: true. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "schedule_post", {"post_id":"148420031","publish_at":"2026-10-13T08:00:00-04:00","send_email":True})
```

**Node.js**

```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**

```bash
cotlet call substack schedule_post --data '{"post_id":"148420031","publish_at":"2026-10-13T08:00:00-04:00","send_email":true}'
```

### Example response

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

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

Access: write. Confirm: waits for the user's OK before it runs.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `post_id` | string | yes | Post ID from list_posts. |
| `send_email` | boolean | no | Email it to subscribers. Default: true. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "publish_post", {"post_id":"post_8c41d2"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'publish_post', {"post_id":"post_8c41d2"});
```

**CLI**

```bash
cotlet call substack publish_post --data '{"post_id":"post_8c41d2"}'
```

### Example response

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

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

Access: write. Confirm: waits for the user's OK before it runs.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `body` | string | yes | Note text, up to 1,000 characters. |
| `link_post_id` | string | no | Attach one of your posts as a card. |
| `publish_at` | datetime | no | ISO 8601 with time zone. Omit to post now. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "publish_note", {"body":"body"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'publish_note', {"body":"body"});
```

**CLI**

```bash
cotlet call substack publish_note --data '{"body":"body"}'
```

### Example response

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

## Reply to comment

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

Access: write. Confirm: waits for the user's OK before it runs.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `comment_id` | string | yes | Comment ID from list_comments. |
| `body` | string | yes | Reply text. |

### Request

**cURL**

```bash
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**

```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**

```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**

```bash
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

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

## Add subscriber

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

Access: write. Confirm: waits for the user's OK before it runs.

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

| Name | Type | Required | Description |
|---|---|---|---|
| `email` | string | yes | Subscriber email. |
| `plan` | enum | no | free or comp. Default: free. |
| `comp_until` | date | no | End of the complimentary plan. |

### Request

**cURL**

```bash
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**

```python
result = client.call("substack", "add_subscriber", {"email":"jane@example.com"})
```

**Node.js**

```js
const result = await cotlet.call('substack', 'add_subscriber', {"email":"jane@example.com"});
```

**CLI**

```bash
cotlet call substack add_subscriber --data '{"email":"jane@example.com"}'
```

### Example response

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