Use SubstackClient directly for programmatic access to Substack.
import{SubstackClient}from'@postcli/substack/client';constclient=newSubstackClient({token: string,// Base64-encoded auth tokenpublicationUrl: string,// e.g. "https://myname.substack.com"maxRequestsPerSecond?: number,// Rate limiting (optional)});Test if authentication is valid by fetching a single post from your publication.
testConnectivity(): Promise<boolean>Returns:true if the token works, false otherwise.
constok=awaitclient.testConnectivity();Get your own Substack profile. Extracts author byline from your publication's first post.
ownProfile(): Promise<Profile>Returns:Profile with fields: id, name, handle, bio, photoUrl, publications[].
constme=awaitclient.ownProfile();console.log(me.name,me.handle);console.log(me.publications.map(p=>p.subdomain));Get a profile by publication subdomain.
profileForSubdomain(subdomain: string): Promise<Profile>constprofile=awaitclient.profileForSubdomain('nicolascole77');console.log(profile.name,profile.bio);List posts from a publication's archive.
listPosts(options?: {subdomain?: string,// Defaults to own publicationlimit?: number,// Default: 10offset?: number,// Default: 0}): Promise<PreviewPost[]>Returns: Array of PreviewPost with fields: id, title, subtitle, slug, publishedAt, canonicalUrl, coverImage, wordcount, reactionCount, commentCount, restacks, authors[], truncatedBody, publicationSubdomain.
// Own postsconstmyPosts=awaitclient.listPosts({limit: 20});// Another publicationconstposts=awaitclient.listPosts({subdomain: 'platformer',limit: 5});Get a full post by slug.
getPost(slug: string,subdomain?: string): Promise<FullPost>Returns:FullPost with all PreviewPost fields plus: htmlBody, description, postTags[], youtubeUrls[].
constpost=awaitclient.getPost('my-first-post');console.log(post.title,post.htmlBody);Get a post by numeric ID. Searches the archive to find the slug, then fetches the full post.
getPostById(id: number,subdomain?: string): Promise<FullPost>If no subdomain is given, searches all your publications. Paginates through the archive in pages of 50 with up to 20 attempts per subdomain.
constpost=awaitclient.getPostById(12345);List notes from the reader feed. Filters feed items to only those with typeBucket=notes.
listNotes(options?: {limit?: number,// Default: 10}): Promise<Note[]>Returns: Array of Note with fields: id, body, author ({ name, handle }), publishedAt, reactions, childrenCount.
Automatically paginates through the feed until limit notes are collected or no more pages are available.
constnotes=awaitclient.listNotes({limit: 25});List comments on a post.
listComments(postId: number,options?: {subdomain?: string,// Defaults to ownlimit?: number,// Default: 50}): Promise<Comment[]>Returns: Array of Comment with fields: id, body, authorName, authorId, date, reactions, childrenCount.
constcomments=awaitclient.listComments(12345,{limit: 10});Get the authenticated user's reader feed.
getFeed(options?: {tab?: string,// "for-you", "subscribed", or category slugcursor?: string,// Pagination cursor}): Promise<{items: SubstackFeedItem[],nextCursor?: string}>Returns: Feed items and an optional cursor for the next page.
constfeed=awaitclient.getFeed({tab: 'subscribed'});for(constitemoffeed.items){console.log(item.type,item.entity_key);}// Paginateconstpage2=awaitclient.getFeed({tab: 'subscribed',cursor: feed.nextCursor});Get a user's profile feed (their comments and notes).
getProfileFeed(userId: number,options?: {cursor?: string,}): Promise<{items: SubstackFeedItem[],nextCursor?: string}>constprofile=awaitclient.ownProfile();constmyFeed=awaitclient.getProfileFeed(profile.id);Get a single comment with its parent comments (ancestors in the thread).
getComment(commentId: number): Promise<{comment: any,parentComments: any[],}>constdetail=awaitclient.getComment(54321);console.log(detail.parentComments.length,'parents');console.log(detail.comment.body);Get replies to a comment (children threads).
getCommentReplies(commentId: number): Promise<{rootComment: any,branches: {comment: any,descendants: any[]}[],}>Each branch contains a direct reply and its nested descendants.
constreplies=awaitclient.getCommentReplies(54321);for(constbranchofreplies.branches){console.log(branch.comment.body);console.log(' nested:',branch.descendants.length);}Publish a new note. Text is converted to ProseMirror format. Supports **bold** markdown.
publishNote(text: string,options?: {replyMinimumRole?: string,// Default: "everyone"}): Promise<{id: number}>constresult=awaitclient.publishNote('Just shipped **v2.0**!');console.log('Published note:',result.id);Reply to a note or comment.
replyToNote(parentId: number,text: string): Promise<{id: number}>constreply=awaitclient.replyToNote(98765,'Thanks for sharing!');console.log('Reply ID:',reply.id);Comment on a post. Substack converts the plain text body to ProseMirror server-side.
commentOnPost(postId: number,text: string,subdomain?: string): Promise<{id: number}>constcomment=awaitclient.commentOnPost(12345,'Great article!');console.log('Comment ID:',comment.id);React to a post (heart).
reactToPost(postId: number,subdomain?: string): Promise<void>awaitclient.reactToPost(12345);React to a comment or note (heart).
reactToComment(commentId: number): Promise<void>awaitclient.reactToComment(54321);Restack a post to your feed.
restackPost(postId: number): Promise<void>awaitclient.restackPost(12345);Restack a note/comment to your feed.
restackNote(commentId: number): Promise<void>awaitclient.restackNote(98765);Get the default subdomain for this client (extracted from the publication URL).
getSubdomain(): stringconstsub=client.getSubdomain();// "myname"Verify an auth token is valid by testing reader feed access.
staticverifyToken(token: string): Promise<boolean>constvalid=awaitSubstackClient.verifyToken(myToken);Auto-discover user profile from a token and optional substack.lli JWT. Used during login to find the user's handle and publications without prompting.
staticdiscoverProfile(token: string,lliToken?: string): Promise<{handle: string|null,name: string|null,id: number,publications: {subdomain: string,name: string}[],subscriptions: {subdomain: string,name: string}[],}|null>Extract the userId from a substack.lli JWT token.
staticextractUserId(lliToken?: string): number|null