New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@chirpie/sdk

Package Overview
Dependencies
Maintainers
1
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@chirpie/sdk

Chirpie SDK: TypeScript client for the Chirpie social media API

Source
npmnpm
Version
1.0.47
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

@chirpie/sdk

One typed client for posting to X, Bluesky, LinkedIn, Threads, Mastodon, Instagram, Facebook and Telegram. Chirpie handles the OAuth, the token refresh, the media upload and the per-platform quirks, so posting to eight networks is eight calls to the same method rather than eight SDKs.

Threads, scheduling, drafts, deletion and analytics are all in here too, and the same account you post from works identically through the CLI, the MCP server, and the REST API.

Install

npm install @chirpie/sdk

Quick start

import { ChirpieClient } from "@chirpie/sdk";

const chirpie = new ChirpieClient({ apiKey: process.env.CHIRPIE_API_KEY! });

const accounts = await chirpie.listAccounts();

const post = await chirpie.createPost({
  account_id: accounts[0].id,
  text: "Shipped a new release.",
});

console.log(post.status, post.platform_post_url);

Get an API key from the dashboard, or run npx chirpie login, which saves one to ~/.chirpie/config.json.

Configuration

const chirpie = new ChirpieClient({
  apiKey: "chirpie_sk_...",
  baseUrl: "https://chirpie.ai", // optional, this is the default
});

Or pick up credentials the CLI already saved:

import { ChirpieClient, requireConfig } from "@chirpie/sdk";

const config = requireConfig(); // CHIRPIE_API_KEY, else ~/.chirpie/config.json
const chirpie = new ChirpieClient({ apiKey: config.api_key, baseUrl: config.base_url });

CHIRPIE_API_KEY takes precedence over the config file. CHIRPIE_BASE_URL overrides the base URL.

Posts

// Publish now
await chirpie.createPost({ account_id, text: "Hello" });

// Publish later. An absolute ISO 8601 instant carrying a timezone; relative
// offsets are rejected.
await chirpie.createPost({ account_id, text: "Later", schedule_at: "2026-04-01T14:00:00Z" });

// Or a local time with no offset, read in `timezone` (an IANA name) or the
// timezone saved on your account. Daylight saving is worked out for the date
// you named, which an offset computed today gets wrong across a clock change.
await chirpie.createPost({
  account_id,
  text: "Half nine, my time",
  schedule_at: "2026-11-01T09:30:00",
  timezone: "America/New_York",
});

// Retries. createPost() and createThread() send an Idempotency-Key generated
// per call; pass your own so it survives a process restart, or null for none.
await chirpie.createPost(
  { account_id, text: "Hello" },
  { idempotencyKey: "campaign-2026-11-01" }
);

// With media. Chirpie downloads the URLs and uploads them to the platform.
await chirpie.createPost({ account_id, text: "Look", media_urls: ["https://example.com/a.png"] });

// Or upload a file first, and describe it for people using a screen reader.
const media = await chirpie.uploadMedia({ data: bytes, filename: "shot.png" });
await chirpie.createPost({
  account_id,
  text: "Look",
  media: [{ id: media.id, alt: "The new dashboard" }],
});

// Read back, newest first. Page with limit/offset; a short page is the last one.
const posts = await chirpie.listPosts({ status: "published", limit: 20, offset: 0 });
const one = await chirpie.getPost(posts[0].id);

// Edit a post that has not gone out yet. A published post is refused with
// `409 post_not_editable`, so take one that is still queued. Leaving
// `schedule_at` out keeps the time it already has, so this never publishes it.
const [queued] = await chirpie.listPosts({ status: "scheduled", limit: 1 });
await chirpie.updatePost(queued.id, { text: "Now with the typo fixed" });
await chirpie.updatePost(queued.id, { schedule_at: "2027-04-02T09:00:00Z" });
// Replace the first comment, or pass "" to remove it. This is the only way to
// change one, so do it before the post goes out.
await chirpie.updatePost(queued.id, { first_comment: "Full write-up: https://example.com" });

// Delete. The post is taken down from the platform, and reported deleted only
// once the platform confirms it is gone. Chirpie keeps the post, marked deleted,
// so it stays in your history. Instagram and TikTok publish no delete API and
// throw `501 delete_unsupported`. Deleting any post of a scheduled
// thread cancels the whole thread, and `cancelled_ids` lists every post that
// went with it.
const { cancelled_ids } = await chirpie.deletePost(one.id);

// Hide. Nothing reaches the platform: the post stays exactly as it is and
// `unhidePost` puts it back. A thread or a multi-account send moves whole.
await chirpie.hidePost(one.id);
await chirpie.unhidePost(one.id);

// Hidden posts are left out of every listing unless you ask for them.
await chirpie.listPosts({ include_hidden: true });

A thread is atomic: if any part fails, every part that had already published is deleted from the platform and the whole thread's quota is refunded. Read what happened with ChirpieApiError.threadRollback(). upstream_error means nothing is left on the platform and a retry is safe; thread_rollback_incomplete means the posts in still_live are really still up, so a retry would publish them twice.

The request field is schedule_at; the response field is scheduled_at. Sending scheduled_at back is rejected rather than published immediately.

First comment

first_comment publishes one comment under the post the moment it goes out, the "link in the first comment" pattern. X, Threads, Instagram and Facebook only: anywhere else the call throws 400 first_comment_unsupported rather than dropping it. It counts as one post against your monthly quota.

const post = await chirpie.createPost({
  account_id,
  text: "We rebuilt scheduling this week.",
  first_comment: "Full write-up: https://example.com/blog/scheduling",
});

post.first_comment;
// { text, status: "pending" | "posted" | "failed", comment_id, error }

// A failed first comment never fails its post. This re-sends the text the post
// already holds. That text cannot be changed once the post is out, so set it
// while the post is still a draft or still queued, with `updatePost`.
if (post.first_comment?.status === "failed") {
  await chirpie.retryFirstComment(post.id);
}

createThread takes first_comment too: one comment for the whole thread, published under the last part and reported on that part.

On a fan-out the shared first_comment reaches every account unless its account_configurations entry says otherwise. An entry naming a first_comment replaces it for that account, and first_comment: "" publishes that account with none, which is how one call sends a first comment to the accounts that take one while an account whose platform has none still publishes the post.

Stories, reels and other publishing options

configuration says how a platform that publishes more than one way should publish this post. Instagram takes a feed post, a story or a reel, and a Facebook Page takes a feed post or a story. Leave it out and everything goes to the feed. Instagram and Facebook are coming soon.

// A reel, with a cover frame and the co-author invited.
await chirpie.createPost({
  account_id: instagram_account,
  text: "Three minutes on how we schedule posts.",
  media: [{ id: video_id }],
  configuration: {
    instagram: {
      placement: "reel",
      video_cover_timestamp_ms: 1500,
      collaborators: ["a_co_author"],
      share_to_feed: true,
    },
  },
});

// A story. Exactly one image or video, and no caption, so the text is empty.
await chirpie.createPost({
  account_id: instagram_account,
  text: "",
  media: [{ id: image_id }],
  configuration: { instagram: { placement: "story" } },
});

// A Facebook Page feed post with a link preview.
await chirpie.createPost({
  account_id: facebook_page,
  text: "The write-up is out.",
  configuration: { facebook: { link: "https://example.com/blog/scheduling" } },
});

Nothing is dropped quietly. A block keyed on a platform that takes no options, or a field the placement you chose does not carry, throws 400 configuration_unsupported naming the platform and the field.

Each placement carries its own options:

  • instagram feed: up to 10 images, collaborators (at most 3 usernames) and user_tags, each of which needs both x and y.
  • instagram story: exactly one image or video, no caption (send empty text), no first comment and no collaborators. user_tags may carry coordinates or leave them out.
  • instagram reel: exactly one video and no images, plus collaborators, user_tags (the username on its own, since Instagram reads coordinates only on images and stories), cover or video_cover_timestamp_ms but never both, share_to_feed and trial_reel.
  • facebook feed: a link, shown as a preview.
  • facebook story: exactly one image or video, no text, no first comment and no link.

A story and a reel are each a single post, so createThread refuses either placement. On a fan-out one block serves every account of that platform, and an account_configurations entry naming its own configuration replaces it for that account. On updatePost an absent configuration keeps the options the post already has, and configuration: {} puts it back to a plain feed post.

Several accounts in one call

Name account_ids instead of account_id to publish the same post to up to 25 accounts at once.

const { group_id, results } = await chirpie.createPost({
  account_ids: [x_account, bsky_account, linkedin_account],
  text: "Shared text",
  account_configurations: {
    // This one account publishes its own text and no media.
    [bsky_account]: { text: "Shorter, for Bluesky", media: [] },
  },
});

for (const result of results) {
  if (!result.success) console.error(result.account_id, result.error?.message);
}

// Every post of the group, read back together.
const posts = await chirpie.listPosts({ group_id });

results carries one entry per account, in the order they were named. The call resolves even when some accounts failed, so check success on each: the accounts that worked stay published. Anything that fails the request as a whole (a character limit, a media rule, a spent quota) throws as usual and nothing is published. Each account may be named once.

An override says only what differs: an absent field inherits the request's own, and naming any media field replaces the shared media for that account outright. Every post carries the group_id it belongs to, or null when it went to a single account.

Threads work the same way, with posts replacing the whole thread for one account:

await chirpie.createThread({
  account_ids: [x_account, bsky_account],
  posts: [{ text: "One" }, { text: "Two" }],
  account_configurations: {
    [bsky_account]: { posts: [{ text: "A" }, { text: "B" }, { text: "C" }] },
  },
});

Threads

const thread = await chirpie.createThread({
  account_id,
  posts: [{ text: "One" }, { text: "Two" }, { text: "Three" }],
  schedule_at: "2026-04-01T14:00:00Z", // optional
  timezone: "America/New_York",        // optional, for a schedule_at with no offset
});

2 to 25 posts. X, Bluesky, Threads, Mastodon and Telegram publish them as a native reply chain; LinkedIn, Instagram and Facebook publish each item as a standalone post. A thread counts as N posts against your quota.

Drafts

draft: true saves a post or a thread without sending it. Nothing reaches the platform, nothing is queued, and nothing counts against your quota until you promote it.

const draft = await chirpie.createPost({
  account_id,
  text: "Half an idea. Finish it later.",
  draft: true,
});

// Always present, and empty when nothing would go wrong.
for (const w of draft.warnings) console.log(w.platform, w.code, w.message);

// A draft thread may be a single part while you are still writing it.
await chirpie.createThread({ account_id, posts: [{ text: "Opening line" }], draft: true });

// Ask for them by name: a listing with no status leaves drafts out.
const drafts = await chirpie.listPosts({ status: "draft" });

// Change the time it remembers, still a draft
await chirpie.updatePost(draft.id, { schedule_at: "2027-04-02T09:00:00Z", draft: true });

// Promote it: queue it, or send it now. Never both.
await chirpie.updatePost(draft.id, { schedule_at: "2027-04-02T09:00:00Z" });
await chirpie.updatePost(draft.id, { publish: true });

A draft is held to far less than a post: the text may be empty, the media may be missing, and a schedule_at on it is only the time you have in mind. Promotion runs every rule a create runs and spends the quota the draft never spent, so anything refused leaves the draft exactly as it was. The answer is a new post carrying promoted_from_draft_id, or promoted_from_draft_ids for a draft thread, which is promoted whole.

Accounts

// Accounts plus your plan's limits in one call
const { accounts, accounts_limit, accounts_active } = await chirpie.listAccountsWithLimits();

// Connect. OAuth platforms return a URL for the user to open.
const { authorization_url } = await chirpie.connectXAccount();
await chirpie.connectBlueskyAccount({ platform: "bluesky", identifier: "you.bsky.social", app_password: "xxxx-xxxx-xxxx-xxxx" });
await chirpie.connectTelegramAccount({ platform: "telegram", bot_token: "...", chat_id: "@yourchannel" });

// Choose which accounts publish. Deactivating frees a plan slot and keeps the
// account connected, but CANCELS its scheduled posts: `scheduled_posts` says how
// many would go, `canceled_posts` on the result says how many did.
const off = await chirpie.deactivateAccount(accounts[0].id);
await chirpie.activateAccount(accounts[1].id);

// Done with an account? Disconnecting frees a plan slot and cancels its scheduled
// posts like deactivating, but also removes the stored credential, so connecting it
// again means authorizing it on the platform again. Published posts are kept.
const gone = await chirpie.disconnectAccount(accounts[2].id);
console.log(gone.disconnected, gone.canceled_posts);

Also available: connectLinkedInAccount (pass "pages", or call connectLinkedInPagesAccount, for the LinkedIn Pages you administer: coming soon), connectMastodonAccount, and connectThreadsAccount, connectInstagramAccount and connectFacebookAccount (coming soon). Threads, Instagram, Facebook, Pinterest, TikTok, YouTube and Google Business Profile are coming soon; accounts already connected keep posting as normal.

Your own X developer app

Connect X accounts through your own X app so posts bill your X API credits, and X link posts are not surcharged. Full walkthrough: chirpie.ai/docs/x-byo-keys.

await chirpie.setXKeys({ client_id, client_secret, label: "Acme social app" });
await chirpie.getXKeysStatus(); // never returns the secret
await chirpie.removeXKeys();

Analytics and keys

const metrics = await chirpie.getPostAnalytics(post.id); // cached for 1 hour

// Ask the platform now instead. Floored at one forced refresh per post every
// 30 minutes; past that it throws 429 analytics_refresh_rate_limited, and the
// stored numbers are still one ordinary call away.
await chirpie.getPostAnalytics(post.id, { refresh: true });

const { key, expires_at } = await chirpie.createKey("My Bot"); // shown once, expires in 90 days

// A narrower key. Omitting `scopes` copies the calling key's own scopes, so a
// bare name gives full access only when the caller has it. Vocabulary: posts:read, posts:write, accounts:read,
// accounts:write, analytics:read, comments:read, comments:write, media:write,
// keys:write. A key can never grant a scope it does not itself hold, and a
// call outside a key's scopes is 403 insufficient_scope naming the missing
// one. All three key methods need keys:write; there is no keys:read.
await chirpie.createKey({ name: "Publishing bot", scopes: ["posts:write", "media:write"] });

await chirpie.listKeys(); // each key carries `scopes`; null means full access
await chirpie.revokeKey(id);

uploadMedia(input, options), retryFirstComment(id, options) and replyToComment(postId, commentId, text, options) take the same { idempotencyKey } options object the create methods do, but generate nothing.

Errors

import { ChirpieApiError, ChirpieError } from "@chirpie/sdk";

try {
  await chirpie.createPost({ account_id, text });
} catch (err) {
  if (err instanceof ChirpieApiError) {
    err.code;    // "usage_limit_exceeded", "rate_limited", "not_found", ...
    err.status;  // 400, 401, 402, 404, 429, 502
    err.message; // the API's own message, safe to show a user
  } else if (err instanceof ChirpieError) {
    err.message; // network or configuration problem
  }
}

The full code list is at chirpie.ai/docs/errors.

Types

Every input and response type is exported, including ApiPost, ApiThread, ApiAccount, AccountList, ApiAnalytics, ApiKeyInfo, CreatePostInput, CreateThreadInput, UpdatePostInput, ApiFirstComment, ListPostsOptions, and the draft types DraftWarning, DraftPostResponse, DraftThreadResponse, FanOutDraftPostResponse and FanOutDraftThreadResponse.

MIT

Keywords

chirpie

FAQs

Package last updated on 21 Sep 2026

Related posts