This is an unofficial TypeScript library providing an interface for Substack. I am in no way affiliated with Substack.
If you are looking for the Python version, see: https://github.com/ma2za/python-substack
npm install substack-tsSet the following environment variables in a .env file:
EMAIL=
PASSWORD=
PUBLICATION_URL= # Optional: your publication URL
COOKIES_PATH= # Optional: path to cookies JSON file
COOKIES_STRING= # Optional: cookie string for authenticationThe examples also accept these aliases:
SUBSTACK_EMAIL=
SUBSTACK_PASSWORD=
SUBSTACK_PUBLICATION_URL=Some Substack accounts only use magic-link login.
To set a password:
- Sign out of Substack
- At sign-in, click
Sign in with passwordunder theEmailtext box - Choose
Set a new password
Keep .env out of source control and never commit secrets.
import { Api } from "substack-ts";
async function main() {
const api = new Api({
email: process.env.EMAIL,
password: process.env.PASSWORD,
publication_url: process.env.PUBLICATION_URL
});
await api.ready;
console.log(await api.get_user_id());
}
main().catch(console.error);import { Api } from "substack-ts";
async function main() {
const api = new Api({
cookies_path: process.env.COOKIES_PATH,
// OR
// cookies_string: process.env.COOKIES_STRING,
publication_url: process.env.PUBLICATION_URL
});
await api.ready;
console.log(await api.get_user_id());
}
main().catch(console.error);import { Api, Post } from "substack-ts";
async function main() {
const api = new Api({
email: process.env.EMAIL,
password: process.env.PASSWORD,
publication_url: process.env.PUBLICATION_URL
});
await api.ready;
const userId = await api.get_user_id();
const post = new Post(
"How to publish a Substack post using the TypeScript API",
"This post was published using the TypeScript API",
userId,
"everyone",
"everyone"
);
post.add({ type: "paragraph", content: "This is how you add a new paragraph to your post!" });
post.add({
type: "paragraph",
content: [
{ content: "This is how you " },
{ content: "bolden ", marks: [{ type: "strong" }] },
{ content: "a word." }
]
});
const image = await api.get_image("image.png");
post.add({ type: "captionedImage", src: image.url || image.image_url });
const embedded = await api.publication_embed("https://jackio.substack.com/");
post.add({ type: "embeddedPublication", url: embedded });
const markdownContent = `# My Heading\n\nThis is a paragraph with **bold** and *italic* text.`;
await post.from_markdown(markdownContent, api);
const draft = await api.post_draft(post.get_draft());
await api.prepublish_draft(draft.id);
await api.publish_draft(draft.id);
}
main().catch(console.error);Use the provided example script:
# Default: draft.yaml in current folder
npm run example:yaml
# Choose file
npm run example:yaml -- -p examples/draft.yaml
# Optional cookies file override
npm run example:yaml -- -p examples/draft.yaml --cookies ./cookies.jsonThe script:
- Loads
.env - Parses YAML with nested body/tags support
- Authenticates via cookies or email/password
- Builds
Post(...)with audience/write-comment settings - Uploads
captionedImageitems viaapi.get_image(...) - Creates draft using
post.get_draft() - Applies slug/SEO metadata with
api.put_draft(...) - Adds tags with
api.add_tags_to_post(...)
Example YAML structure:
title: "My Post Title"
subtitle: "My Post Subtitle"
audience: "everyone" # everyone, only_paid, founding, only_free
write_comment_permissions: "everyone" # none, only_paid, everyone
section: "my-section"
tags:
- "typescript"
- "substack"
body:
0:
type: "heading"
level: 1
content: "Introduction"
1:
type: "paragraph"
content: "This is a paragraph."
2:
type: "captionedImage"
src: "local_image.jpg"Use the markdown example script:
# Default: README.md
npm run example:markdown
# Custom markdown file
npm run example:markdown -- -m my-post.md
# Publish instead of draft only
npm run example:markdown -- -m my-post.md --publish
# Optional cookies file override
npm run example:markdown -- -m my-post.md --cookies ./cookies.jsonThe markdown example:
- Loads
.env - Resolves file path from cwd, then project root fallback
- Extracts title from first
#heading - Parses markdown with
post.from_markdown(...) - Creates a draft using
post.get_draft() - Optionally publishes with
--publish
npm run example:subscriber-countApi- Main Substack API clientPost- Post builder compatible with python-substack flowparseInline/parse_inline- Markdown inline formatter parserSubstackAPIException- API-level exceptionsSubstackRequestException- Request exceptionsSectionNotExistsException- Missing section exception
Requires Node 18+.
npm run build
npm testTo get a cookie string:
- Sign in to Substack
- Open dev tools (
F12) and go to Network - Refresh and pick a request (for example a subscriptions request)
- Copy as fetch (Node.js)
- Extract the
cookieheader value - Put it in
.envasCOOKIES_STRING