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/nodeQuick 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
| Method | What 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.