# Node.js Social Media API SDK

The official Node.js / TypeScript SDK. Zero runtime dependencies (native `fetch`),
Node 18+, ships ESM and CommonJS, fully typed.

## Install

```bash
npm install @omnisocials/sdk
```

## Authentication

```ts
import { OmniSocials } from "@omnisocials/sdk";

const client = new OmniSocials({ apiKey: "omsk_live_..." });
// or set OMNISOCIALS_API_KEY and call new OmniSocials()
```

CommonJS: `const { OmniSocials, verifyWebhookSignature } = require("@omnisocials/sdk");`

## Create a post

```ts
const { data: post } = await client.posts.create({
  content: "New drop this Friday",
  channels: ["instagram", "facebook", "linkedin"],
  scheduled_at: "2026-08-01T09:00:00Z",
  media_urls: ["https://example.com/teaser.jpg"],
});
console.log(post.id, post.status);
```

Any `media_urls` (or `media_ids`) entry can also be an object carrying per-media
alt text (max 1500 chars), delivered to Mastodon, Bluesky, X (photos/GIFs),
Pinterest, Instagram (images), and LinkedIn (images):

```ts
media_urls: [{ url: "https://example.com/teaser.jpg", alt: "Red sneaker on a white background" }],
```

Publish immediately:

```ts
await client.posts.createAndPublish({
  content: "Going live right now",
  channels: ["x", "bluesky"],
});
```

## Upload media

```ts
const upload = await client.media.uploadFromUrl({
  url: "https://example.com/launch-video.mp4",
  name: "launch-video-v2",
  folder: "Campaigns",
});
console.log(upload.data.id, upload.compatibility);
```

Local files via multipart:

```ts
// From a path
const res = await client.media.upload({ file: "./photos/product.jpg", name: "product-hero" });

// Or from bytes / a Blob
import { readFile } from "node:fs/promises";
const bytes = await readFile("./photos/product.jpg");
await client.media.upload({ file: bytes, filename: "product.jpg" });
```

## Analytics

```ts
// One post's latest per-platform metrics
const { data: stats } = await client.analytics.post("post_id");
console.log(stats.platforms.instagram?.metrics);

// Batch: up to 100 posts in one call
const batch = await client.analytics.posts(["id1", "id2", "id3"]);

// Workspace-wide overview
const { data: overview } = await client.analytics.overview({ period: "30d" });
console.log(overview.total_impressions, overview.total_engagements);

// Account-level stats (followers etc)
const accounts = await client.analytics.accounts({ platform: "instagram" });
```

## Social Inbox

Read and answer DMs, comments and mentions from your connected accounts. The API key
needs the opt-in `inbox:read` / `inbox:write` scopes. The lists use cursor pagination
(`cursor` in, `pagination.next_cursor` out).

```ts
const { data: conversations } = await client.inbox.listConversations({
  platform: "instagram",
  unread: true,
});
const conversationId = conversations[0].conversation_id;

const { data: messages } = await client.inbox.getMessages(conversationId);
await client.inbox.reply(conversationId, { text: "Thanks for reaching out!" });
await client.inbox.markRead(conversationId);

// Comment moderation: hide (pass { hide: false } to unhide) or delete
await client.inbox.hide(messages[0].id);
await client.inbox.deleteMessage(messages[0].id);
```

`inbox.next()` hands out the next conversation that still needs an answer, with the
thread and the post it belongs to. On a comment thread, pass `message_id` so the reply
goes under the comment you answer and not under the newest comment on the post.

```ts
let { data: item, remaining } = await client.inbox.next();
while (item) {
  if (!item.reply_window.open) {
    // An Instagram/Facebook DM past Meta's 24-hour window: answer it in the app
    await client.inbox.markRead(item.message.conversation_id);
    ({ data: item, remaining } = await client.inbox.next());
    continue;
  }

  const reply = await client.inbox.reply(item.message.conversation_id, {
    text: "Thanks! DM sent.",
    message_id: item.message.id,
    include_next: true,
  });
  item = reply.next ?? null;
  remaining = reply.remaining ?? 0;
}
```

See the [Social Inbox guide](/inbox) for the platforms, the reply rules and the error
codes.

## Pinterest product tags

Tag products on a Pin so people can shop the items in the image. List the product Pins
of the connected Pinterest account, then pass up to 24 of their Pin ids as
`product_tags` in the `pinterest` options of the post. Only product Pins of your own
account can be tagged; products of other merchants cannot.

```ts
const list = await client.pinterest.listProducts();

if (list.error) {
  console.warn(list.error.code, list.error.message);
} else {
  await client.posts.create({
    content: "Our summer picks",
    channels: ["pinterest"],
    media_urls: ["https://example.com/summer-look.jpg"],
    scheduled_at: "2026-08-01T09:00:00Z",
    pinterest: {
      board_id: "1234567890",
      product_tags: (list.products ?? []).slice(0, 3).map((product) => product.pin_id),
    },
  });
}
```

The list answers with HTTP 200 in both cases: `products` on success, or an `error`
object (`pinterest_not_connected`, `pinterest_catalog_access_required`,
`platform_error`) when it could not be read. It reads the Pinterest catalog when the
connection has catalog access (given one time in the OmniSocials composer: Pinterest
options, Add products, Connect catalog), else the account's own Pins. The tags are added
right after the Pin is published. A product that Pinterest refuses never fails the post:
the post then carries `pinterest.product_tags_result` with the tagged and the skipped
Pin ids. `client.pinterest.validateProduct(id)` checks one Pin id or Pin link before you
post. On `posts.update()` the `pinterest` object replaces the stored one, so leave
`product_tags` out to remove the tags.

## Verify webhooks

```ts
import express from "express";
import { verifyWebhookSignature, WebhookVerificationError } from "@omnisocials/sdk";

const app = express();

app.post(
  "/omnisocials/webhook",
  express.raw({ type: "application/json" }), // keep the raw body!
  (req, res) => {
    try {
      const event = verifyWebhookSignature({
        payload: req.body, // Buffer of the raw body
        signature: req.get("X-OmniSocials-Signature") ?? "",
        secret: process.env.OMNISOCIALS_WEBHOOK_SECRET!,
        tolerance: 300, // seconds (default)
      });

      switch (event.type) {
        case "post.published":
          console.log("Published:", event.data.post_id, event.data.targets);
          break;
        case "post.failed":
          console.error("Failed:", event.data.post_id);
          break;
        case "post.rejected":
          console.warn("Rejected:", event.data.post_id, event.data.approval?.reason);
          break;
      }
      res.sendStatus(200);
    } catch (err) {
      if (err instanceof WebhookVerificationError) {
        return res.sendStatus(400);
      }
      throw err;
    }
  }
);
```

Events: `post.scheduled`, `post.published`, `post.failed`, `post.approved` and `post.rejected`.
The two approval events carry `data.approval` with `status`, `decided_by` (the approver's
user id) and `reason` (`null` on `post.approved`). To read the full review of a post (steps,
approvers, the rejection reason and the comment thread), call `client.posts.getApproval(id)`.

## Errors

The base class is `OmniSocialsError`. API errors are `APIError` subclasses carrying
`status`, `code`, `message`, and `body`: `ValidationError` (400/422),
`AuthenticationError` (401), `PermissionDeniedError` (403), `NotFoundError` (404),
`RateLimitError` (429, with `.retryAfter`), `ServerError` (5xx), plus
`APIConnectionError` and `WebhookVerificationError`.

```ts
import { APIError, RateLimitError, ValidationError, APIConnectionError } from "@omnisocials/sdk";

try {
  await client.posts.create({ content: "Hi", channels: ["instagram"] });
} catch (err) {
  if (err instanceof RateLimitError) {
    console.warn(`Rate limited, retry in ${err.retryAfter}s`);
  } else if (err instanceof ValidationError) {
    console.error(`Bad request (${err.code}): ${err.message}`, err.body);
  } else if (err instanceof APIConnectionError) {
    console.error("Network problem:", err.message);
  } else if (err instanceof APIError) {
    console.error(`API error ${err.status} (${err.code}): ${err.message}`);
  } else {
    throw err;
  }
}
```

## Configuration

```ts
const client = new OmniSocials({
  apiKey: "omsk_live_...",
  baseUrl: "https://api.omnisocials.com/v1", // default
  timeout: 30_000,   // per-request timeout in ms (default 30s)
  maxRetries: 2,     // automatic retries on 429 / 5xx / network errors (default 2)
});
```

Note that `timeout` is in milliseconds; the other SDKs use seconds or durations.

## Notes

- Responses destructure as `{ data }`; list endpoints return `{ data, pagination }`
- `204` deletes resolve to `null`
- `client.health()` returns `{ status, version, timestamp }`
- Source: [github.com/OmniSocials/omnisocials-node](https://github.com/OmniSocials/omnisocials-node)
