API Reference
REST API for the AI SNS · Social Studio publishing platform. All responses are JSON unless noted. This page is generated from the server source and reflects the deployed build.
Auth & conventions
How authentication, roles, plan limits and credits work across every route.
Registering creates a user and a workspace — you become the owner — and returns a token together with the workspace id. Logging in returns the same pair. The token is a self-signed HS256 JWT valid for 30 days. There is no refresh endpoint, so sign in again once it expires.
POST /api/auth/register {email, password, workspace?} → {token, workspace_id}
POST /api/auth/login {email, password, workspace_id?} → {token, workspace_id}
JWT payload {uid, ws, email}
curl -H "Authorization: Bearer <token>" \
https://<host>/api/posts
Every route below /api passes through requireAuth, which resolves your workspace, user id and role. Authentication is resolved in this order:
- a legacy API_TOKEN environment match, treated as the owner of workspace 1;
- a workspace API key (sk_studio_…, minted in Settings → API keys), acting as an editor of the one workspace it belongs to;
- a valid JWT, with workspace membership checked;
- open development or demo mode.
AUTH routes accept any valid session. WRITE routes additionally require write permission, which only the owner and editor roles hold — a viewer or client is refused with 403 and a read-only role error. ADMIN routes need the x-admin-secret header and CRON routes need x-cron-secret. PUBLIC routes need no bearer token at all, because the token or id in the path is itself the credential.
OWNER routes are the seat and team decisions, and a session that is not a workspace owner is refused with 403 whatever its write permission.
Some writes check the workspace plan limit for posts, AI units and connected accounts. When a limit is exceeded the route responds:
402 { "error":"limit_reached", "kind":"posts|ai_units|accounts", "limit":30, "upgrade":true }
Monthly allowances per plan:
free posts 30 ai_units 50 accounts 1 starter posts 200 ai_units 500 accounts 3 pro posts 1000 ai_units 1500 accounts 5 business posts ∞ ai_units 3000 accounts 15
Credits are a separate ledger. AI actions spend them according to a cost map: a draft or a set of hashtags costs one credit, an image or an image reference costs twelve, a frontier video costs 120 and an audio clip costs eight. Credits are soft by default — usage is tracked but never blocked — unless credit enforcement is switched on. Only the influencer analysis route hard-checks credits and can refuse the request.
lib/billing.js · lib/plans.js ENFORCE_CREDITS=1 → credits become blocking POST /api/influencers/analyze → 402 out_of_credits
AI Studio job lifecycle
How a generation moves from submit to a saved file, and when credits are charged, skipped or refunded.
A generation is submitted once, polled until it reaches a terminal status, and then copied into the media library so it can be attached to a post before the platform's signed url expires.
GET /api/hf/catalog → pick a model id + its declared settings and media roles
POST /api/hf/generate → 200 {job:{id, status:"queued", ...}, credits}
GET /api/hf/jobs/:id (repeat) → 200 {job:{status, results:[...], error}}
non-terminal: keep polling (the studio UI polls every 2.5 s)
terminal: completed | failed | nsfw
POST /api/hf/jobs/:id/save → 200 {ok:true, media:{id, key, url}} (completed jobs only)
POST /api/hf/jobs/:id/cancel → stops a job that has not finished; it ends as failed
{id, request_id, model, surface:"image"|"video"|"audio", prompt, status, error:<string|null>,
settings:{...as submitted}, media:{...as submitted}, results:[{type:"image"|"video"|"audio", url}],
created_at, updated_at}
// A finished job answers from the server's own row, so polling a completed job never calls the platform again.
// nsfw → error "The model's safety filter rejected this generation."
// failed→ error is the platform's message, or "The generation failed."
Credits are charged when the job is submitted, at a fixed price per surface, and the charge is skipped entirely when an active model pass covers the model.
hf_image 12 credits hf_video 120 credits hf_audio 8 credits (lib/billing.js COST)
pass covers the model when: scope == "all" | scope == model id | scope empty and surface matches
402 {error:"Not enough credits for this generation.", upgrade:true} only when ENFORCE_CREDITS=1 and no pass applies
A job that ends as failed or nsfw, or that is cancelled before it finishes, is refunded in full unless a pass made it free in the first place, so a pass-covered failure never mints credits.
Usage is metered on every generation whether or not it was billed, and the job is recorded before the charge is taken, so a client can never pay for a generation that has no row to poll.
API keys, MCP & CLI
How a client outside the browser authenticates, and how the MCP server and the command line map onto the routes above.
A key is minted in the studio under Settings → API keys and is shown exactly once; only a salted hash is stored, so it cannot be recovered or listed again.
It belongs to one workspace and acts there as an editor: it can generate, poll and read, but it cannot change billing, seats or keys, and the key routes refuse a caller that is itself a key.
A revoked key is refused as unknown, and a key whose workspace owner or minter is suspended is refused with 403, on the same check the JWT path makes on every request.
Authorization: Bearer sk_studio_xxxx → req.ws = the key's workspace, role editor, actor "api-key:<prefix>"
401 {error:"invalid api key"} unknown, malformed or revoked
403 {error:"account suspended"} the account behind it is suspended
The endpoint speaks the Model Context Protocol over streamable HTTP: one JSON-RPC 2.0 message per POST, answered as JSON, with notifications acknowledged by 202 and no server-initiated stream.
Every tool is a thin call into the same code the routes above run, with the workspace the key resolved to, so a generation made through MCP is priced, checked, charged and refunded exactly as one made in the studio.
initialize → {protocolVersion, capabilities:{tools:{}}, serverInfo, instructions}
tools/list → list_models · list_effects · quote_generation · generate_image · generate_video · job_status · account_balance
tools/call → {content:[{type:"text", text}], structuredContent, isError}
a refused call (no credits, other tenant's job, bad model) is a result with isError:true and the route's own message
-32700 Parse error -32600 Invalid Request -32601 Method not found -32602 Unknown tool (HTTP 400 for the first two)
GET / DELETE /mcp → 405
429 → more than 120 calls a minute on one key
The CLI is a single dependency-free file served by the studio; it reads the key from STUDIO_API_KEY or ~/.studio/config.json, quotes the price before a generation and asks unless --yes is given.
curl -fsSL https://studio.ai-sns.io/cli/studio.js -o /usr/local/bin/studio && chmod +x /usr/local/bin/studio studio login · studio models · studio effects · studio balance studio generate --model <id> --prompt "…" [--duration N] [--resolution R] [--aspect A] [--out file] [--yes] studio jobs <id> [--wait] [--out file] maps onto: POST /api/hf/quote → POST /api/hf/generate → GET /api/hf/jobs/:id (GET /api/hf/models, /api/hf/presets, /api/billing)