Developers · API v1 · v0.39.0

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

curl
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, progress is a percentage when the job reports one.
  • output names the new assets; fetch each with GET /assets/{id} for its download link. A failed job refunds its reserve and carries error.
  • 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
Reads a reference image into a locked character every later scene, clip and sound inherits. Upload the image first with POST /uploads.
inputtypenotes
reference_path*stringThe `path` from POST /uploads (your own refs folder).
consent*trueYour attestation that you have the rights to the image and the person's consent (Acceptable Use Policy).
Output: output.character_id: the new character.
generate_image · Scene takesAll API plans
The locked character in a new scene. Studio Refine directs the prompt, scores every take against the reference, ranks them and re-rolls the take that drifted most (charged only if it runs).
inputtypenotes
character_id*uuidOne of your characters (GET /characters).
scene*stringThe scene, 3–600 characters.
takesinteger1 to your plan's takes per run (default 1).
aspect1:1 | 4:5 | 3:4 | 16:9 | 9:16Default 1:1; the quote's spec.aspects lists what the model draws.
resolution1K | 2K | 4KDefault 1K; larger sizes by plan and model.
Output: output.assets[]: {id, rank, score} best identity first.
generate_video · Storyboard clipAll API plans
Animates a scene take (frame 0): only the action changes, the character stays locked. Each clip is scored for identity.
inputtypenotes
scene_asset_id*uuidA scene take of yours: frame 0.
action*stringWhat happens, 3–600 characters.
seconds*integerOne of the model's durations (POST /quote returns them), up to your plan's limit.
aspect*stringOne of the model's aspects, e.g. 16:9.
camerastringCamera direction, e.g. slow push-in.
dialoguestringA line the character speaks on camera (≤ 200; models with audio, e.g. Veo 3.1).
voicestringHow it sounds, e.g. warm, Jamaican (≤ 80).
Output: output.asset_id, output.score.
generate_sound · Voice, music or sound effectAll API plans
A voice line in one of the studio voices, a music bed or a sound effect.
inputtypenotes
sound_kind*voice | music | sfxWhat to make.
text*stringvoice: the script (up to 2,000 characters). music / sfx: a description, 3–600.
voicestringvoice: a voice id, e.g. warm-female, deep-male, smooth-male.
deliverystringvoice: how to read it (≤ 120), e.g. excited, whispered.
secondsintegermusic: 30, 60 or 120; sfx: 5, 10 or 20.
character_iduuidOptional: file it under one of your characters.
Output: output.asset_id.
quick_shot · Quick ShotAll API plans
One job: 4 scene takes from the locked character, scored and ranked, then the best 1–4 animated and scored. Reserved up front; you pay only for what comes back.
inputtypenotes
character_id*uuidOne of your characters.
scene*string3–600 characters.
action*stringWhat happens in the clips, 3–600.
seconds*integerA duration the clip model offers (POST /quote with kind quick_shot).
aspect*stringA shape both models make (the quote's aspects).
clipsinteger1–4 (default 1).
camerastringCamera direction.
Output: output.scenes[], output.clips[] {asset_id, scene_asset_id, score, rank}, output.failed[].
extend_shot · Shot ExtenderAll API plans
Continues one of your clips seamlessly from a bridge frame; Refine scores the seam and can re-roll it.
inputtypenotes
source_asset_id*uuidA video of yours.
action*stringWhat happens next, 3–600.
seconds*integerLength of the new segment (model durations); the total is capped by your plan.
bridge_atnumberSeconds into the source to continue from (default: the end).
camerastring≤ 120.
promptstringA full directed prompt (≤ 1,500), if you have one.
negativestringWhat to avoid (≤ 300).
motioninteger1–10 (default 4).
seam_blendnumber0.04–0.5 seconds (default 0.12).
audioboth | mute_new | muteDefault both.
rerollbooleanRe-roll a drifted seam once (default true; charged only if it runs).
Output: output.asset_id (the joined clip).
analyze · Drift Diff (free)All API plans
Where a take drifted from its locked identity: area scores and boxed regions. The only analyze task offered through the API.
inputtypenotes
task*drift_diffAlways drift_diff.
asset_id*uuidA scene take, clip or character take of yours.
Output: output: {score, areas, regions[{area, severity, note, box{x,y,w,h} in %}], lighting}.

* 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} returns url, a signed link valid for an hour. Add ?download=1 to have it save as a file. Fetch a fresh one whenever you need it.
  • Uploads (reference images for extract_dna): ask POST /uploads for a one-time link, PUT the image to it, then start the job with the returned path.
curl
# 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.

What arrives
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): v1 is the hex HMAC-SHA256 of `${t}.${rawBody}`. Reject t older 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 in Studio876-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.
Node.js
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);
}
Python
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

401missing_api_keyNo Authorization: Bearer header.
401invalid_api_keyUnknown or revoked key.
402insufficient_creditsThe balance doesn't cover the quote.
403api_not_in_planThe account's plan has no API (Advance and Studio do).
403not_in_plan / preset_not_in_plan / …_not_in_planA tool, quality preset, size or length the plan doesn't include.
403account_suspendedThe account is suspended.
404not_foundMissing, or not yours.
429rate_limitedToo many requests or jobs; back off and retry.
429too_many_jobsYour plan's concurrent jobs are all running.
503no_model_available / model_unavailableNo model is serving that tool right now.
400bad_*A field failed validation; the message says which.

The message is plain language you can show to a person; branch on code.

09Limits

Requests120 a minute per key; 600 a minute per IP address
Paid jobs120 an hour per account (30 exports)
Running at onceYour plan's concurrent jobs (Advance 4, Studio 8)
Uploads60 an hour
Keys / webhooks10 active keys; 5 webhook endpoints; 10 test pings an hour
Download links1 hour; upload links 2 hours

Over a limit you get 429 rate_limited: wait and retry with backoff.

10Endpoints

GET/api/v1/me→ 200Your plan, credit balance and the key in use.
POST/api/v1/quote→ 200The model and credits a job would cost (same formula as the charge).body {kind, preset?, takes?, seconds?, resolution?, clips?}
POST/api/v1/jobs→ 202Start a job. Credits are reserved now and charged on delivery.body {kind, input, preset?, project_id?}
GET/api/v1/jobs→ 200Your recent jobs, newest first.query status, before, limit
GET/api/v1/jobs/{id}→ 200One job: status, progress, output, error, credits.
GET/api/v1/assets→ 200Your images, clips, sounds and exports, newest first.query project_id, type, before, limit
GET/api/v1/assets/{id}→ 200One asset with a download link valid for 1 hour (?download=1 to save as a file).query download
GET/api/v1/characters→ 200Your locked characters, with their DNA.query limit
GET/api/v1/projects→ 200Your projects.query limit
POST/api/v1/uploads→ 201A one-time link to upload a reference image (PUT it within 2 hours).body {mime: image/png | image/jpeg | image/webp}
GET/api/v1/webhooks→ 200Your webhook endpoints (never their secrets).
POST/api/v1/webhooks→ 201Add an endpoint (up to 5). The signing secret is in this response only.body {url, events[], description?}
DELETE/api/v1/webhooks/{id}→ 200Remove an endpoint.
POST/api/v1/webhooks/{id}/test→ 202Send a signed ping now (10 an hour).