TypeScript SDK

A typed client for posts, schedules, channels, the inbox, Auto-DM and analytics.

npm install @orbitwrite/sdk

It runs anywhere with fetch (Node 20+, Bun, Deno, workers, browsers) and has no runtime dependencies.

Getting started

import { Orbitwrite } from "@orbitwrite/sdk";

const orbit = new Orbitwrite({ apiKey: process.env.ORBITWRITE_API_KEY });

const [channel] = await orbit.channels.postable();
if (!channel) throw new Error("Connect a channel first.");

const slot = await orbit.schedule.nextTime(channel.id);
if (!slot) throw new Error("No free posting slot in the next two weeks.");

await orbit.posts.create({
  channels: [channel.id],
  content: "Shipped something today.",
  scheduledAt: slot.at,
});

apiKey falls back to ORBITWRITE_API_KEY. The key decides the workspace, so there's nothing else to configure.

Client options

OptionDefault
apiKey$ORBITWRITE_API_KEYA workspace key (orbit_sk_…).
accessTokenAn OAuth access token, sent instead of a key.
baseUrlhttps://orbitwrite.com
organizationIdfrom the keySet it to skip one GET /me lookup.
timeoutMs300000 disables it.
maxRetries2Extra attempts after a retryable failure.
fetchglobalA proxy agent or a test stub.
headers, userAgentExtra headers, and a suffix to identify your integration.

Posts

A post sent to several channels is one group, and every mutation takes its groupId.

// The same words everywhere
await orbit.posts.create({ channels: ["acc_1", "acc_2"], content: "Hello.", scheduledAt: "2026-10-01T09:00:00Z" });

// A thread
await orbit.posts.create({ channels: ["acc_1"], content: ["First.", "Second.", "Third."], scheduledAt });

// Now, with media uploaded through /api/v1/media
await orbit.posts.create({ channels: ["acc_1"], content: "Look", mediaIds: ["med_1"], publishNow: true });

// A draft
await orbit.posts.saveDraft({ channels: ["acc_1"], content: "half an idea" });

Different words on different channels

Give a channel its own content instead of a bare id:

await orbit.posts.create({
  content: "Shipping today.",
  scheduledAt: slot.at,
  channels: ["acc_x", "acc_bs", { accountId: "acc_li", content: "A longer take, for a slower feed." }],
});

Channels sharing words are one post; a channel with its own copy becomes a post of its own, so create returns a list of groupIds (two here). For per-platform options such as TikTok privacy, Instagram Stories or reply settings, use the accounts form, which is the wire shape and is passed through untouched. The compiler holds you to one form or the other.

Reading and changing posts

await orbit.posts.list({ status: ["scheduled"], limit: 50 });
for await (const post of orbit.posts.iterate({ bucket: ["needs_approval"] })) console.log(post.id);

await orbit.posts.get(groupId);
await orbit.posts.reschedule(groupId, "2026-10-02T09:00:00Z");
await orbit.posts.unschedule(groupId);
await orbit.posts.plan(groupId, "2026-10-02T09:00:00Z");
await orbit.posts.publish(groupId);
await orbit.posts.delete(groupId);

Checking a publish

publishNow returns once the post is accepted, not once it's live. The SDK doesn't poll; read it back when you want to know:

const post = await orbit.posts.get(groupIds[0]);
post.status; // "published" | "failed" | "mixed" | "scheduled" | …

const status = await orbit.posts.publishStatus(groupIds);
for (const c of status.accounts) console.log(c.platform, c.status, c.platformUrl ?? c.error);

Deleting a published post

posts.deletePublished removes it from Orbitwrite. It's only taken down from the networks you name in deleteFromAccountIds, or everywhere with fromNetworks. Name none and the live posts stay up.

Everything else

ResourceCovers
channelslist, postable, get, Pinterest boards
schedulenextTime, suggestions, plan a run of posts at a cadence
slotsRead and replace the weekly posting times
postsAlso search, score, scoreContent, cover, setCover, deleteDraft
conversationsThe inbox: list, get, reply, resolve, snooze, assign, bulk sweeps, sync
heldRepliesReplies waiting for approval
teamInboxes, inboxRouting, assignment and availability
hashtagSetsSaved sets, apply, platform guidance
autoDmsStart, update, pause, resume, stop, recipients
analyticsOverview, series, followers, best times, breakdown, insights, top posts, post metrics, benchmarks, results

Request and response types are generated from the API's OpenAPI document and exported, so a renamed route is a compile error, not a runtime surprise. For areas the SDK doesn't wrap yet (media, campaigns, tags, approvals, webhooks), call the API directly.

Errors and retries

Every non-2xx response throws a typed error with status, message, body and any field-level issues: OrbitwriteBadRequestError, OrbitwriteAuthenticationError, OrbitwritePermissionError, OrbitwriteNotFoundError, OrbitwriteConflictError, OrbitwriteRateLimitError (with retryAfterSeconds), OrbitwriteServerError and OrbitwriteConnectionError, all extending OrbitwriteError.

A 429 is always retried, since the limiter refuses before anything runs. Other failures are retried only on GET: a POST /posts that fails late may already have committed, and replaying it would publish twice.