Run Studio·876 from your own code
Lock a character once, then generate scenes, clips, voice and Quick Shots that keep it on-model, and hear back through webhooks the moment they're done. The API runs on the same engine as the studio: same models, Studio Refine scoring, safety checks and Content Credentials, and the same credits and plan limits.
01Overview
- Base URL
https://app.thestudio876.com/api/v1. JSON in, JSON out:{ data }on success,{ error: { code, message } }on failure. - On the Advance and Studio plans. Everything you make through the API also appears in your studio projects.
- Generation is asynchronous: start a job, then poll it or receive a webhook. Credits are reserved when it starts and charged only for what is delivered.
- The full machine-readable description is at /api/v1/openapi.json (OpenAPI 3.1): import it into Postman, Insomnia or a client generator.
02Authentication
Create a key in the studio under Settings → Developer API. It is shown once; we keep only a fingerprint. Send it on every request:
Authorization: Bearer s876_…
- A key acts as you. Keep it on a server: never in a web page, an app or a public repository.
- Up to 10 active keys; revoking one takes effect immediately. If your plan changes to one without the API, keys stop working until it has it again.
03Quick start
export S876_KEY=s876_… # Settings → Developer API
# 1 · Who am I, and what's my balance?
curl https://app.thestudio876.com/api/v1/me -H "Authorization: Bearer $S876_KEY"
# 2 · Pick a character, price a run, start it
curl https://app.thestudio876.com/api/v1/characters -H "Authorization: Bearer $S876_KEY"
curl https://app.thestudio876.com/api/v1/quote -H "Authorization: Bearer $S876_KEY" \
-d '{"kind":"generate_image","preset":"balanced","takes":2}'
curl https://app.thestudio876.com/api/v1/jobs -H "Authorization: Bearer $S876_KEY" \
-d '{"kind":"generate_image","preset":"balanced",
"input":{"character_id":"<id>","scene":"A Kingston street at dusk, neon","takes":2}}'
# → 202 {"data":{"id":"<job id>","status":"queued","credits_reserved":16,…}}
# 3 · Wait (or use a webhook), then download
curl https://app.thestudio876.com/api/v1/jobs/<job id> -H "Authorization: Bearer $S876_KEY"
# → {"data":{"status":"succeeded","output":{"assets":[{"id":"<asset id>","rank":1,"score":0.93},…]}}}
curl https://app.thestudio876.com/api/v1/assets/<asset id> -H "Authorization: Bearer $S876_KEY"
# → {"data":{…,"url":"https://…","url_expires_in":3600}}04Jobs
POST /jobs with a kind, its input and a quality preset (economy, balanced or best, as your plan allows). Price it first with POST /quote: it returns the model and the credits, with the same formula the charge uses.
- Status goes
queued→running→succeeded|failed|cancelled. While running,progressis a percentage when the job reports one. outputnames the new assets; fetch each withGET /assets/{id}for its download link. A failed job refunds its reserve and carrieserror.- Jobs land in your most recently opened project unless you pass
project_id.
05Job kinds
extract_dna · Lock a character (Identity DNA)All API plans| input | type | notes |
|---|---|---|
| reference_path* | string | The `path` from POST /uploads (your own refs folder). |
| consent* | true | Your attestation that you have the rights to the image and the person's consent (Acceptable Use Policy). |
generate_image · Scene takesAll API plans| input | type | notes |
|---|---|---|
| character_id* | uuid | One of your characters (GET /characters). |
| scene* | string | The scene, 3–600 characters. |
| takes | integer | 1 to your plan's takes per run (default 1). |
| aspect | 1:1 | 4:5 | 3:4 | 16:9 | 9:16 | Default 1:1; the quote's spec.aspects lists what the model draws. |
| resolution | 1K | 2K | 4K | Default 1K; larger sizes by plan and model. |
generate_video · Storyboard clipAll API plans| input | type | notes |
|---|---|---|
| scene_asset_id* | uuid | A scene take of yours: frame 0. |
| action* | string | What happens, 3–600 characters. |
| seconds* | integer | One of the model's durations (POST /quote returns them), up to your plan's limit. |
| aspect* | string | One of the model's aspects, e.g. 16:9. |
| camera | string | Camera direction, e.g. slow push-in. |
| dialogue | string | A line the character speaks on camera (≤ 200; models with audio, e.g. Veo 3.1). |
| voice | string | How it sounds, e.g. warm, Jamaican (≤ 80). |
generate_sound · Voice, music or sound effectAll API plans| input | type | notes |
|---|---|---|
| sound_kind* | voice | music | sfx | What to make. |
| text* | string | voice: the script (up to 2,000 characters). music / sfx: a description, 3–600. |
| voice | string | voice: a voice id, e.g. warm-female, deep-male, smooth-male. |
| delivery | string | voice: how to read it (≤ 120), e.g. excited, whispered. |
| seconds | integer | music: 30, 60 or 120; sfx: 5, 10 or 20. |
| character_id | uuid | Optional: file it under one of your characters. |
quick_shot · Quick ShotAll API plans| input | type | notes |
|---|---|---|
| character_id* | uuid | One of your characters. |
| scene* | string | 3–600 characters. |
| action* | string | What happens in the clips, 3–600. |
| seconds* | integer | A duration the clip model offers (POST /quote with kind quick_shot). |
| aspect* | string | A shape both models make (the quote's aspects). |
| clips | integer | 1–4 (default 1). |
| camera | string | Camera direction. |
extend_shot · Shot ExtenderAll API plans| input | type | notes |
|---|---|---|
| source_asset_id* | uuid | A video of yours. |
| action* | string | What happens next, 3–600. |
| seconds* | integer | Length of the new segment (model durations); the total is capped by your plan. |
| bridge_at | number | Seconds into the source to continue from (default: the end). |
| camera | string | ≤ 120. |
| prompt | string | A full directed prompt (≤ 1,500), if you have one. |
| negative | string | What to avoid (≤ 300). |
| motion | integer | 1–10 (default 4). |
| seam_blend | number | 0.04–0.5 seconds (default 0.12). |
| audio | both | mute_new | mute | Default both. |
| reroll | boolean | Re-roll a drifted seam once (default true; charged only if it runs). |
analyze · Drift Diff (free)All API plans| input | type | notes |
|---|---|---|
| task* | drift_diff | Always drift_diff. |
| asset_id* | uuid | A scene take, clip or character take of yours. |
* required. Also accepted, with the studio's inputs (not documented here yet): character_take, motion_poster, render, talking_character. Account export and deletion are never available through the API.
06Files
- Downloads:
GET /assets/{id}returnsurl, a signed link valid for an hour. Add?download=1to have it save as a file. Fetch a fresh one whenever you need it. - Uploads (reference images for
extract_dna): askPOST /uploadsfor a one-time link, PUT the image to it, then start the job with the returnedpath.
# A reference image → a locked character
curl https://app.thestudio876.com/api/v1/uploads -H "Authorization: Bearer $S876_KEY" -d '{"mime":"image/png"}'
# → 201 {"data":{"path":"acct/…/refs/api-….png","upload_url":"https://…","method":"PUT",…}}
curl -X PUT "<upload_url>" -H "Content-Type: image/png" --data-binary @portrait.png
curl https://app.thestudio876.com/api/v1/jobs -H "Authorization: Bearer $S876_KEY" \
-d '{"kind":"extract_dna","input":{"reference_path":"<path>","consent":true}}'07Webhooks
Add up to 5 endpoints in Settings → Developer API or with POST /webhooks. We POST a signed event to each for the events it subscribes to: job.succeeded, job.failed (every generation, from the studio or the API) and credits.low (once, when your balance drops below 100). ping is sent by the test button.
POST /your/endpoint
Content-Type: application/json
Studio876-Event: job.succeeded
Studio876-Delivery: 7b0c…
Studio876-Signature: t=1793770000,v1=5f2a…
{"id":"7b0c…","type":"job.succeeded","created":1793770000,
"data":{"job":{"id":"…","kind":"generate_image","status":"succeeded","project_id":"…",
"output":{"assets":[{"id":"…","rank":1,"score":0.93}]},"credits_charged":16,"via_api":true,…}}}- Verify every request with the endpoint's secret (
whsec_…, shown once):v1is the hex HMAC-SHA256 of`${t}.${rawBody}`. Rejecttolder than 5 minutes. - Answer any 2xx within 10 seconds; do the work afterwards. Anything else is retried 8 times over about 2 days, so de-duplicate on
id(also inStudio876-Delivery). - An endpoint that fails 50 times in a row is paused until you resume it in Settings.
- Endpoints must be public https URLs on the default port; we never follow redirects.
import crypto from "node:crypto";
// rawBody: the exact request body string (before JSON.parse).
export function verifyStudio876(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds || !parts.v1) return false;
const want = Buffer.from(crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
const got = Buffer.from(parts.v1);
return want.length === got.length && crypto.timingSafeEqual(want, got);
}import hashlib, hmac, time
def verify_studio876(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance or "v1" not in parts:
return False
want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts["v1"])08Errors
| 401 | missing_api_key | No Authorization: Bearer header. |
| 401 | invalid_api_key | Unknown or revoked key. |
| 402 | insufficient_credits | The balance doesn't cover the quote. |
| 403 | api_not_in_plan | The account's plan has no API (Advance and Studio do). |
| 403 | not_in_plan / preset_not_in_plan / …_not_in_plan | A tool, quality preset, size or length the plan doesn't include. |
| 403 | account_suspended | The account is suspended. |
| 404 | not_found | Missing, or not yours. |
| 429 | rate_limited | Too many requests or jobs; back off and retry. |
| 429 | too_many_jobs | Your plan's concurrent jobs are all running. |
| 503 | no_model_available / model_unavailable | No model is serving that tool right now. |
| 400 | bad_* | A field failed validation; the message says which. |
The message is plain language you can show to a person; branch on code.
09Limits
| Requests | 120 a minute per key; 600 a minute per IP address |
| Paid jobs | 120 an hour per account (30 exports) |
| Running at once | Your plan's concurrent jobs (Advance 4, Studio 8) |
| Uploads | 60 an hour |
| Keys / webhooks | 10 active keys; 5 webhook endpoints; 10 test pings an hour |
| Download links | 1 hour; upload links 2 hours |
Over a limit you get 429 rate_limited: wait and retry with backoff.