
Company News
Socket Joins New OpenJS Program to Fund Node.js Security Work
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.
@chirpie/sdk
Advanced tools
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.
npm install @chirpie/sdk
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.
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.
// 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 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.
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.
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" }] },
},
});
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.
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 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.
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();
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.
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.
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
FAQs
Chirpie SDK: TypeScript client for the Chirpie social media API
The npm package @chirpie/sdk receives a total of 0 weekly downloads. As such, @chirpie/sdk popularity was classified as not popular.
We found that @chirpie/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.

Research
/Security News
A malicious Firefox extension fetches its payload after installation to evade detection, steal Google session cookies, and automate account takeover.