Social360

API reference

Publish from your own platform

Create posts, schedule them, and follow them out to every channel — from your backend, with a key. This page is generated from the same schema the server enforces, so it cannot describe an endpoint that does not exist.

Quickstart

Issue a key under Settings → API keys, then publish something. Three calls: find your channels, create the post, watch it land.

export S360_KEY="bqp_…"

# 1. Which channels can I publish to?
curl "/api/accounts/" \
  -H "Authorization: Bearer $S360_KEY"

# 2. Post to two of them, right now.
curl -X POST "/api/posts/" \
  -H "Authorization: Bearer $S360_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Autumn launch",
        "body": "The new range is live. https://example.com/autumn",
        "targets": [3, 7],
        "intent": "now"
      }'

# 3. Did it go out? Each target reports for itself.
curl "/api/posts/42/" \
  -H "Authorization: Bearer $S360_KEY" | jq '.targets[] | {channel: .account.label, status, external_url, last_error}'

With an image

Attachments go as multipart/form-data. Repeat the media field once per file — order is kept, and the first is the cover. Instagram, TikTok and YouTube refuse a post without one.

curl -X POST "/api/posts/" \
  -H "Authorization: Bearer $S360_KEY" \
  -F "title=Autumn launch" \
  -F "body=The new range is live." \
  -F "targets=3" -F "targets=7" \
  -F "intent=now" \
  -F "media=@hero.jpg" \
  -F "media=@detail.jpg"

Different wording per channel

overrides maps a channel id to the caption that channel should get. Anything left out uses the shared body, so an edit to the post still reaches it.

curl -X POST "/api/posts/" \
  -H "Authorization: Bearer $S360_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Autumn launch",
        "body": "The new range is live.",
        "targets": [3, 7],
        "overrides": { "3": "The new range is live 🍂 #autumn" },
        "scheduled_at": "2026-09-01T09:00:00Z"
      }'

Authentication

Every call carries a key in the Authorization header:

Authorization: Bearer bqp_<id>_<secret>
The secret is shown once.It is stored as a hash, so nothing — not this app, not an administrator — can read it back. Lose it and you revoke the key and issue another.

Server-to-server only

Nothing here sends CORS headers, so a browser on another origin cannot call this API. That is deliberate: a key in front-end JavaScript is a key you have published. Keep it on your backend, in an environment variable.

What a key cannot do

A key can never exceed the person who created it, and is shut out of three things regardless of scope: managing keys (a key that mints keys cannot be revoked meaningfully), changing its owner’s profile, and the AI trend endpoints, which spend a small shared model quota and stay in the browser.

Scopes

A key carries the scopes you pick when you issue it. Give an integration the narrowest set that does its job — a dashboard that only reads should not hold a key that can publish.

ScopeGrants

A call missing its scope answers 403 and names the one it wanted, so you never have to guess which to add.

Errors

Failures are JSON with a detail string. When several things are wrong at once — a post breaking three platform rules — a problems array lists all of them, so a fix does not have to be found one round-trip at a time.

{
  "detail": "Instagram: needs an attached image or video.",
  "problems": [
    "Instagram: needs an attached image or video.",
    "X: caption is 402 characters, limit is 280."
  ]
}
StatusMeaning
400The request is wrong. detail says how.
401No key, or a key that is unknown, revoked or expired.
403Valid key, missing scope — or an endpoint keys cannot reach.
404No such record.
409The record is in a state that forbids it — cancelling something already published.
429Too many requests. Nothing was changed.

Rate limits

120 requests per minute, counted per key rather than per account, so one runaway script cannot starve your other integrations. A 429 means wait and retry — nothing was changed, so a retry is safe.

Publishing is asynchronous

POST /api/posts/ returns as soon as the post is stored, not when it is live. Delivery happens in a worker, one channel at a time, each with its own retries — a platform being slow or down must not hold your request open.

Poll GET /api/posts/{id}/ and read each entry in targets. A target moves pending → sending → sent, and a failure leaves last_error populated with what the platform said. The post’s own status settles to done, partial or failed once every target has finished.

A partial is not a failure to retry wholesale.Re-posting would publish twice on the channels that worked. Retry the individual target instead — the API refuses a retry on one that already sent.

Loading the schema…