SDKsNode.js SDK

SDKs

Node.js SDK

Install @sofia-post/node and call Sofia API from Node.js: publish, schedule, upload media, read insights and register webhooks, with typed answers and errors.

The official Node.js SDK wraps Sofia API v1 in a single class, Sofia: one method per route, typed requests and answers, and one error type for every failure. It has no dependencies and runs on Node.js 20 or later.

Installation

The SDK is published on npm as the @sofia-post/node package. It works with import and require alike.

npm install @sofia-post/node

Quick start

Create a client with your key, find the establishment to publish for, then schedule a post on Instagram:

import { Sofia } from '@sofia-post/node';

const sofia = new Sofia(process.env.SOFIA_API_KEY);

const [establishment] = await sofia.establishments();

const post = await sofia.post({
  establishmentId: establishment.id,
  targets: [{ platform: 'instagram', text: 'Tarte tatin du jour 🍎' }],
  scheduledAt: new Date('2026-12-01T09:00:00Z'),
});

console.log(post.id, post.status);

Authentication

The constructor takes a personal key, created in Settings › API, Personal access tab, or an OAuth access token if your application acts for other Sofia users (OAuth2). The SDK sends it as Authorization: Bearer … on every request.

The client talks to https://api.sofia-post.com by default. The second argument changes the base URL, or the fetch it sends requests with:

const sofia = new Sofia(apiKey, { baseUrl: 'http://localhost:3008' });

Methods

MethodWhat it does
me()Who the key acts for, its scopes, and whether it is a test key
platforms()Each network's rules: caption length, media, formats
establishments()The establishments the key reaches
accounts(establishmentId)The social accounts connected to an establishment
connectionLink(establishmentId, input)A page hosted by Sofia where the person connects a network
insights(establishmentId, period?)Reach, engagement and followers, network by network
post(input, options?)Publish now, schedule with scheduledAt, or save a draft
postList(filters?)A page of posts, filtered by establishment, status or dates
getPost(id) · updatePost(id, input) · deletePost(id)Read, change or withdraw one post
upload(input) · uploadFromUrl(input)Add a file or a URL to the establishment's media library
mediaList(filters) · getMedia(id) · deleteMedia(id)Read and clean up the media library
createWebhook(input) · webhooks() · deleteWebhook(id) · testWebhook(id)Be told when a post goes out or fails

Posts

A post has one target per network, each with its own text, media and format. Check it first with dryRun: nothing is written, and a refusal lists every rule it breaks. Pass an idempotencyKey so that a retried request never publishes twice. scheduledAt takes a Date or an ISO 8601 string.

import type { PublishPostInput } from '@sofia-post/node';

const input: PublishPostInput = {
  establishmentId,
  targets: [
    { platform: 'instagram', text: 'Menu du jour', media: [{ id: media.id }] },
    { platform: 'facebook', text: 'Menu du jour : tarte tatin et café' },
  ],
};

await sofia.post(input, { dryRun: true });
await sofia.post(input, { idempotencyKey: crypto.randomUUID() });

Media

Upload the file once, then put its id in the media of a post. upload takes a Buffer, a Uint8Array or a Blob, and reads the content type from the file name unless you give contentType. uploadFromUrl lets Sofia download the file itself.

import { readFile } from 'node:fs/promises';

const media = await sofia.upload({
  establishmentId,
  file: await readFile('tarte-tatin.jpg'),
  fileName: 'tarte-tatin.jpg',
});

const imported = await sofia.uploadFromUrl({ establishmentId, url: 'https://example.com/menu.png' });

Pagination

postList and mediaList answer one page at a time. Pass the nextCursor of a page as the cursor of the next call; the last page has none.

let cursor: string | undefined;
do {
  const page = await sofia.postList({ establishmentId, status: 'published', limit: 100, cursor });
  page.data.forEach((post) => console.log(post.id));
  cursor = page.nextCursor;
} while (cursor);

Errors

Every method throws a SofiaApiError when the API refuses the call. It carries the HTTP status, the stable code of the error envelope, its details (for a refused post, each network rule it breaks), the requestId to give support and, on 429, retryAfterSeconds.

import { SofiaApiError } from '@sofia-post/node';

try {
  await sofia.post(input);
} catch (error) {
  if (!(error instanceof SofiaApiError)) throw error;
  if (error.code === 'post_refused') console.log(error.details);
  if (error.status === 429) await wait(error.retryAfterSeconds);
}

When the API cannot be reached at all, the error has the code network_error and the status 0. The SDK never retries on its own: wait for retryAfterSeconds on 429 and back off on 5xx.

TypeScript

The package ships its types: PublishPostInput, Post, Media, SocialAccount, EstablishmentInsights and the others are exported next to Sofia, so your editor completes every field and every enum value.

Next: the API reference describes every route the SDK calls, and the CLI drives the same API from a terminal.

Was this page helpful?