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>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.
| Scope | Grants |
|---|
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."
]
}| Status | Meaning |
|---|---|
400 | The request is wrong. detail says how. |
401 | No key, or a key that is unknown, revoked or expired. |
403 | Valid key, missing scope — or an endpoint keys cannot reach. |
404 | No such record. |
409 | The record is in a state that forbids it — cancelling something already published. |
429 | Too 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.
Loading the schema…