turboyap

REST API reference

Endpoints, authentication, scopes, objects, statuses and error format for the Turboyap API. The MCP server and the SDK use this same API, and so will the dashboard.

Early access: the API is built and being tested, but api.turboyap.com isn’t public yet. TikTok and Instagram open to early-access users as each platform approves Turboyap. Bluesky, Telegram and Discord are in beta: early-access users can connect them and post today, but they haven’t been proven with a real production post yet. Test mode needs no platform approval.

Base URL: https://api.turboyap.com. Authenticate with Authorization: Bearer <key>. Bodies are JSON unless noted.

Keys and headers

Endpoints

Method Path What it does Needs
GET /health { "ok": true } while the API and its database answer; 503 when the database doesn’t No key
GET /v1/platform-rules?platform=&post_type= Platform limits No key
GET /v1/me Workspace, kill-switch state, and the calling key Any key
GET /v1/keys/me The calling key: type, mode, scopes, account allowlist and a can summary Any key
GET /v1/accounts Connected accounts (only the allowed ones, for a key with an allowlist) read
GET /v1/connect_methods How each platform connects on this server: oauth (a connect link) or credentials (POST /v1/accounts), with the steps and fields Any key
POST /v1/connect_links { "platform" }: a 7-day link to a page where the account’s owner connects it, on the platform’s own screen (TikTok, Instagram, Discord) or with a form (Bluesky, Telegram) Live key, draft
POST /v1/accounts Connect with credentials: { "platform": "bluesky", "handle", "app_password", "service"? } or { "platform": "telegram", "chat" } Human live key, draft
POST /v1/accounts/test Create a simulated account on any platform in early access or beta: { "platform", "handle"? } Test key, draft
PATCH /v1/accounts/:id { "autonomy_level": 1 | 2 | 3 }. Level 2 behaves like level 1 until guardrails ship Human key, publish
DELETE /v1/accounts/:id Disconnect: deletes the stored tokens, revokes them on the platform where it can (TikTok’s token, Discord’s webhook), and cancels the account’s queued posts Human key, draft
POST /v1/media Upload raw bytes; Content-Type is the file type, optional X-Filename draft
GET /v1/media/:id Media details read
POST /v1/posts Create, schedule, or preview (dry_run: true) draft, plus schedule to set a time and publish for "now"
GET /v1/posts?status=&account_id=&from=&to=&limit= List posts read
GET /v1/posts/:id One post with per-account status read
PATCH /v1/posts/:id { "schedule_at"?, "caption"? } draft, plus schedule to set a time or edit a queued post, and publish for "now"
POST /v1/posts/:id/approve Approve pending targets; optional { "account_ids": […] } Human key, schedule
POST /v1/posts/:id/reject Reject pending targets; optional reason and account_ids Human key, schedule
POST /v1/posts/:id/cancel Cancel targets that haven’t started publishing draft
POST /v1/workspace/pause Kill switch: stop every publish in the workspace Human key
POST /v1/workspace/resume Start publishing again Human key, publish
POST /v1/webhook_endpoints { "url", "enabled_events"? }; the response includes the whsec_ signing secret, once Human key, read
GET /v1/webhook_endpoints List endpoints, without secrets Human key
DELETE /v1/webhook_endpoints/:id Delete an endpoint Human key

A key with an account allowlist only sees the posts that go to its accounts, and gets account_not_allowed if it targets any other account. Events and signatures are in webhooks.

Create post

Field Type Notes
type "text" | "image" | "slideshow" "text" is for Bluesky, Telegram and Discord (beta). Video posts are coming soon
media_ids string[] In slide order. Text posts take none
caption string Default caption for all targets. For text posts, this is the post
link object { url, title?, description? }: a link card on Bluesky text posts, the link preview on Telegram text posts, an embed on Discord. Ignored with a warning elsewhere
alt_text object Alt text per media id, e.g. { "med_…": "A pint of ice cream next to its label" }. Sent where the platform supports it (Bluesky, Discord)
targets array { account_id, caption?, media_ids?, tiktok? } per account
schedule_at string ISO 8601 with timezone, or "now". Omit for a draft
fit object { overflow: "strict" | "truncate", aspect: "strict" | "pad" | "crop", pad_color: "auto" | "#rrggbb" }
dry_run boolean Return the plan, errors and warnings without creating
idempotency_key string Retries with the same key return the original post

Objects

Post

Field Notes
id post_…
type "text", "image" or "slideshow"
caption, media_ids The defaults each target starts from
source "human" or "agent"
created_by { "type", "key_id", "client" }: human or agent, the key that created the post, and the X-Turboyap-Client value (or null)
livemode false for test-mode posts, which publish to simulated platforms
idempotency_key The key you sent, or null
created_at ISO 8601
targets One per account

Target

Field Notes
id tgt_…
account_id, platform, handle The account it goes to
post_type What it posts as there; a one-slide slideshow goes to Instagram as an image
status, next_step See statuses
scheduled_at When it publishes, or null for a draft
caption, media_ids What this account gets
link The link card or preview this account shows, or null
platform_post_id, platform_url, published_at Set once it’s published
error { "code", "message", "fix" } when it failed
approved_by Key id of the human who approved it, or who last edited it while it was scheduled; null if no human has

Account

Field Notes
id acct_…
platform, handle, display_name Which account it is
livemode false for simulated test accounts
autonomy_level, autonomy 1, 2 or 3, and what that level means in words
status active, needs_reauth (a human must reconnect it) or disabled
status_reason { "code", "message", "at" } saying why the account isn’t active, or null
token_expires_at When the platform token expires. Turboyap refreshes it ahead of time. If a refresh fails or the platform rejects the token, the account becomes needs_reauth and account.reauth_required fires
next_step What a human has to do, when the account needs one; otherwise null
connected_at ISO 8601

Key

GET /v1/keys/me returns { "key": … }, and GET /v1/me returns the same key next to the workspace:

{
  "workspace": { "id": "ws_…", "name": "Your workspace", "paused": false },
  "key": {
    "id": "key_…",
    "name": "Claude Code",
    "prefix": "ty_agent_k3m9x",
    "type": "agent",
    "mode": "live",
    "livemode": true,
    "scopes": ["read", "draft", "schedule"],
    "account_ids": null,
    "can": { "read": true, "create_drafts": true, "schedule": true, "publish_now": false, "approve": false, "change_settings": false },
    "created_at": "2026-10-20T15:00:00.000Z"
  }
}

account_ids: null means every account in the workspace. can sums up what the key may do: publish_now needs draft, schedule and publish; approve needs a human key with schedule; change_settings is true for human keys (changing autonomy and resuming the workspace also need publish).

Statuses

Each account a post goes to (a target) moves through: draft → pending_approval → scheduled → publishing → processing → published, or sent_to_inbox (TikTok inbox mode, waiting for you to post in-app). It can also end as failed, rejected, or cancelled. Every target includes a plain-language next_step.

A target that failed with error.code unknown_outcome was cut off mid-publish, so Turboyap can’t tell whether it went out. Turboyap doesn’t publish it again: check the platform before you recreate it.

Errors

{ "error": { "code": "aspect_ratio_out_of_range", "message": "…", "field": "media_ids", "fix": "Set fit.aspect to \"pad\"…", "issues": [] } }

fix is always a concrete change. Multi-problem requests list each problem in issues, each with its own fix.

Keys and permissions have their own errors:

Code HTTP When
unauthorized 401 The key is missing, invalid or revoked
insufficient_scope 403 The key lacks a scope the call needs. field names the request field that needs it, such as schedule_at, or scopes when no single field does
account_not_allowed 403 The request touches an account outside the key’s account allowlist. field names where, such as targets[1].account_id
forbidden_scope 403 An agent key tried something only a human key can do: approve, reject, change autonomy, pause or resume the workspace, or manage webhooks
test_mode_only 403 POST /v1/accounts/test with a live key
not_found 404 No such object for this key, including objects in the other mode
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key lacks the \"publish\" scope, which is needed to publish immediately. It has: read, draft, schedule.",
    "field": "schedule_at",
    "fix": "This key can schedule but not publish immediately. Set schedule_at to a future ISO 8601 time instead of \"now\", or use a key with the \"publish\" scope."
  }
}

Other errors use 409 (conflict, invalid_status_transition), 500 (internal), 502 (platform_error), or 422 for everything else, including validation and platform-rule errors such as too_many_items.

Health check

GET /health needs no key. It returns 200 with { "ok": true } when the API can reach its database, and 503 with "ok": false and an error when it can’t. Point uptime checks at it.