Skip to content
Developers · API v1

Schedule posts
with code.

Every brand has its own API key and secret. Your external system — a CMS, automation or production pipeline — can schedule, edit, cancel or publish a video or post to every platform it suits. You choose which platforms and which actions each key may use from the panel.

Overview

#How it works

The Publishing API is the programmatic counterpart of the planner in the Social Analyze panel. Everything you do there — choosing accounts, uploading media, per-platform suitability checks, scheduling, editing, cancelling — exists here and goes through the same validation. Posts created through the API show up in the panel with an “API” badge and can be edited there too.

  1. Create a keyPanel → Brand settings → API. Pick platforms and permissions; the secret is shown only once.
  2. Prepare mediaImport the video/image from a URL or upload it in chunks; you get a media id when it is ready.
  3. Check suitabilityFind out which accounts the content suits, or let targets:"auto" send it to the suitable ones.
  4. ScheduleGive a time in the brand's timezone. Get the result by webhook or poll the status.
Scope of the API
The API currently publishes to Facebook, Instagram, YouTube, LinkedIn and TikTok. X is connected to Social Analyze for statistics only and is not publishable. A key can only reach the accounts of a single brand.
Getting started

#Quick start

The flow below schedules a video, at a time in the brand's timezone, to every allowed account the content suits. Keep the key and secret in environment variables; never put them in client-side code.

export SA_KEY="sak_…"      # from the panel
export SA_SECRET="sas_…"

# 1) Verify the connection
curl https://app.socialanalyze.org/api/v1/me -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET"

# 2) Import the video from a URL  → 202 + media id
curl -X POST https://app.socialanalyze.org/api/v1/media/import -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://cdn.example.com/launch.mp4"}'

# 3) Wait until it is ready  (status: "ready")
curl https://app.socialanalyze.org/api/v1/media/MEDIA_ID -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET"

# 4) Schedule to the suitable accounts
curl -X POST https://app.socialanalyze.org/api/v1/publications -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: launch-2026-10-10" \
  -d '{
    "text": "The new release is live! 🚀",
    "media": ["MEDIA_ID"],
    "targets": "auto",
    "local_time": "2026-10-10T09:00",
    "external_ref": "cms-post-4821"
  }'

Successful response (201):

JSON
{
  "data": {
    "id": "7ed788c0-878c-4c8e-a46e-a83621a55521",
    "status": "scheduled",
    "text": "The new release is live! 🚀",
    "media": [{ "id": "588ff7a4-…", "kind": "video", "mime": "video/mp4", "size": 18349211, "name": "launch.mp4", "url": "https://app.socialanalyze.org/api/social/media/94e3….mp4" }],
    "scheduled_at": "2026-10-10T06:00:00.000Z",
    "published_at": null,
    "external_ref": "cms-post-4821",
    "source": "api",
    "targets": [
      { "account_id": "fb17aa48-…", "platform": "instagram", "text": null, "options": {}, "result": null },
      { "account_id": "4f030b35-…", "platform": "tiktok",    "text": null, "options": {}, "result": null }
    ]
  },
  "skipped": [
    { "account_id": "9c1d…", "platform": "youtube", "reason": "YouTube: video başlığı gerekli." }
  ]
}
Getting started

#Authentication

Every request carries two headers: the public key identifier (sak_…) and the secret (sas_…). The secret must never appear in URLs, logs or client-side code. All requests use HTTPS.

cURL
curl https://app.socialanalyze.org/api/v1/me \
  -H "X-API-Key: sak_5a2ebf67ddb00876af15202e" \
  -H "X-API-Secret: sas_••••••••••••••••••••••••••••••••"

# Alternative: HTTP Basic (key:secret)
curl -u "$SA_KEY:$SA_SECRET" https://app.socialanalyze.org/api/v1/me
  • You see the secret only at creation/rotation; we store only its SHA-256 hash, so rotate it if lost.
  • A wrong key and a wrong secret return the same response (401 invalid_credentials); key existence is not leaked.
  • Rotating invalidates the old secret immediately. For a zero-downtime switch, create a second key, move your system over, then revoke the old one.
  • If you set an IP allowlist, only requests from those IPs/CIDRs are accepted (403 ip_not_allowed).
Getting started

#Keys & permissions

Keys are managed in the panel under Brand settings → API (account owner only). A brand can have up to 10 active keys, each with its own platforms, permissions and limits.

Permission (scope)What it allows
accounts:readList allowed accounts; query suitability via /compatibility.
media:writeUpload, import, list and delete media.
publications:readRead publications created through the API and their results.
publications:writeCreate drafts/schedules, edit and cancel.
publications:publishPublish immediately (mode:"publish" and /publish).
Permission presets
The panel offers three presets: Scheduling (no immediate publishing — ideal for review workflows), Full access and Read-only (for reporting dashboards). A key without the publish permission can never publish anything immediately.

Platform access is per key: if a key is allowed only Instagram and TikTok, targeting any other platform returns 403 platform_not_allowed. If a new account is later connected to the brand, the key sees it automatically if its platform is allowed.

Concepts

#Conventions

TopicRule
Base URLhttps://app.socialanalyze.org/api/v1
FormatRequests and responses are JSON, field names snake_case, dates ISO 8601 (UTC).
Success{ "data": …, "meta"?: …, "skipped"?: … }
Error{ "error": { "code", "message", "details"? } }
Rate limitPer-key per-minute limit (default 60, 1–1200 in the panel). Every response carries X-RateLimit-Limit / -Remaining / -Reset; exceeding returns 429 + Retry-After.
PaginationLists take limit (≤100) and offset; meta.total returns the total.
VersioningThe path is versioned with /v1; backwards-compatible additions (new fields/endpoints) do not bump the version, so ignore unknown fields.
Body sizeJSON bodies up to 1 MB; media goes through dedicated endpoints.
Unknown fieldsUnknown fields in requests return 400 (to catch typos).
Concepts

#Publication lifecycle

draft ──schedule──▶ scheduled ──(time reached)──▶ publishing ──▶ published
  ▲                      │  ▲                           │  └──▶ partial   (some targets published)
  └────unschedule────────┘  └─────── edit ──────────────┘  └──▶ failed    (none published)
                           failed / partial ──retry (publish / reschedule)──▶ publishing
StatusMeaningVia API
draftDraft; no time set.Edit, schedule, publish, delete
scheduledWaiting for its time.Edit, reschedule, unschedule, publish, cancel
publishingBeing sent to targets (video uploads can take minutes).Read-only
publishedPublished on all targets (or processing on the platform).Read-only; cannot be deleted
partialSome targets published, others failed.Retry failed ones via /publish
failedCould not be published anywhere.Edit and reschedule / publish, delete

Per-target results are always in targets[].result: status (published | processing | failed), external_id, permalink and the error message. A target that is already published is never sent again on retry.

Concepts

#Targets & compatibility

The same content does not suit every platform: Instagram and TikTok need media, YouTube accepts only video and needs a title, and text limits differ. The API applies these rules for you and answers “which platforms does it suit?” in three ways:

targetsBehaviour
"auto"Default. Every allowed account is tried; unsuitable ones are skipped and returned in skipped with the reason. If none suit, 422 no_eligible_targets.
["instagram","tiktok"]Platform name → all connected accounts on it. If unsuitable, 422 validation_failed (skipped with skip_invalid:true).
[{"account_id":"…","text":"…","options":{…}}]Per account: target-specific text and platform options.

To ask in advance use POST /compatibility — you can even ask before uploading, with just the kinds (media_kinds).

Concepts

#Time & timezones

You can give the time in two ways; do not send both:

FieldExampleNote
scheduled_at2026-10-10T09:00:00+03:00ISO 8601 with offset (Z or ±hh:mm) required.
local_time2026-10-10T09:00Local time in the brand's timezone (GET /me → brand.timezone). We handle DST transitions for you.
  • The time must be at least 1 minute and at most 1 year ahead.
  • The scheduler runs every minute; a post goes out in its scheduled minute.
  • scheduled_at: null (in PATCH) removes the schedule and returns the post to draft.
Concepts

#Idempotency & external_ref

To safely retry after a network error, add a unique Idempotency-Key header to POST /publications. When the same key + Idempotency-Key arrives again no new post is created; the original is returned with 200 and an Idempotent-Replayed: true header.

Store your own system's identifier in external_ref (≤120 chars) and find it again with GET /publications?external_ref=…. metadata is a free-form JSON object and comes back unchanged in webhooks.

Reference

#Endpoints

GET/me

Key & brand info

Returns the key's permissions, allowed platforms, limits and the brand's timezone. Ideal to verify connectivity.

JSON
{
  "data": {
    "key": {
      "key_id": "sak_5a2e…",
      "name": "CMS otomasyonu",
      "scopes": ["accounts:read", "media:write", "publications:read", "publications:write"],
      "platforms": ["instagram", "tiktok"],
      "expires_at": null,
      "rate_limit_per_min": 60,
      "daily_publication_limit": 100,
      "ip_restricted": false,
      "webhook_configured": true,
      "webhook_events": [],
      "created_at": "2026-10-02T11:10:09.784Z"
    },
    "brand": { "id": "b193a53c-…", "name": "Fonoloji", "timezone": "Europe/Istanbul", "local_date": "2026-10-02" }
  }
}
GET/accountsaccounts:read

List allowed accounts

Only connected, publishable accounts on the key's allowed platforms. status: active | error | expired — an expired account cannot publish and must be reconnected in the panel.

JSON
{
  "data": [
    {
      "id": "fb17aa48-a440-470b-af3d-873044f93d54",
      "platform": "instagram",
      "name": "Fonoloji",
      "username": "fonoloji",
      "avatar_url": "https://…",
      "profile_url": "https://www.instagram.com/fonoloji/",
      "status": "active",
      "capabilities": {
        "publish": true,
        "publish_kinds": ["image", "video", "carousel"],
        "requires_media": true,
        "max_text_length": 2200,
        "max_media": 10
      }
    }
  ]
}
POST/compatibilityaccounts:read

Which platforms does the content suit?

Without creating anything, returns each allowed account's suitability for text + media and, if unsuitable, why.

FieldTypeReq.Description
textstring○Post text (≤70,000).
mediauuid[]○Ready media ids.
media_kinds("image"|"video")[]○To query by kind before uploading (when media is omitted).
optionsobject○Platform name → flat options, e.g. {"youtube":{"title":"…"}}.
curl -X POST https://app.socialanalyze.org/api/v1/compatibility -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"text":"New video","media_kinds":["video"]}'

Media

Videos and images are first brought into Social Analyze, then attached to a post by media id. There are two ways: (1) import from a public URL (easiest), (2) upload in 5 MB chunks. Supported: JPG, PNG, WEBP (≤30 MB), GIF (≤15 MB), MP4, MOV, WEBM (≤1 GB). File contents are verified against the declared type; mismatches are rejected.

POST/media/importmedia:write

Import from a URL

Downloads in the background (202). The source must be a public address returning Content-Length; internal and private IPs are blocked. Track progress with GET /media/{id}: uploading → ready (or failed + error).

FieldTypeReq.Description
urlstring (url)●http(s) address to download (up to 3 redirects followed).
namestring○File name; derived from the URL if omitted.
curl -X POST https://app.socialanalyze.org/api/v1/media/import -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" -H "Content-Type: application/json" \
  -d '{"url":"https://cdn.example.com/launch.mp4"}'
POST/media/uploadsmedia:write

Chunked upload

Start the upload, PUT each 5 MB chunk in order, then complete it. A chunk that arrives again (retry) is silently ignored.

FieldTypeReq.Description
namestring●File name.
mimestring●image/jpeg · image/png · image/webp · image/gif · video/mp4 · video/quicktime · video/webm
sizeinteger●Total bytes.
# 1) Start
curl -X POST https://app.socialanalyze.org/api/v1/media/uploads -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" -H "Content-Type: application/json" \
  -d '{"name":"launch.mp4","mime":"video/mp4","size":18349211}'
# → { "data": { "id": "…", "chunk_size": 5242880, "chunk_count": 4, "status": "uploading" } }

# 2) Each chunk (index starts at 0)
curl -X PUT "https://app.socialanalyze.org/api/v1/media/uploads/MEDIA_ID/chunk?index=0" -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" \
  -H "Content-Type: application/octet-stream" --data-binary @chunk0.bin

# 3) Complete
curl -X POST https://app.socialanalyze.org/api/v1/media/uploads/MEDIA_ID/complete -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET"
Storage quota is 5 GB per account and at most 20 uploads may be open at once. Abandoned uploads are cleaned up automatically.
GET/media · /media/{id} · DELETE /media/{id}media:write

Media status, list, delete

GET /media (status=ready|uploading, limit ≤100) lists recent media of the key's brand. GET /media/{id} returns one item's status; if a URL import failed it returns status: "failed" and an error. DELETE /media/{id} removes the file unless it is used by a draft/scheduled post (otherwise 409 media_in_use).

POST/publicationspublications:write

Create a publication (draft / schedule / publish)

If mode is omitted: schedule when a time is given, otherwise draft. mode:"publish" publishes at once and requires publications:publish; it cannot be combined with a time.

FieldTypeReq.Description
textstring○Shared text (≤70,000). Required if there is no media.
mediauuid[]○Ready media ids (max 35, order kept).
targets"auto" | (string|object)[]○Default "auto". See Targets & compatibility.
platform_optionsobject○Platform name → flat options; default for all targets on that platform.
scheduled_at / local_timestring○Time. See Time & timezones.
mode"draft"|"schedule"|"publish"○Kind of action.
skip_invalidboolean○true: skip unsuitable targets instead of failing (already the case for auto).
validate_onlyboolean○true: validate without creating anything (200).
external_refstring○Your own system's identifier (≤120).
metadataobject○Free-form JSON; returned in webhooks.

Example 1 — schedule a video to every suitable platform:

JSON
{
  "text": "The new release is live! 🚀",
  "media": ["588ff7a4-0c36-44bf-b0b9-7f942ed9f76e"],
  "targets": "auto",
  "local_time": "2026-10-10T09:00",
  "external_ref": "cms-post-4821",
  "metadata": { "campaign": "launch" }
}

Example 2 — per-platform text and options:

JSON
{
  "text": "Compare funds with Fonoloji",
  "media": ["588ff7a4-…"],
  "targets": [
    { "platform": "youtube",   "text": "Fund comparison guide — description text…", "options": { "title": "How do I compare funds?", "privacy": "public", "tags": ["fon", "yatırım"] } },
    { "platform": "instagram", "options": { "type": "reel", "first_comment": "Link in bio" } },
    { "platform": "tiktok",    "options": { "title": "Compare funds", "privacy_level": "SELF_ONLY", "disable_duet": true } }
  ],
  "scheduled_at": "2026-10-10T09:00:00+03:00"
}

Example 3 — validate only, do not create:

cURL
curl -X POST https://app.socialanalyze.org/api/v1/publications -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" -H "Content-Type: application/json" \
  -d '{"text":"deneme","media":["MEDIA_ID"],"targets":"auto","local_time":"2027-03-01T10:00","validate_only":true}'
# → { "data": { "valid": true, "mode": "schedule", "scheduled_at": "2027-03-01T07:00:00.000Z",
#               "targets": [ { "account_id": "…", "platform": "instagram" }, … ] }, "skipped": [] }

Response: 201 (created) or on an idempotent replay / validate_only 200. Possible errors: 400 invalid request, 403 permission/plan, 422 validation (details.targets gives per-account reasons), 429 daily limit.

GET/publicationspublications:read

List / get publications

Only publications created through the API (posts created by hand in the panel are not reachable via the API). GET /publications/{id} returns a single publication with per-target results.

FieldTypeReq.Description
statusstring○Comma separated: draft,scheduled,publishing,published,partial,failed
platformstring○Having this platform among targets.
external_refstring○Exact match.
from / toISO date○scheduled_at range.
sort"created_at"|"scheduled_at"○Default created_at (newest first); scheduled_at ascending.
limit / offsetinteger○limit defaults to 25, max 100.
cURL
# Everything scheduled next week
curl "https://app.socialanalyze.org/api/v1/publications?status=scheduled&from=2026-10-12T00:00:00Z&to=2026-10-19T00:00:00Z&sort=scheduled_at" -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET"
PATCH/publications/{id}publications:write

Edit a scheduled publication

Only draft, scheduled and failed publications can be edited (otherwise 409 not_editable). Omitted fields are kept. Send only what changes:

cURL
# Change the time and text
curl -X PATCH https://app.socialanalyze.org/api/v1/publications/PUB_ID -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" -H "Content-Type: application/json" \
  -d '{"text":"Updated text","local_time":"2026-10-11T18:30"}'

# Remove the schedule (back to draft)
curl -X PATCH https://app.socialanalyze.org/api/v1/publications/PUB_ID -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" -H "Content-Type: application/json" -d '{"scheduled_at":null}'

# Change targets: Instagram only
curl -X PATCH https://app.socialanalyze.org/api/v1/publications/PUB_ID -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" -H "Content-Type: application/json" -d '{"targets":["instagram"]}'

# Retry a failed post with a new time
curl -X PATCH https://app.socialanalyze.org/api/v1/publications/PUB_ID -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET" -H "Content-Type: application/json" -d '{"mode":"schedule","local_time":"2026-10-12T09:00"}'

Fields are the same as POST. After an edit the publication fires a publication.updated webhook event.

POST/publications/{id}/publishpublications:publish

Publish now / retry

Immediately publishes a draft, scheduled, failed or partial post (the schedule is cleared). Targets already published are not sent again. If it is already publishing/published, 409 already_published.

cURL
curl -X POST https://app.socialanalyze.org/api/v1/publications/PUB_ID/publish -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET"
DELETE/publications/{id}publications:write

Cancel / delete

Deletes a draft, scheduled or failed post (a publication.cancelled event is sent). Published (published/partial) posts cannot be deleted (409 already_published) and the post on the platform is untouched; a publishing post returns 409 publishing.

cURL
curl -X DELETE https://app.socialanalyze.org/api/v1/publications/PUB_ID -H "X-API-Key: $SA_KEY" -H "X-API-Secret: $SA_SECRET"
# → { "data": { "id": "…", "deleted": true } }
Reference

#Platform guide

Rules follow each platform's official API and can change; for a definitive answer use /accounts → capabilities and /compatibility.

PlatformContentText limitMediaNotes
FacebookText, link, image, video63.206≤10 images or 1 videoImages and video cannot be mixed.
InstagramImage, video, carousel, Reels, story2.200Media required; carousel ≤10Reels need exactly 1 video, stories 1 image/video.
YouTubeVideo only5.0001 videoTitle required (options.title or text). Shorts ≤60 s.
LinkedInText, image, video, carousel3.000≤20 images or 1 videoImages and video cannot be mixed.
TikTokVideo or photos2.2001 video or ≤35 photosUntil the app review is complete posts are published as private (SELF_ONLY). Photos must be JPEG/WebP.

Platform options

Options are flat snake_case: options: { … } on a target or, in bulk, platform_options: { platform: { … } }. An unknown key returns 400 invalid_options.

PlatformOptionType / valuesDescription
facebooklinkstring (url)Adds a link card to the post.
instagramtypefeed | reel | storyPost format (a single video defaults to Reels).
instagramshare_to_feedbooleanAlso show the Reel in the feed.
instagramfirst_commentstring ≤2200First comment after publishing (e.g. links).
youtubetitlestring ≤100Video title (required; text is used if omitted).
youtubeprivacypublic | unlisted | privateVisibility.
youtubetags / category_id / made_for_kidsstring[] / string / booleanTags, category, made-for-kids flag.
linkedinvisibilityPUBLIC | CONNECTIONSWho can see it.
tiktokprivacy_levelstringOne of the options the account allows; SELF_ONLY otherwise.
tiktoktitlestring ≤150Video title.
tiktokdisable_comment / disable_duet / disable_stitchbooleanDisable interactions.
tiktokbrand_content / brand_organicbooleanCommercial content disclosure.
Reference

#Webhooks

Instead of polling, let us tell you the result. When you set an https webhook URL on a key, a signed POST is sent whenever the status of a publication created with that key changes. The URL must be public (internal/private IPs are blocked).

EventWhen
publication.publishedPublished on every target.
publication.partialSome published, others failed.
publication.failedCould not be published anywhere.
publication.updatedEdited from the panel or the API.
publication.cancelledCancelled / deleted.

Request headers and body:

JSON
POST /your/webhook  HTTP/1.1
Content-Type: application/json
User-Agent: SocialAnalyze-Webhooks/1.0
X-SocialAnalyze-Event: publication.published
X-SocialAnalyze-Delivery: 2b4f9e6e-…
X-SocialAnalyze-Timestamp: 1790939393
X-SocialAnalyze-Signature: sha256=38877139…

{
  "id": "evt_2b4f9e6e…",
  "type": "publication.published",
  "created_at": "2026-10-10T06:00:07.214Z",
  "data": { …publication object (same as GET /publications/{id})… }
}

Verifying the signature

Signature = HMAC-SHA256(webhook_secret, `${timestamp}.${raw_body}`) (hex) with a “sha256=” prefix. Use the raw body (before parsing), check the timestamp is no older than 5 minutes and compare in constant time. You can view the webhook secret by editing the key in the panel.

import crypto from "node:crypto";

// Express: app.post("/hook", express.raw({ type: "application/json" }), handler)
function verify(req, secret) {
  const ts = req.header("X-SocialAnalyze-Timestamp");
  const sig = req.header("X-SocialAnalyze-Signature") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay koruması
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.${req.body}`).digest("hex");
  const a = Buffer.from(sig), b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Delivery & retries

  • Return 2xx for success. Respond within 10 seconds; push heavy work to the background.
  • A failed delivery is retried after 1 min, 5 min, 30 min, 2 h and 6 h (6 attempts in total), then marked failed. You can retry manually from the key's log in the panel.
  • The same event may arrive more than once (at-least-once): dedupe using the id field.
  • Order is not guaranteed; rely on data.status and updated_at in the event.
  • Use “Send test” in the panel to deliver a ping event.
Reference

#Error codes

Every error has the same shape. Branch on the stable error.code, not on the HTTP status or message.

JSON
{
  "error": {
    "code": "validation_failed",
    "message": "Instagram: Reels için tek video seçin.",
    "details": { "targets": { "fb17aa48-a440-470b-af3d-873044f93d54": "Instagram: Reels için tek video seçin." } }
  }
}
HTTPcodeMeaning / fix
401missing_credentialsX-API-Key / X-API-Secret headers missing.
401invalid_credentialsWrong key or secret.
401key_revoked · key_expiredKey revoked / expired; create a new key in the panel.
403ip_not_allowedRequest came from outside the IP allowlist.
403feature_disabled · account_disabledThe API is not in your account's plan / account disabled.
403insufficient_scopeThe key lacks permission for this action (details.required).
403platform_not_allowed · account_not_allowedPlatform/account is outside this key's permissions.
403plan_limitYour plan's monthly post quota is used up (details.max/used).
400invalid_request · invalid_options · invalid_targetBody/field error; details.issues says which field.
400unknown_platform · schedule_required · publish_with_schedule · ambiguous_schedule · invalid_local_timeBad target or time parameter.
404publication_not_found · media_not_found · not_foundRecord missing or belongs to another brand/source.
409not_editable · publishing · already_publishedThe status does not allow this action (see lifecycle).
409media_in_use · chunk_in_progressMedia in use / previous chunk still writing.
422validation_failedContent unsuitable for a target (details.targets per-account reason).
422no_eligible_targets · no_accounts · no_account_for_platformNo suitable/connected target (see skipped).
422media_not_readyMedia missing or not ready; wait for status: ready.
422source_unreachable · content_length_required · unsupported_typeImport source unreachable / no Content-Length / unsupported type.
400/413/429media_error · unsafe_urlMedia rule (type, size, quota) or unsafe URL.
429rate_limited · daily_limit_reachedPer-minute / daily limit. Wait Retry-After.
429api_quota_exceededYour plan's monthly API post/video allowance is used up (details.limit = apiPublications | apiVideos). It resets on the 1st.
413 · 415payload_too_large · unsupported_media_typeBody too large / wrong Content-Type.
500internal_errorOur side; retry safely with an Idempotency-Key.
Reference

#Limits

ItemLimit
Request rate60/min per key (default; 1–1200 in the panel)
Daily posts100 in the last 24 h per key (default; 1–5000) — within your plan quota
Monthly API allowanceN posts and M videos a month depending on your plan (table below); exceeding returns 429 api_quota_exceeded. GET /me shows your allowance and usage this month.
Monthly plan quotaThe monthly scheduled/published post allowance of your workspace plan, shared with the panel (API posts count too)
Scheduling windowNow +1 minute … +1 year
Targets per postUp to 30
Media per postUp to 35 (platform limits also apply)
File sizeImage ≤30 MB (GIF ≤15 MB), video ≤1 GB
Storage0.5–500 GB depending on plan (workspace total), 20 open uploads at once. Media uploaded through the API is removed automatically 14 days after its post is published.
Keys per brand2–20 active keys per brand depending on plan
Log retentionRequest logs and webhook deliveries kept 30 days
Reference

#Plans & API allowance

The external API is metered like a separate service: it has its own monthly post and video allowance, key count and request rate. It is included from Studio up; on a lower plan it can be added as a separate service with an allowance sized to your needs.

PlanPosts / moVideos / moKeys / brandRateStorageWebhook
Spark————0.5 GB—
Pulse————2 GB—
Studio15030230/min10 GB✓
Atlas1,0002505120/min40 GB✓
Summit5,0001,50010300/min150 GB✓
Zenith25,0006,00020600/min500 GB✓
How are “posts” and “videos” counted?
Every non-draft API post counts as one, no matter how many platforms it goes to. Videos are the video files in those posts. Drafts and validate_only requests do not count. We limit volume, not price: pricing is set by quote.
Reference

#Best practices

  • Put an Idempotency-Key on every POST. Derive it from your content id (e.g. cms-post-4821-v3); retry with the same key on 429/5xx or timeouts.
  • Honour Retry-After on 429 and back off exponentially. Spread bulk imports below the per-minute limit.
  • Get results by webhook. As a safety net, reconcile occasionally with GET /publications?status=publishing,partial,failed.
  • Validate first. In approval flows show users where a post will go (and why not) using validate_only:true.
  • For big videos: media first, post second. Finish import/upload well before the scheduled time; only ready media can be attached.
  • Surface platform errors. On partial/failed, targets[].result.error carries the platform's real error; fix and retry via PATCH + /publish.
  • Grant least privilege. Use the “Scheduling” preset (no immediate publishing) for review workflows, a separate key per integration and an IP allowlist where possible.
  • One key set per brand. A key reaches a single brand; in multi-brand systems keep a brand → key mapping on your side.
Reference

#Changelog

DateVersionChange
2026-10-02v1.0First release: per-brand key + secret, platform and action permissions, accounts/compatibility, media (chunked upload + URL import), create/edit/cancel/publish, Idempotency-Key, signed webhooks, OpenAPI 3.1.
Need help?
If you get stuck during integration or miss an endpoint, write to us; sharing one row of your request log (time + path + status) is enough. Never share your API key or secret. Contact