OverviewAPI Overview

Overview

API Overview

Every endpoint of Sofia API v1 with its parameters, scopes and examples, and a playground to try each one with your own API key.

Base URL https://api.sofia-post.com/api · OpenAPI document

The Sofia API gives your code access to your Sofia account: the establishments of your Workspaces, their connected networks, their media library, their posts and their statistics. It is a REST API over HTTPS; requests and responses are JSON. It lets you:

  • Publish: now, at a date or as a draft, on one or several networks at once.
  • Check before publishing: a dry run tells you whether each network would accept the post, without writing anything.
  • Manage media: upload a file or import it from a URL into an establishment’s media library.
  • Track posts: read each network’s state, its permanent link or the reason for the failure. A signed webhook can also notify you.
  • Read statistics: the latest figures collected by Sofia for each connected account.
  • Connect networks: create a hosted link that lets the owner connect a missing network.

SDKs and integrations

  • `sofia` CLI: the CLI, published on npm as sofia-post, to drive Sofia from a terminal.
  • MCP server: plug Sofia into your AI agents.
  • Zapier: the Sofia integration connects with your personal key. It offers the “Post Published” and “Post Failed” triggers, the “Create Post” action (publish, schedule or save a draft) and the “Find Post” search. It has not yet been submitted to Zapier’s public directory.

Authentication

Every call carries a Bearer token in the Authorization header. Sofia re-reads the rights attached to it on every call: a person removed from a Workspace, or a revoked authorization, stops the token from the next request.

There are two ways to authenticate:

MethodForToken
Personal keyAutomating your own account, scripts, agentssofia_pk_live_…, valid until it expires
OAuth, authorization code + PKCEA product that acts for its users, on the Workspace of their choicesofia_live_at_…, one hour, renewable
OAuth, client credentialsOne of your servers that automates a Workspace you ownsofia_live_at_…, one hour

API key

If you automate your own Workspace, a personal key is enough. In the Sofia app, open Settings › API, Personal access tab, and create a key. Choose its name, its expiry and its permissions. The key is shown only once: copy it and keep it in a secrets vault, never in your code.

Create a key in Settings › API › Personal access. It acts for you on all your Workspaces: the establishment you name determines the Workspace, and your role in it decides what the key can do.

Send the key in the Authorization header, as a Bearer token:

curl https://api.sofia-post.com/api/v1/establishments \
  -H "Authorization: Bearer sofia_pk_live_…"

`GET /v1/me` tells you who the key acts for, its Workspaces and its permissions.

A call without the required permission (the scope) answers 403 insufficient_scope.

OAuth2 token

For a SaaS product that acts for its users on their own Workspaces, register an OAuth client: each person authorizes your application on the Workspace of their choice. OAuth access tokens start with sofia_live_at_…, last one hour and are used the same way:

curl https://api.sofia-post.com/api/v1/establishments \
  -H "Authorization: Bearer sofia_live_at_…"

Are you building an app that other Sofia accounts connect to? Its authorization code flow with PKCE, its client credentials and its scopes are described in OAuth2.

Base URL

Every endpoint lives under a single base URL:

https://api.sofia-post.com/api/v1

Rate limits

The Sofia API counts requests over three windows: 15 minutes, 24 hours and 30 days.

Token15 minutes24 hours30 days
Personal key (per account)1501,00010,000
OAuth client10050015,000
OAuth client, per authorizing person (up to 1,000)+10+25+100

Headers

Every response tells you what is left, in the IETF format:

RateLimit: "15m";r=144;t=43, "24h";r=994;t=13543, "30d";r=9994;t=13543
RateLimit-Policy: "15m";q=150;w=900, "24h";q=1000;w=86400, "30d";q=10000;w=2592000

r is the number of requests remaining, t the seconds before the window resets, q the quota and w the window in seconds.

When a window is full

The response is 429 rate_limit_exceeded, with a Retry-After header and the exhausted window in details. Wait that delay before trying again.

Using the API efficiently

  • Register a webhook rather than polling a post in a loop.
  • Use limit up to 100 to read lists in fewer pages.
  • Cache what rarely changes: platforms, establishments, connected accounts.

Errors

Every error answers with an HTTP status and the same JSON envelope. code is the contract your code reads; message is written for a human and may change.

{
  "error": {
    "code": "invalid_request",
    "message": "The request is invalid",
    "details": [
      { "field": "targets[0].text", "code": "required", "message": "targets[0].text is required" }
    ],
    "docUrl": "https://www.sofia-post.com/developers/errors#invalid_request",
    "requestId": "2aab5943-6333-4caf-8478-25a85ebd4abf"
  }
}
  • details says more: the rejected field, a network’s reason, the exhausted rate-limit window.
  • docUrl leads to the explanation of the code on this page.
  • requestId identifies the request: give it to support.

Error codes

StatusCodeMeaning
400invalid_requestThe body, the query or a parameter is invalid. One detail per refused field: field, code (required, is_string, max…) and message.
400invalid_jsonThe body is not valid JSON.
400invalid_cursorThe cursor was not returned by Sofia API.
400webhook_url_invalidThe webhook address is not https, or leads to a private address.
400webhook_event_invalidUnknown webhook event.
400application_name_invalidSettings › API: the name of the client is missing or too long.
400application_profile_invalidSettings › API: a field of the client profile is invalid; the detail names it.
400logo_invalidSettings › API: the logo is refused (at least 180×180 px, PNG, JPEG or WebP, 2 MB at most).
401invalid_tokenThe token is missing, unknown, expired or revoked, or the person behind it lost the rights it carries. See the WWW-Authenticate header.
401unauthorizedAuthentication is required.
402subscription_requiredA live write (dry run included) needs an active subscription or trial.
403insufficient_scopeThe token lacks the scope of the endpoint; the detail names it.
403insufficient_roleYour role in this Workspace does not allow this action.
403live_mode_requiredA real write with a test token.
403route_closed_to_applicationsThis endpoint is not open to applications.
403forbiddenAccess is refused.
403session_requiredSettings › API: this action needs a Sofia session, not a token.
403impersonation_forbiddenSettings › API: this action is refused in a borrowed session.
403workspace_owner_requiredOnly the owner of the Workspace can do this.
403workspace_member_requiredYou are not a member of this Workspace.
404route_not_foundNo endpoint answers this method and path.
404establishment_not_foundUnknown establishment, or out of reach of the token.
404media_not_foundUnknown media, or out of reach of the token.
404post_not_foundUnknown post, or out of reach of the token.
404webhook_not_foundUnknown webhook, or one of another application.
404not_foundThe resource does not exist.
404application_not_foundSettings › API: unknown client.
404grant_not_foundUnknown authorization, or already revoked.
404personal_key_not_foundSettings › API: unknown personal key.
405method_not_allowedThis method is not allowed on this endpoint.
409post_in_flightA network is sending the post: retry the deletion a moment later.
409post_not_editableA network already sent the post: it can no longer be edited.
409account_already_connectedThis network is already connected to the establishment.
409webhook_limit_reachedThe application already has 10 webhooks.
409idempotency_in_progressThe first request with this Idempotency-Key is still running.
409conflictThe request conflicts with the current state of the resource.
409scope_removal_forbiddenSettings › API: a permission of a client can be added but not removed.
409public_client_has_no_secretSettings › API: a public client has no secret.
413payload_too_largeThe file is above the upload limit.
415unsupported_media_typeUnsupported content type.
422post_refusedA network, the establishment or your plan would refuse the post. One detail per reason, each with its message.
422media_import_refusedThe media could not be imported from this URL; the detail gives the reason.
422media_upload_refusedThe uploaded file is not a supported image or video.
422platform_not_offeredThis network is not offered for connection.
422idempotency_key_reusedThis Idempotency-Key was used with a different body.
422unprocessable_entityThe request is understood but cannot be processed.
429rate_limit_exceededA rate limit window is full: wait for Retry-After.

Reasons a post is refused

A post_refused lists each reason in details[].code:

CodeMeaning
unsupported_platformThis network is not supported.
duplicate_platformThe same network is named twice.
unsupported_formatThis network does not offer this format.
caption_too_longThe text is too long (max, actual).
too_many_hashtagsToo many hashtags (max, actual).
too_many_mentionsToo many mentions (max, actual).
too_many_thread_postsThe thread is too long (max, actual).
too_many_mediaToo many media (max, actual).
media_mix_refusedPhotos and videos cannot be mixed on this network.
media_requiredThis network needs a photo or a video.
media_type_refusedThis network does not accept this type of media.
scheduled_in_pastscheduledAt is in the past.
scheduled_at_invalidA scheduled post needs a future scheduledAt.
account_not_connectedNo account connected on this network for this establishment.
account_ambiguousSeveral accounts are connected on this network: name one with accountId.
media_not_foundThe media is not in the library of this establishment.
media_url_invalidThe media URL must be a public https address.
platform_unavailableThis network is not open on Sofia yet.
publication_quota_exceededThe publication quota of your plan is reached.

Handling errors

  • Base your logic on code, never on message.
  • Retry a 429 after Retry-After, and a 5xx with exponential backoff.
  • Never retry a 4xx unchanged: fix the request first.
  • Log requestId on every failure.

Pagination

Lists (GET /v1/posts, GET /v1/media) are returned most recent first, one page at a time. limit sets the page size, from 1 to 100 (20 by default).

{
  "data": [ … ],
  "nextCursor": "WyIyMDI2LTEwLTI3VDE1OjExOjU5LjAxN1oiLCIx…"
}

Pass nextCursor as cursor to read the next page. When nextCursor is absent, you have reached the end.

let cursor;
do {
  const url = new URL('https://api.sofia-post.com/api/v1/posts');
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('cursor', cursor);
  const page = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${key}` } }).then((r) => r.json());
  handle(page.data);
  cursor = page.nextCursor;
} while (cursor);

A cursor is opaque: never build or modify one. A cursor that Sofia did not return answers 400 invalid_cursor.

Idempotency

A network error can leave you unsure whether a post was created. Send an Idempotency-Key header with POST /v1/posts and POST /v1/media, and retry with the same key: Sofia answers what it answered the first time, and creates nothing twice.

curl -X POST https://api.sofia-post.com/api/v1/posts \
  -H "Authorization: Bearer $SOFIA_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d @post.json
  • Use a new random key (a UUID) for each new post, and the same key for each retry of that post.
  • The same key with a different body answers 422 idempotency_key_reused.
  • While the first request is still running, the same key answers 409 idempotency_in_progress: try again a moment later.

Test mode and dry run

Dry run. Add ?dry_run=true to POST /v1/posts: Sofia runs the checks of a real post (each network’s rules, the connected account, the media, the date and your plan) without saving or sending anything. The detail of the responses is on the Publish page.

Test mode. An application created in test mode (sofia_test_at_… tokens) reads your real Workspace but never writes to it: its posts are simulated and judged by the same rules, and any real write answers 403 live_mode_required. Keys and clients created in Settings › API are in live mode; use the dry run to check without publishing.

OAuth

Account

Platforms

Establishments

Media

Posts

Webhooks