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
- Keys are
ty_agent_…orty_human_…, and test keys arety_agent_test_…orty_human_test_…. Every key has scopes (read,draft,schedule,publish) and can be limited to some accounts. The autonomy dial lists what each scope allows, and test mode covers test keys. - Modes don’t mix. Test keys see only test objects, and live keys only live ones. An id from the other mode returns
not_found. X-Turboyap-Client(optional) names the caller, for examplemy-app/1.0. Turboyap records it on each post the call creates (created_by.client) and in the audit log. It can be up to 100 characters; a longer value getsinvalid_request. The MCP server sends it for you, and the SDK sends it when you passclient.
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 |
- A new post returns
201with{ "post", "warnings", "replayed": false }. - Sending the same
idempotency_keyagain returns200with the original post and"replayed": true, even if the body changed. Idempotency keys are unique per workspace and mode. - A dry run returns
200with{ "dry_run": true, "ok", "schedule_at", "targets", "errors", "warnings" }and saves nothing. It still checks the key’s scopes and account allowlist, and each target’sstatus_on_createshows whether the post would wait for approval.
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.
This page as markdown: /docs/api.md. All docs for LLMs: /llms.txt and /llms-full.txt.