Server API reference
SosoAPI publishing API: authentication, requests and outcomes
Build a server-side publishing integration around one Scheduled Post per destination. This reference covers the publishing API contract reviewed 20 October 2026. Examples contain placeholders and do not run on this page.
Read as MarkdownReviewed by SosoAPI team ·
1. Authenticate from your server
Create an API key in SosoAPI App → Settings → Agents & API Access. Store it as SOSOAPI_API_KEY in your server environment. Requests use Authorization: Bearer <your-key> and Content-Type: application/json. Never embed the key in a website, mobile app bundle, URL or public repository.
All endpoints below use POST with a JSON body. Keys and delegated agent grants are constrained by their capabilities, permitted Workspaces and the owner’s current membership. Reads need agent:operate and at least viewer access; publishing mutations need agent:publish and at least editor access. A grant cannot widen the owner’s rights.
Use the app’s current workspace and connected accounts, or call /identity-api/workspace/list and /identity-api/user/social-accounts/list to find IDs you are authorized to use. Do not guess a customer’s workspace or account ID.
// Node.js 20+; run only on your server.
const apiKey = process.env.SOSOAPI_API_KEY;
if (!apiKey) throw new Error("Set SOSOAPI_API_KEY on your server");
const apiBase = "https://gqeshlawtcexsapabaui.supabase.co/functions/v1";
async function sosoPost(path, body) {
const response = await fetch(apiBase + path, {
method: "POST",
headers: {
Authorization: "Bearer " + apiKey,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const result = await response.json();
if (!response.ok) {
const requestId = result.request_id ?? response.headers.get("x-request-id");
// Handler errors use code/message; schema-validation 400s instead use
// success:false and error:{name:"ZodError",message:...}.
// Preserve status, available code and requestId in server diagnostics.
// Never log the key or an Authorization header.
throw new Error("SosoAPI " + response.status + " (" +
(requestId ?? "no request id") + ")");
}
return result;
}2. Publishing endpoint map
These paths are relative to the API base used in the server helper. Fetch the Scheduled Post detail to get its current version, available actions and publishing result.
| Path | Request | Response and boundary |
|---|---|---|
| /studio-api/scheduled-posts/list | workspace_id; optional project_id, status, time pair, limit, cursor. | posts, next_cursor, returned_count. Workspace-scoped read. |
| /studio-api/scheduled-posts/get | id. | post, delivery, actions, attempts, outcomes. Read the current version here. |
| /studio-api/scheduled-posts/eligibility | workspace_id, targets; optional include_quota. | destinations and optional quota. Read-only preflight, not a reservation. |
| /studio-api/scheduled-posts/create | workspace_id, exact account, provider_payload, content, scheduled_at, idempotency_key. | post and deduped. 201 for a new schedule; 200 for a matching replay. |
| /studio-api/scheduled-posts/bulk-create | workspace_id and 1–50 targets, each with its own idempotency_key. | batch_id, successes, failures. 201 when at least one succeeds; 422 when none succeed. Inspect both arrays. |
| /studio-api/scheduled-posts/update | id, expected_version, patch; optional matching provider_payload. | post. A stale version is rejected; destination is immutable. |
| /studio-api/scheduled-posts/publish-now | id and expected_version. | post. Requests immediate execution; it does not prove publication. |
| /studio-api/scheduled-posts/retry | id, expected_version; optional future scheduled_at. | post. Failed-only, evidence-gated recovery; never a blind resend. |
| /studio-api/scheduled-posts/cancel | id and expected_version. | post. Cancels unpublished work; remote cancellation may remain pending. |
| /studio-api/scheduled-posts/delete | id and expected_version. | post. Machine callers cannot delete already-published provider content. |
| /studio-api/scheduled-posts/withdraw-delete | id and expected_version. | post. Withdraws a stopped, verification-only Instagram Login deletion request; retains provider content. |
3. List and paginate without losing scope
limit defaults to 50 and accepts 1–200. Optional status is one of scheduled, publishing, published, failed, expired, needs_reschedule, cancelled or deleted. scheduled_from and scheduled_to must be supplied together as ISO-8601 instants with offsets; the end must be after the start.
next_cursor is either null or an object with scheduled_at and id. Send that entire object back as cursor alongside the same Workspace and filters. This endpoint does not accept page, offset, platform or social_account_id filters.
await sosoPost("/studio-api/scheduled-posts/list", {
"workspace_id": "<workspace-uuid>",
"limit": 50
});const first = await sosoPost("/studio-api/scheduled-posts/list", {
workspace_id: "<workspace-uuid>", limit: 50
});
if (first.next_cursor) {
const next = await sosoPost("/studio-api/scheduled-posts/list", {
workspace_id: "<workspace-uuid>", limit: 50, cursor: first.next_cursor
});
// Consume next.posts; continue only while next_cursor is not null.
}4. Validate and create one reviewed post
Replace every placeholder with values from your authorized scope and a future time. This example schedules a real Threads text post when executed. Obtain the person’s confirmation of the account, content and time before the create call.
Set provider_payload.platform and that platform’s publish_options. The server checks that they match the selected account. You may include project_id to record where the post came from; the project does not grant publishing access or control the schedule.
| Field | Contract |
|---|---|
| workspace_id / social_account_id | Required UUIDs for the authorized Workspace and exact destination. |
| provider_payload | Required platform discriminator and platform-specific publish_options. |
| scheduled_at / display_timezone | Required ISO-8601 instant with offset; valid IANA display timezone, default UTC. |
| idempotency_key | Required trimmed string, 8–160 characters, stable for this one intent. |
| caption / hashtags | Caption is nullable, at most 5,000 characters; default null. Hashtags default []; at most 100 strings of 100 characters. Provider limits can be narrower. |
| media | Default []; at most 20 registered media references. Each has kind image/video and a canonical url; include its ready asset_id. Platform rules can be narrower. |
| link_url | Optional URL or null. Destination and post type must support it. |
| local_scheduled_at / schedule_dst_disambiguation | Optional pair: local time without offset and exact/earlier/later. Supply both or neither; they must match the intended instant and timezone. |
| expires_at / automatic_retry_enabled | Optional expiry after scheduled_at; null means no expiry. Automatic retry defaults true and remains subject to provider safety. |
| project_id / source_ref | Optional nullable Project UUID and caller provenance object. Neither changes authority. |
await sosoPost("/studio-api/scheduled-posts/eligibility", {
"workspace_id": "<workspace-uuid>",
"include_quota": true,
"targets": [
{
"social_account_id": "<threads-account-uuid>",
"media": [],
"provider_payload": {
"platform": "threads",
"publish_options": {
"post_type": "TEXT"
}
},
"draft": {
"caption": "A reviewed update from our team.",
"hashtags": [],
"scheduled_at": "<future-ISO-8601-instant-with-offset>",
"display_timezone": "Asia/Hong_Kong"
}
}
]
});await sosoPost("/studio-api/scheduled-posts/create", {
"workspace_id": "<workspace-uuid>",
"social_account_id": "<threads-account-uuid>",
"caption": "A reviewed update from our team.",
"hashtags": [],
"media": [],
"provider_payload": {
"platform": "threads",
"publish_options": {
"post_type": "TEXT"
}
},
"scheduled_at": "<future-ISO-8601-instant-with-offset>",
"display_timezone": "Asia/Hong_Kong",
"idempotency_key": "<stable-key-for-this-one-intent>"
});5. Read the outcome and cancel safely
Create returns { post, deduped }. List returns { posts, next_cursor, returned_count }. Get returns { post, delivery, actions, attempts, outcomes }. A post includes id, status, version, destination, schedule and available provider identity/permalink; delivery and actions explain current progress and permitted next steps.
A post with status scheduled or publishing is still pending. A terminal publish outcome with status published confirms publication and includes the provider evidence available for that post; a permalink may be missing. If the client disconnects or a response is delayed, read the existing post before reporting a result.
Before cancelling, fetch the post detail and pass its current numeric version. The result may be cancellation_pending while SosoAPI cancels a schedule held by the provider. Cancelling a schedule does not delete an already-published post.
await sosoPost("/studio-api/scheduled-posts/get", {
"id": "<scheduled-post-uuid>"
});const detail = await sosoPost("/studio-api/scheduled-posts/get", {
id: "<scheduled-post-uuid>"
});
// Run only after the person confirms cancelling this unpublished post.
await sosoPost("/studio-api/scheduled-posts/cancel", {
id: detail.post.id, expected_version: detail.post.version
});6. Idempotency, conflicts and uncertain writes
Use the same idempotency_key when repeating the same create request. A matching replay returns the existing post with deduped=true. Changing the content while reusing the key causes a conflict. This prevents duplicate creates in SosoAPI; it does not guarantee exactly-once delivery by the provider.
For updates and actions, pass expected_version from the latest detail response. If it conflicts, fetch the post again and review your change against the new version. A lost response is a reason to check existing state, not to create another post with a new key.
The duplicate-content guard may reject a separate request with the same normalized public content, account and a nearby publishing time. Inspect the existing-post reference if you are authorized to read it, and ask the user before changing the request. Bulk requests return a result for each destination; some can succeed while others fail.
7. Errors, rate limits and integration limits
Handler errors use an envelope with error, message, optional code, retryable, request_id, optional details and retry_after_seconds. Schema-validation 400s return a different shape: { success: false, error: { name: "ZodError", message: "…" } }. The validation message currently contains the serialized field issues, including their paths; do not assume the handler-envelope fields are present.
When diagnosing a failure, keep the HTTP status, error code and correlation ID from result.request_id ?? response.headers.get("x-request-id"). Use validation field paths to fix the request. For authentication or permission errors, fix the credential or grant before trying again.
The current studio-api limit is 240 requests per minute per human user and 480 per minute in that user’s separate machine-credential bucket. These are request ceilings, not publishing throughput or plan quotas. Other endpoints and social providers have separate limits. Follow the live Retry-After header on 429 or retryable 503; do not busy-loop.
Read status at bounded intervals while the user is following the post, or use the app’s live status view. This API does not promise outbound webhooks, a subscriber SLA, identical features across platforms or a fixed publishing time.
| Status | Meaning and response |
|---|---|
| 400 | Schema validation can return success:false with a ZodError in error, rather than the handler envelope. Correct its field issues; use x-request-id if the body has no request_id. Handler-level invalid or unsupported requests can also use 400. |
| 401 / 403 | Missing/expired credential or denied capability, role, Workspace or account access. Repair the exact grant or membership. |
| 404 | The resource is unavailable in the authorized scope. Verify the identity. |
| 409 | State/version, idempotency, duplicate-content or eligibility conflict. Re-read; do not blindly resend. |
| 422 for bulk-create | No destination succeeded. Inspect failures; partial success instead uses 201. |
| 429 | Request throttle. Honor Retry-After and keep the same intent identity. |
| 503 / other 5xx | Temporary service failure or internal fault. Preserve request_id; inspect existing state before retrying an uncertain mutation. |