# TypeScript SDK

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

```bash
npm install @orbitwrite/sdk
```

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

## Getting started

```ts
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`.

```ts
// 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:

```ts
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

```ts
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:

```ts
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](https://orbitwrite.com/docs/api-reference) 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.
