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
| Option | Default | |
|---|---|---|
apiKey | $ORBITWRITE_API_KEY | A workspace key (orbit_sk_…). |
accessToken | An OAuth access token, sent instead of a key. | |
baseUrl | https://orbitwrite.com | |
organizationId | from the key | Set it to skip one GET /me lookup. |
timeoutMs | 30000 | 0 disables it. |
maxRetries | 2 | Extra attempts after a retryable failure. |
fetch | global | A proxy agent or a test stub. |
headers, userAgent | Extra 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
| Resource | Covers |
|---|---|
channels | list, postable, get, Pinterest boards |
schedule | nextTime, suggestions, plan a run of posts at a cadence |
slots | Read and replace the weekly posting times |
posts | Also search, score, scoreContent, cover, setCover, deleteDraft |
conversations | The inbox: list, get, reply, resolve, snooze, assign, bulk sweeps, sync |
heldReplies | Replies waiting for approval |
teamInboxes, inbox | Routing, assignment and availability |
hashtagSets | Saved sets, apply, platform guidance |
autoDms | Start, update, pause, resume, stop, recipients |
analytics | Overview, 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.