Simple Socials API v1.0.0

API-first publishing to Facebook Pages and Instagram. Simple Socials is operated by GAU Web Solutions. Limits are per Workspace and can be raised by the operator: requests per minute (default 60; over it you get `429 rate_limited` with `Retry-After`), connected Social Accounts (default 10; see Connect Links) and max file size (default 250 MB; `media_too_large`). The current limits are in `GET /v1/workspace`. A Workspace disabled by the operator gets `403 workspace_disabled` on every endpoint, its Connect Links stop working and its scheduled work (publishing, media copies, webhook deliveries) is paused until it is re-enabled.

Base URL https://api.simplesocials.dev. Machine-readable spec: /openapi.json.

Authentication

Send your API Key as a bearer token on every /v1 request:

Authorization: Bearer ssk_xxxxxxxx_…

A missing, invalid or revoked key gets 401 with code unauthorized.

Errors

Every error uses the same JSON shape:

{"code": "validation_failed", "message": "Human-readable explanation.", "details": {}}

Stable codes:

Webhooks

Register an endpoint with POST /v1/webhook-endpoints to be notified instead of polling. Events:

Each body is {"id": "evt_…", "type": "…", "created_at": "…", "data": {…}}.

Verifying deliveries

Every delivery is a `POST` with a JSON body, signed with the endpoint's secret. To verify it: take the `Simple-Socials-Timestamp` header (Unix seconds) and the raw request body, compute HMAC-SHA256 with the secret (the whole `whsec_…` string as the key) over `timestamp + "." + body`, hex-encode it, and compare it in constant time with the `v1=` value in `Simple-Socials-Signature`. Reject deliveries whose timestamp is more than 5 minutes from your clock, to stop replays. Answer with any 2xx status within 10 seconds; anything else (including redirects and timeouts) is retried with exponential backoff for about 24 hours. Delivery is at least once and unordered: deduplicate by event `id`.

// Node.js
const crypto = require("crypto");
function verify(secret, headers, rawBody) {
  const ts = headers["simple-socials-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const want = crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  return headers["simple-socials-signature"].split(",").some((part) => {
    const got = part.trim().replace(/^v1=/, "");
    return got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
  });
}

Endpoints

GET /v1/workspace

Get the current Workspace

Returns the Workspace that owns the API Key used to authenticate the request.

StatusMeaning
200The Workspace.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

POST /v1/webhook-endpoints/{id}/rotate-secret

Rotate a Webhook Endpoint's secret

Replaces the signing secret immediately and returns the new one (shown only here). Every delivery attempt from now on, including retries of earlier events, is signed with the new secret.

StatusMeaning
200The endpoint with its new `secret`.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /v1/webhook-endpoints/{id}/deliveries

List an endpoint's deliveries

Every event sent (or being sent) to the endpoint, with each HTTP attempt.

StatusMeaning
200A page of deliveries.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
422The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /v1/webhook-endpoints/{id}

Get a Webhook Endpoint

StatusMeaning
200The endpoint (without its secret).
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

PATCH /v1/webhook-endpoints/{id}

Update a Webhook Endpoint

Changes the fields present in the body; omitted fields stay as they are. `description: null` clears the description.

StatusMeaning
200The updated endpoint.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
422The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

DELETE /v1/webhook-endpoints/{id}

Delete a Webhook Endpoint

Removes the endpoint and its delivery history; pending retries stop.

StatusMeaning
204Deleted.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /v1/webhook-endpoints

List Webhook Endpoints

StatusMeaning
200A page of Webhook Endpoints.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
422The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

POST /v1/webhook-endpoints

Register a Webhook Endpoint

Registers a URL to receive signed events. The response includes the endpoint's signing `secret`, shown only here and when rotated; store it to verify deliveries. A Workspace can have at most 10 endpoints.

StatusMeaning
201Registered; includes `secret`.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
422The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /v1/social-accounts/{id}

Get a Social Account

StatusMeaning
200The Social Account.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

DELETE /v1/social-accounts/{id}

Disconnect a Social Account

Destroys the account's stored token and removes it from the Workspace. Connecting the same account again later restores it with the same ID.

StatusMeaning
204Disconnected.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /v1/social-accounts

List Social Accounts

Lists the Workspace's connected Social Accounts. Paginate with `limit` and the opaque `cursor` from the previous page's `next_cursor`; `next_cursor` is null on the last page.

StatusMeaning
200A page of Social Accounts.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
422The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

POST /v1/posts/{id}/cancel

Cancel a Post

Cancels the Post's unpublished targets (`scheduled`, or `publishing` while queued or waiting to retry); cancelled targets never publish. Targets that already published or failed are left as they are. Send `target_ids` to cancel only some targets; omit the body (or the field) to cancel all. A target whose publish request is already in flight at the platform may still publish, and then reports `published`. Returns the Post in its new state.

StatusMeaning
200The Post.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
422The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /v1/posts/{id}

Get a Post

Returns the Post with each target's status, platform post ID, permalink and error.

StatusMeaning
200The Post.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /v1/posts

List Posts

Lists your Workspace's Posts, newest first (by `created_at`), with their targets. Filters combine with AND; date ranges include `*_after` and exclude `*_before`, and take RFC 3339 date-times with an offset. An invalid filter fails with `validation_failed`, `details.field` naming it.

StatusMeaning
200A page of Posts.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
422The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

POST /v1/posts

Create a Post

Creates a Post and fans it out into one Post Target per Social Account. The call returns at once; each target is published by a background job. Poll `GET /v1/posts/{id}` for the outcome. Set `publish_at` to schedule the Post: its targets stay `scheduled` until then; without it (or with a time in the past) they publish as soon as possible. Cancel unpublished targets with `POST /v1/posts/{id}/cancel`. Content is validated against every target's platform first, with that platform's `overrides` applied (Facebook: a caption, a link, or both; or one to 10 photos (JPEG, PNG or GIF, at most 10 MB each; several become one multi-photo post); or one video (MP4 or MOV, at most 1 GB and 20 minutes); captions up to 63,206 characters, optional with Media; photos and videos can't be mixed, and posts with Media can't have a `link` (put it in the caption). Instagram: one JPEG image, one MP4/MOV video published as a Reel, or a carousel of 2–20 images and videos; images JPEG, at most 8 MB, aspect ratio 4:5 to 1.91:1; videos 3 s to 15 min, at most 300 MB; captions up to 2,200 characters, 30 hashtags and 20 @mentions; text-only posts are rejected, and so are Posts with a `link` unless `overrides.instagram.link` removes it (`""` or `null`), naming the target). Every Media in `media_ids` must exist in your Workspace and be `ready`; Media that cleanup has deleted fails with 422 `media_deleted` (`details.field` names it), so upload it again. A problem fails the whole request with `validation_failed`, where `details.field` names the field and `details.social_account_id` the offending target. Targeting a Social Account that is `needs_reauth` fails with 422 `account_needs_reauth` (same details); reconnect it first. Send an `Idempotency-Key` header to make retries safe: repeating the request with the same key and body (in the same Workspace) returns the original Post with `Idempotent-Replayed: true` instead of creating another. Reusing a key with a different body (including `media_ids` and `overrides`) returns 422 `idempotency_key_reused`. Keys are kept for at least 24 hours. Facebook publishes a video that fits its Reels specs (portrait, about 9:16, at least 540×960, 3–90 seconds) as a Reel, and other videos as Page videos; set `overrides.facebook.video_type` to `reel` or `video` to choose (`auto` is the default). Facebook fetches the video from its Media URL; the target stays `publishing` while Facebook processes it (checked every 30 seconds, giving up after 2 hours). Facebook allows 30 API-published Reels per Page per rolling 24 hours. Creating a Post checks this first: when the Page already has 30 Reels published in the 24 hours before the Post's publish time, or pending (scheduled or in progress) and due to publish within 24 hours either side of it (an overdue one counts as publishing now), the request fails with 422 `publishing_limit_reached` naming the target (`details.field`, `details.social_account_id`, plus `cap`, `limit`, `window_seconds`, `count`). The check is repeated just before publishing (Reels in progress count): a Reel over the limit then fails its target with `publishing_limit_reached` (same details). Instagram publishes in the background through Instagram's media containers: the job creates the containers, checks once a minute until Instagram has processed them (giving up after 30 minutes), then publishes. Before creating containers it checks the account's 24-hour publishing limit (`content_publishing_limit`, currently 50 posts; a carousel counts as one), counting the account's other Instagram posts still being published through Simple Socials on top of Instagram's own count; when it is used up the target fails with `publishing_limit_reached`.

StatusMeaning
201The Post (or, for a replayed Idempotency-Key, the original Post in its current state).
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
422Invalid request (`validation_failed`), an Idempotency-Key reused with a different body (`idempotency_key_reused`), a target that `needs_reauth` (`account_needs_reauth`), deleted Media (`media_deleted`), or a target already at a platform publishing cap (`publishing_limit_reached`; `details.social_account_id` names it).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

POST /v1/media/{id}/complete

Complete a direct upload

Checks the uploaded file and marks the Media `ready`, recording its detected type, size and, where detectable, dimensions and duration. If the file is larger than the Workspace's limit, or its bytes are not the declared type, it is deleted, the Media becomes `failed` and the error is returned. Completing `ready` Media again returns it unchanged.

StatusMeaning
200The ready Media.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
409Nothing has been uploaded yet, or the Media is not a pending upload. Codes: `media_not_uploaded`, `media_not_pending`.
422The uploaded file was rejected; the Media is now `failed`. Codes: `media_too_large`, `unsupported_media_type`.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /v1/media/{id}

Get Media

Returns the Media's status, type, size and, once `ready`, its public URL.

StatusMeaning
200The Media.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
404No such resource in your Workspace (code `not_found`).
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

POST /v1/media/uploads

Start a direct upload

Creates `pending` Media and returns a presigned URL. Upload the file with an HTTP `PUT` to `upload.url`, sending exactly the headers in `upload.headers` (the Content-Type is part of the signature), before `upload.expires_at` (1 hour). The bytes go straight to storage, not through the API. Then call `POST /v1/media/{id}/complete`. Supported types: `image/jpeg`, `image/png`, `image/gif`, `video/mp4`, `video/quicktime` (each platform accepts a subset when you post). `size` must not exceed the Workspace's max file size (250 MB by default); the actual upload is checked again on complete.

StatusMeaning
201Pending Media with upload instructions.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
422The request is invalid. Codes: `validation_failed`, `unsupported_media_type`, `media_too_large`.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

POST /v1/media

Create Media from a URL

Creates `pending` Media and copies the file at `url` into storage in the background. Poll `GET /v1/media/{id}` until it is `ready` (or `failed`, with an `error`). The URL must be publicly reachable over http(s); URLs pointing at private, loopback or link-local addresses (including through DNS or redirects) are refused with `source_url_not_allowed`. The file's type is detected from its bytes, not the server's Content-Type. Failure codes: `source_fetch_failed` (HTTP error or unreachable after retries), `source_url_not_allowed`, `media_too_large`, `unsupported_media_type`, `storage_failed`.

StatusMeaning
202Pending Media; the copy runs in the background.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
422The URL is malformed or not allowed. Codes: `validation_failed`, `source_url_not_allowed`.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

POST /v1/connect-links

Create a Connect Link

Creates a single-use link to a hosted page where the owner of a social account signs in with the platform and chooses which accounts to attach to your Workspace. Send the returned `url` to them. The link expires after one hour and can complete once. When `redirect_url` is given, the person is sent back to it afterwards with `status=connected`, `connect_link_id` and `social_account_ids` (comma-separated), or with `status=error`, `connect_link_id` and `error` (one of `access_denied`, `missing_permissions`, `no_accounts`, `personal_account`, `nothing_selected`, `account_mismatch`, `platform_error`, `link_expired`, `link_used`, `invalid_state`, `account_limit_reached`, `workspace_disabled`). Your Workspace's Social Account limit (`limits.max_social_accounts`) is enforced when the person confirms their selection: accounts that are `active` or `needs_reauth` count, `disconnected` and deleted ones don't. Reconnecting an account that already counts is always allowed; if the new accounts would go over the limit, none are attached, the page answers 422 and the redirect carries `error=account_limit_reached` (the link stays usable, so they can choose fewer). **Reconnecting:** pass `social_account_id` to repair an existing Social Account (for example one that is `needs_reauth` or `disconnected`) in place. `provider` may then be omitted; if given it must match the account's. The person must sign in as that same platform account: the account picker offers only it, and signing in as anyone else fails with `error=account_mismatch` without changing anything (the link stays usable). On success the account is `active` again with the same ID and a new token.

StatusMeaning
201The Connect Link.
401The API Key is missing, invalid or revoked (code `unauthorized`).
403The operator disabled this Workspace (code `workspace_disabled`).
422The request is invalid (code `validation_failed`); `details.field` names the offending field when there is one.
429The Workspace's requests-per-minute limit was reached (code `rate_limited`). Wait `Retry-After` seconds; `details.limit` is the limit and `details.retry_after_seconds` the wait.

GET /healthz no auth

Health check

Returns 200 when the service and its database are reachable, 503 otherwise. No authentication.

StatusMeaning
200Healthy.
503The database is unreachable.