{"openapi":"3.1.0","info":{"title":"TurboClip Public API","version":"1.0.0","description":"## Overview\n\nTurboClip turns a prompt into finished, ready-to-post content over one REST API — images, AI video clips, narrated videos, UGC ads and slideshows.\n\n- **An image** — `POST /media/image`: one image from the model you pick.\n- **A video clip** — `POST /media/video`: one clip straight from a video model, up to 30 seconds at 1080p on Seedance 2.5.\n- **A narrated video** — `POST /video`: script, voiceover, scenes, captions and music, cut into one MP4.\n- **A UGC ad** — `POST /ad`: a presenter speaking to camera about your product, as one MP4.\n- **A slideshow** — `POST /slideshow`: carousel slides made from your copy.\n- **Captions on your video** — `POST /captions`: a video you already have, transcribed and captioned in the look you pick, as one MP4.\n\n**Base URL:** `https://api.turboclip.ai/v1`. Every response is JSON: `{ \"success\": true, \"data\": … }`, or `{ \"success\": false, \"error\": … }` when something goes wrong.\n\n## Quickstart\n\n1. **Get an API key** in the app under **API → API keys** (https://platform.turboclip.ai/api#api-keys).\n2. **Ask for an image.** The answer carries a `jobId` and the credits it reserved.\n\n```bash\ncurl -X POST https://api.turboclip.ai/v1/media/image \\\n  -H \"x-api-key: $TURBOCLIP_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"prompt\": \"A golden retriever in a striped party hat, confetti falling in a sunlit living room\" }'\n```\n\n3. **Poll the job** every 5–10 seconds until `status` is `completed` — `output.url` is your image.\n\n```bash\ncurl https://api.turboclip.ai/v1/jobs/{jobId} -H \"x-api-key: $TURBOCLIP_API_KEY\"\n```\n\nEverything else works the same way: create, then poll `GET /jobs/{id}`. Each create call also has a free `/estimate` twin that prices the same body first.\n\n## Authentication\n\nSend your key in an `x-api-key: tc_sk_…` header on every request — not `Authorization: Bearer`, which is for the MCP server. Create keys in the app under **API → API keys** (https://platform.turboclip.ai/api#api-keys); the full key is shown once, so store it safely.\n\nCORS is open (`*`) on `api.turboclip.ai` — auth is key-based, never cookie-based, so browser tools can call it directly. The legacy base `https://platform.turboclip.ai/api/public` serves the same paths, indefinitely, but without CORS.\n\n## Credits\n\nAny account can use the API: what a call generates draws on the account's credits — a plan's monthly credits, or pay-as-you-go top-ups from **$5** (1 credit = 1¢). One welcome mat: an account that has never paid can plan UGC ads for free — the ad planning endpoints work up to a lifetime allowance, and credits come in at the generate step.\n\n- **Estimate first** (optional, free): `POST …/estimate` takes the same body as its create call and returns the cost, a line-by-line breakdown, and the exact models that will run.\n- **Create** reserves the estimated credits (402 if the balance is short), and **the reservation is the most it can cost**: a completed job is charged for what it made — anything it couldn't make is refunded when it settles (`creditsConsumed` on `GET /jobs/{id}`) — and a failed job releases everything.\n\n## Jobs and results\n\nEvery create call returns a `jobId`. Poll `GET /jobs/{id}` every 5–10 seconds until it's `completed` or `failed` — there's no render call and no separate fetch:\n\n- `completed` carries **`output`** — the image or clip (`output.url`), the MP4 (`output.videoUrl`), or the slides (`output.slides`). Videos stitch their final MP4 inside the job, so `completed` always means the file exists.\n- `failed` carries **`failure`** — what went wrong, whether a retry may work, and the credits released.\n\n`output` URLs are signed and expire after 24 hours — download the file, and fetch the job again for a fresh link rather than storing it. A job can take minutes; if your own request times out, poll the same `jobId` rather than creating again, which would start (and charge for) a second job.\n\n## Models and options\n\n`GET /models?for=image` (or `clip`, `video`, `ad`, `slideshow`) lists the models you can pick, each with its `capabilities`: aspect ratios, clip durations, resolutions, size tiers, qualities, and frame and reference rules. Pass a model's `id`:\n\n- on `POST /media/image` and `/media/video` as a top-level field — `imageModel`, or `videoModel` with `duration`, `resolution` and `aspectRatio`;\n- on `POST /video`, `/ad` and `/slideshow` inside `settings` — `settings.videoModel`, `settings.imageModel`, `settings.textModel`.\n\nFor one 30-second 1080p clip on Seedance 2.5:\n\n```json\n{ \"prompt\": \"…\", \"videoModel\": \"seedance-2.5\", \"duration\": 30, \"resolution\": \"1080p\" }\n```\n\nA value the model doesn't offer is a 400 whose message lists the ones it does.\n\n## Your own files\n\nAny field that takes an image or clip URL — and `videoUrl` on `POST /captions` — accepts a public URL, or upload a local file first: `POST /media/upload-url`, PUT the bytes to the URL it returns, then `POST /media/upload-complete` for a URL you can pass to any generate call. No public hosting needed.\n\n## Caption styles\n\n`captions` takes the same object on `POST /captions`, `/video` and `/ad`. Every field is optional: start from a `style` and change only what you want.\n\n- **style** — a preset to start from: `default`, `simple-plain`, `social-bold`, `simple-outline`, `minimal-lato-accent-2`, `minimal-simple-2`, `modern-inter-clean`, `viral-bangers-red`, `simple-subtitle`, `minimal-lato-accent`, `viral-bebas-punch`, `bold-impact-red`, `minimal-simple`, `glow-neon`, `elegant-editorial`, `elegant-silk`, `viral-bangers-classic`, `handwritten-caveat-simple`, `elegant-cinema`, `gaming-clutch`, `tech-orbitron-clean`, `tech-terminal`, `gaming-arcade`, `mystery-case-file`, `horror-ghost`, `horror-creepster-classic`, `horror-nosifer-blood` — or the id of one of your saved caption presets. `GET /styles?for=captions` shows each preset's look in these same fields.\n- **density** — words on screen at a time: `fast` (1-2 words), `medium` (3-4 words), `slow` (5-6 words, 9-11 words on a widescreen video), `cinematic` (8-10 words, 18-22 words on a widescreen video).\n- **position** — `top`, `middle` or `bottom`, or a number from 0 (top edge) to 100 (bottom edge).\n- **The words** — `font`, `size` (`small`, `medium`, `large`, or pixels on a 1080-pixel-wide frame), `bold`, `italic`, `textCase`, `textColor`, `outlineWidth`, `outlineColor`, `shadow`, and a coloured `glow` with its `glowColor`.\n- **The spoken word** — `highlight`: `word` colours it, `box` puts a box behind it, `none` leaves it plain; `highlightColor` is that colour.\n- **Behind the words** — `background`: `none`, `box` (hugs the words) or `bar` (full width), in `backgroundColor` (`rgba()` to see through it).\n- **animation** — how each caption comes in: `none`, `fade`, `zoom`, `slide_up`, `slide_down`, `slide_left`, `slide_right`, `bounce`, `pop`, `blur_in`, `rotate_in`.\n- **language** — the spoken language as an ISO 639 code (`es`, `pt`…), for transcribed captions; detected when omitted.\n\nColours are hex (`#FFD700`), `rgb()` or `rgba()`.\n\n```json\n{ \"style\": \"viral-bangers-classic\", \"position\": \"top\", \"highlightColor\": \"#00E5FF\" }\n```\n\n## Limits and errors\n\n30 requests a minute per API key, and 300 a minute across every key calling from one IP address — polling counts toward both. Beyond either you get a 429 with `Retry-After`; every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (unix seconds). An account can have at most 30 jobs in flight; a create beyond that is a 429 with `reason: \"too_many_active_jobs\"`.\n\n- **400** — the body isn't valid JSON or doesn't match the schema, a style is unknown, or a value isn't one the model offers (the message lists the allowed values).\n- **401** — the `x-api-key` is missing, wrong, or revoked.\n- **402** — not enough credits. `error` says by how much and where to top up (**Account → Billing**); `topUpUrl` opens it with the top-up that covers the shortfall.\n- **403** — a `teamId` you're not a member of.\n- **404** — not found, not yours, or an id that isn't a UUID.\n- **409** — the project isn't in a state this step can act on.\n- **413** — the upload is over the size limit.\n- **429** — too many requests, or too many jobs in flight; honor `Retry-After`.\n- **503** — maintenance (`reason: \"maintenance\"`); retry later.\n\n## AI agents (MCP)\n\nThe same API is exposed as MCP tools at `https://mcp.turboclip.ai/mcp` — connect with one-click sign-in, or send `Authorization: Bearer tc_sk_…` for headless use."},"servers":[{"url":"https://api.turboclip.ai/v1","description":"Canonical developer origin"},{"url":"https://platform.turboclip.ai/api/public","description":"Legacy base (supported indefinitely; no CORS)"}],"security":[{"apiKey":[]}],"tags":[{"name":"Images","description":"One AI image from the model you choose, saved straight to your media library."},{"name":"Videos","description":"One AI video clip straight from a video model — MiniMax H3, MiniMax H3 Max, Omni 1.1 Flash, Seedance 2.5, Seedance 2.0 Mini, Seedance 2.0, Seedance 2.0 Fast, Wan 3.0, Kling V3, Kling 4.0, Happy Horse 1.1 — up to that model’s longest take, saved to your media library. No script, narration or captions: for a finished video, see Narrated videos."},{"name":"Narrated videos","description":"Finished videos from one call: script, voiceover, scenes, captions and music, cut into one MP4 — from a topic, or from your own script."},{"name":"UGC ads","description":"Ads where a presenter speaks to camera about your product, natively voiced and lip-synced. `POST /ad` makes one in a single call; `POST /ad/plan` lets you review and edit the plan before anything renders."},{"name":"Slideshows","description":"Carousel-ready slides: you write the copy, TurboClip makes the images — with the same person on every slide if you want."},{"name":"Captions","description":"Captions burned into a video you already have: transcribed, broken into captions and styled the way you choose, returned as one MP4 — and kept as a project you can edit in the app."},{"name":"File uploads","description":"Turn a local file into a URL any generate call accepts — no public hosting needed."},{"name":"Jobs","description":"Every create call returns a `jobId`. Poll it until it completes — the finished file comes back on the job."},{"name":"Models","description":"The models and styles you can choose, with what each one supports."},{"name":"Balance","description":"Your credits, and whether you can generate right now."},{"name":"Teams","description":"The team workspaces you can create in."}],"x-tagGroups":[{"name":"Generate","tags":["Images","Videos","Narrated videos","UGC ads","Slideshows","Captions","File uploads"]},{"name":"Account","tags":["Jobs","Models","Balance","Teams"]}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Generate keys in the TurboClip app under **API → API keys** (https://platform.turboclip.ai/api#api-keys). The full key (`tc_sk_…`) is shown once at creation — store it securely; only a masked version is retrievable afterwards. Keys work on any account; a call that generates draws on the account's credits."}},"schemas":{"SuccessEnvelope":{"type":"object","properties":{"success":{"type":"boolean","enum":[true]},"data":{"type":"object","additionalProperties":{},"description":"Endpoint-specific payload — see each operation’s examples."}},"required":["success","data"],"description":"Every 2xx response wraps its payload once: `{ \"success\": true, \"data\": … }`."},"ApiError":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string","description":"Human/agent-readable reason."}},"required":["success","error"],"description":"Error responses are FLAT — `{ \"success\": false, \"error\": … }` with any extra fields alongside (never nested under `data`)."},"CreditError":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"required":{"type":"number","description":"Credits the job needs."},"available":{"type":"number","description":"Credits currently available."},"reason":{"type":"string","enum":["insufficient_credits","payment_failed","unknown_model","tier_gated","taste_limit"],"description":"`insufficient_credits` = top up (or pick a plan); `payment_failed` = the plan is live but its last charge was declined and Stripe is retrying — update the card (`required` is 0 here, so do NOT read it as a shortfall); `taste_limit` = an account that never paid has used its lifetime free ad-plan allowance (add credits to keep going)."},"topUpUrl":{"type":"string","format":"uri","description":"Account → Billing in the app, with the top-up that covers the shortfall preselected — set whenever money fixes it."},"feature":{"type":"string"},"requiredTier":{"type":"string"}},"required":["success","error","required","available","reason"],"description":"402 — the reservation could not be made. `error` says what happened, by how much, and where to top up."},"TierGateError":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"feature":{"type":"string"},"requiredTier":{"type":"string"}},"required":["success","error"],"description":"403 — not allowed: a `teamId` this account isn’t a member of, or (should a capability ever need a plan) a plan gate naming `feature` and `requiredTier`."},"RateLimitError":{"type":"object","properties":{"success":{"type":"boolean","enum":[false]},"error":{"type":"string"},"reason":{"type":"string","enum":["too_many_active_jobs"]},"required":{"type":"number"},"available":{"type":"number"}},"required":["success","error"],"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header."},"CreditEstimateItem":{"type":"object","properties":{"action_type":{"type":"string","example":"image_generation"},"model_id":{"type":"string","example":"gemini-3-pro-image-preview"},"estimated_units":{"type":"number"},"credits_per_unit":{"type":"number","description":"Fractional; the line rounds up once into `subtotal`."},"subtotal":{"type":"integer"}},"required":["action_type","model_id","estimated_units","credits_per_unit","subtotal"],"description":"One line of an estimate breakdown."},"CreditEstimate":{"type":"object","properties":{"total":{"type":"number"},"breakdown":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Array of CreditEstimateItem."}},"required":["total","breakdown"],"description":"The `estimate` object on every `/estimate` response — what the create call reserves, and the most the job can cost (a completed job is charged for what it made; a failed one releases it all)."},"JobModel":{"type":"object","properties":{"role":{"type":"string","enum":["text","image","video"]},"id":{"type":"string","example":"kling-v3"},"name":{"type":"string","example":"Kling V3"}},"required":["role","id","name"],"description":"The `models` array on estimate responses: the model(s) that will ACTUALLY run, with friendly names — derived from the same estimate that bills, so it can’t drift from what you’re charged."},"ImageGenerateRequest":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":30000,"description":"What to generate — the subject, the setting, the look, the motion."},"aspectRatio":{"type":"string","description":"Must be one of the model's capabilities.aspectRatios (GET /models). Defaults: image 1:1, video 9:16."},"imageModel":{"type":"string","description":"Image model id from GET /models?for=image. Top-level — not under settings. Omit for the default."},"imageSize":{"type":"string","description":"One of the image model's capabilities.sizeTiers. Changes the price."},"imageQuality":{"type":"string","description":"One of the image model's capabilities.qualities (models with supportsQualityToggle). Changes the price."},"referenceImageUrls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":5,"description":"Up to 5 image URLs — public, or from the upload flow — that guide style and subject, such as your product."},"teamId":{"type":"string","format":"uuid","description":"The team workspace to create in — an id from GET /teams. Omit for your default team."}},"required":["prompt"]},"VideoGenerateRequest":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":30000,"description":"What to generate — the subject, the setting, the look, the motion."},"aspectRatio":{"type":"string","description":"Must be one of the model's capabilities.aspectRatios (GET /models). Defaults: image 1:1, video 9:16."},"videoModel":{"type":"string","description":"Video model id from GET /models?for=clip (e.g. MiniMax, Seedance, Kling). Top-level — not under settings. Omit for the default."},"resolution":{"type":"string","description":"One of the model's capabilities.resolutions."},"duration":{"type":"number","exclusiveMinimum":0,"description":"Clip length in seconds — must be one of the model's capabilities.durations."},"audio":{"type":"boolean","description":"Native sound on/off — only models with capabilities.supportsAudioToggle. Omit for the model default."},"referenceImageUrls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":5,"description":"Up to 5 image URLs — public, or from the upload flow — that guide style and subject, such as your product."},"startFrameUrl":{"type":"string","format":"uri","description":"Video only — the image the clip opens on (image-to-video)."},"endFrameUrl":{"type":"string","format":"uri","description":"Video only — the image the clip ends on. Needs capabilities.supportsEndFrame."},"teamId":{"type":"string","format":"uuid","description":"The team workspace to create in — an id from GET /teams. Omit for your default team."}},"required":["prompt"]},"MediaGenerateRequest":{"type":"object","properties":{"mode":{"type":"string","enum":["image","video"],"description":"\"image\" or \"video\". Not sent to /media/image or /media/video."},"prompt":{"type":"string","minLength":1,"maxLength":30000,"description":"What to generate — the subject, the setting, the look, the motion."},"aspectRatio":{"type":"string","description":"Must be one of the model's capabilities.aspectRatios (GET /models). Defaults: image 1:1, video 9:16."},"imageModel":{"type":"string","description":"Image model id from GET /models?for=image. Top-level — not under settings. Omit for the default."},"imageSize":{"type":"string","description":"One of the image model's capabilities.sizeTiers. Changes the price."},"imageQuality":{"type":"string","description":"One of the image model's capabilities.qualities (models with supportsQualityToggle). Changes the price."},"videoModel":{"type":"string","description":"Video model id from GET /models?for=clip (e.g. MiniMax, Seedance, Kling). Top-level — not under settings. Omit for the default."},"resolution":{"type":"string","description":"One of the model's capabilities.resolutions."},"duration":{"type":"number","exclusiveMinimum":0,"description":"Clip length in seconds — must be one of the model's capabilities.durations."},"audio":{"type":"boolean","description":"Native sound on/off — only models with capabilities.supportsAudioToggle. Omit for the model default."},"referenceImageUrls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":5,"description":"Up to 5 image URLs — public, or from the upload flow — that guide style and subject, such as your product."},"startFrameUrl":{"type":"string","format":"uri","description":"Video only — the image the clip opens on (image-to-video)."},"endFrameUrl":{"type":"string","format":"uri","description":"Video only — the image the clip ends on. Needs capabilities.supportsEndFrame."},"teamId":{"type":"string","format":"uuid","description":"The team workspace to create in — an id from GET /teams. Omit for your default team."}},"required":["mode","prompt"]},"VideoCreateRequest":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":30000,"description":"What the video is about (mode \"topic\"), or the script to narrate (mode \"script\")."},"mode":{"type":"string","enum":["topic","script"],"default":"topic","description":"\"topic\": the AI writes the script from your prompt. \"script\": your prompt is the script, narrated as written."},"title":{"type":"string","description":"A name for the project."},"settings":{"type":"object","properties":{"videoModel":{"type":"string","description":"A video model id from GET /models. On a video, naming one renders every scene as an AI-video clip (omit it for image scenes — cheaper); on an ad, it is the model the shots render on."},"mediaMode":{"type":"string","enum":["image","video"],"description":"Rarely needed — naming a videoModel already means \"video\"."},"resolution":{"type":"string","description":"One of the video model's capabilities.resolutions."},"duration":{"type":"number","exclusiveMinimum":0,"description":"Total length in seconds."},"sceneCount":{"type":"integer","exclusiveMinimum":0,"description":"How many scenes to cut it into. Omit and the planner decides."},"aspectRatio":{"type":"string","description":"Must be one of the model's capabilities.aspectRatios (GET /models)."},"platform":{"type":"string"},"format":{"type":"string"},"narration":{"type":"boolean","description":"Voiceover on (the default) or off. Ads ignore it: their lines are spoken inside the clips."},"voiceId":{"type":"string","description":"The narrator voice. Omit for the default voice."},"voiceModel":{"type":"string"},"captionDensity":{"type":"string"},"textModel":{"type":"string","description":"The text model that writes and plans — an id from GET /models. Omit for the default."},"imageModel":{"type":"string","description":"The image model for scene images — an id from GET /models."},"imageSize":{"type":"string","description":"One of the image model's capabilities.sizeTiers. Changes the price."},"imageQuality":{"type":"string","description":"One of the image model's capabilities.qualities (models with supportsQualityToggle). Changes the price."},"style":{"type":"string","description":"A style name from GET /styles. An unknown name is a 400."},"imageStyle":{"type":"string","description":"Free-text direction for the look of the images."},"audio":{"type":"boolean","description":"The clips' native sound on or off (models with capabilities.supportsAudioToggle). Omit for the model's default."}},"description":"Length, look and models. Unknown keys are passed through."},"dataSourceIds":{"type":"array","items":{"type":"string","format":"uuid"}},"startFrameUrl":{"type":"string","format":"uri","description":"AI-video scenes only: the image the first scene opens on (image-to-video)."},"endFrameUrl":{"type":"string","format":"uri","description":"AI-video scenes only: the image the last scene ends on (models with capabilities.supportsEndFrame)."},"referenceImageUrls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":5,"description":"Up to 5 image URLs — public, or from the upload flow — that guide style and subject, such as your product."},"music":{"anyOf":[{"type":"boolean"},{"type":"string","minLength":1,"maxLength":2000},{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":2000,"description":"What the music should be. Omit and we write it from your video."},"volume":{"type":"number","minimum":0,"maximum":1,"description":"Mix gain, 0–1. Default 0.25 — the level for music under a voice."}}}],"description":"Background music — off unless set. true: we write the brief from the video. A string: your brief. { prompt?, volume? }: a brief plus the mix gain (0–1, default 0.25)."},"captions":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"style":{"type":"string","minLength":1,"maxLength":80,"description":"A caption style to start from: default (Default), simple-plain (Plain), social-bold (Creator), simple-outline (Outline), minimal-lato-accent-2 (Marker), minimal-simple-2 (Boxed), modern-inter-clean (Clean), viral-bangers-red (Pop), simple-subtitle (Subtitle), minimal-lato-accent (Spotlight), viral-bebas-punch (Punch), bold-impact-red (Impact), minimal-simple (Paper), glow-neon (Neon), elegant-editorial (Editorial), elegant-silk (Silk), viral-bangers-classic (Comic), handwritten-caveat-simple (Notebook), elegant-cinema (Cinema), gaming-clutch (Clutch), tech-orbitron-clean (Circuit), tech-terminal (Terminal), gaming-arcade (Arcade), mystery-case-file (Case File), horror-ghost (Ghost), horror-creepster-classic (Slime), horror-nosifer-blood (Blood). Every other field overrides one setting of it. Defaults to default."},"density":{"type":"string","minLength":1,"maxLength":40,"description":"Words on screen at a time: fast (1-2 words), medium (3-4 words), slow (5-6 words, 9-11 words on a widescreen video), cinematic (8-10 words, 18-22 words on a widescreen video). Defaults to the style's own."},"position":{"anyOf":[{"type":"string","enum":["top","middle","bottom"]},{"type":"number","minimum":0,"maximum":100}],"description":"Where the captions sit: top, middle or bottom, or a number from 0 (top edge) to 100 (bottom edge). Defaults to bottom."},"font":{"type":"string","enum":["Arial","Times New Roman","Courier New","Inter","Instrument Serif","Roboto","Montserrat","Poppins","Oswald","Raleway","Lato","Bebas Neue","Bangers","Creepster","Press Start 2P","Playfair Display","Permanent Marker","Orbitron","Caveat","Anton","Audiowide","Black Ops One","Bungee","Butcherman","Cinzel","Cormorant Garamond","Dancing Script","Eater","Exo 2","Indie Flower","Nosifer","Nunito","Open Sans","Pacifico","Rajdhani","Righteous","Shadows Into Light","Source Sans 3","Space Mono","Special Elite","VT323","Work Sans","Manrope","Fraunces","Syne","Albert Sans","Merriweather","Tenor Sans","Space Grotesk","IBM Plex Sans"],"description":"The caption font."},"size":{"anyOf":[{"type":"string","enum":["small","medium","large"]},{"type":"number","minimum":12,"maximum":120}],"description":"Text size: small, medium or large, or pixels on a 1080-pixel-wide frame (12-120). Defaults to the style's own."},"bold":{"type":"boolean","description":"Bold text."},"italic":{"type":"boolean","description":"Italic text."},"textCase":{"type":"string","enum":["normal","uppercase","lowercase","titlecase"],"description":"Letter case. normal shows the words as spoken."},"textColor":{"type":"string","description":"Text colour. A colour: hex (#FFD700), rgb() or rgba()."},"highlight":{"type":"string","enum":["word","box","none"],"description":"How the word being spoken is marked: word (its own colour), box (a box behind it) or none."},"highlightColor":{"type":"string","description":"The highlight colour: the spoken word's colour (word) or the box behind it (box). A colour: hex (#FFD700), rgb() or rgba()."},"outlineWidth":{"type":"number","minimum":0,"maximum":20,"description":"Text outline thickness, 0-20. 0 for none."},"outlineColor":{"type":"string","description":"Outline colour. A colour: hex (#FFD700), rgb() or rgba()."},"shadow":{"type":"number","minimum":0,"maximum":10,"description":"Drop shadow strength, 0-10. 0 for none."},"glow":{"type":"number","minimum":0,"maximum":10,"description":"Strength of a coloured glow around the text, 0-10. 0 for none."},"glowColor":{"type":"string","description":"Glow colour. A colour: hex (#FFD700), rgb() or rgba()."},"background":{"type":"string","enum":["none","box","bar"],"description":"Behind the text: none, box (hugs the words) or bar (full width)."},"backgroundColor":{"type":"string","description":"Box or bar colour; rgba() makes it see-through. A colour: hex (#FFD700), rgb() or rgba()."},"animation":{"type":"string","enum":["none","fade","zoom","slide_up","slide_down","slide_left","slide_right","bounce","pop","blur_in","rotate_in"],"description":"How each caption comes in."},"language":{"type":"string","pattern":"^[a-zA-Z]{2,3}$","description":"The spoken language as an ISO 639 code (en, es, pt…), for transcribed captions. Detected automatically when omitted."}}}],"description":"Words on screen — off unless set, and free on a narrated video. true, or { density?, style? }."},"teamId":{"type":"string","format":"uuid","description":"The team workspace to create in — an id from GET /teams. Omit for your default team."}},"required":["prompt"]},"AdPlanRequest":{"type":"object","properties":{"brief":{"type":"string","minLength":1,"maxLength":30000,"description":"What the ad is for: the product, who it is for, the angle, and how it should end."},"mode":{"type":"string","enum":["topic","script"],"default":"topic","description":"\"topic\": the AI plans the shots and lines from your brief. \"script\": your brief is the script to follow."},"title":{"type":"string","description":"A name for the project."},"settings":{"type":"object","properties":{"videoModel":{"type":"string","description":"A video model id from GET /models. On a video, naming one renders every scene as an AI-video clip (omit it for image scenes — cheaper); on an ad, it is the model the shots render on."},"mediaMode":{"type":"string","enum":["image","video"],"description":"Rarely needed — naming a videoModel already means \"video\"."},"resolution":{"type":"string","description":"One of the video model's capabilities.resolutions."},"duration":{"type":"number","exclusiveMinimum":0,"description":"Total length in seconds."},"aspectRatio":{"type":"string","description":"Must be one of the model's capabilities.aspectRatios (GET /models)."},"platform":{"type":"string"},"format":{"type":"string"},"narration":{"type":"boolean","description":"Voiceover on (the default) or off. Ads ignore it: their lines are spoken inside the clips."},"voiceId":{"type":"string","description":"The narrator voice. Omit for the default voice."},"voiceModel":{"type":"string"},"captionDensity":{"type":"string"},"textModel":{"type":"string","description":"The text model that writes and plans — an id from GET /models. Omit for the default."},"imageModel":{"type":"string","description":"The image model for scene images — an id from GET /models."},"imageSize":{"type":"string","description":"One of the image model's capabilities.sizeTiers. Changes the price."},"imageQuality":{"type":"string","description":"One of the image model's capabilities.qualities (models with supportsQualityToggle). Changes the price."},"style":{"type":"string","description":"A style name from GET /styles. An unknown name is a 400."},"imageStyle":{"type":"string","description":"Free-text direction for the look of the images."},"audio":{"type":"boolean","description":"The clips' native sound on or off (models with capabilities.supportsAudioToggle). Omit for the model's default."},"adType":{"type":"string","description":"What's being advertised — steers the planner. One of: product, app, game, course, coaching, service, personal-brand, other."},"linkedMode":{"type":"boolean","description":"true: one seamless, continuous take instead of cuts."},"shots":{"type":"integer","exclusiveMinimum":0,"description":"How many shots the ad cuts between. Omit and the planner cuts the fewest, longest clips the model allows; a count the model's clip lengths can't fit is clamped."}},"description":"Length, shots, model and look. Unknown keys are passed through."},"presenterImageUrl":{"type":"string","format":"uri","description":"Your presenter or product photo. The ad opens on it, so no seed image is generated (or charged)."},"referenceImageUrls":{"type":"array","items":{"type":"string","format":"uri"},"maxItems":5,"description":"Up to 5 images (your product, your presenter) bound into the seed image so they hold in every shot. Each one is priced."},"endFrameUrl":{"type":"string","format":"uri","description":"The image the ad ends on (models with capabilities.supportsEndFrame)."},"dataSourceIds":{"type":"array","items":{"type":"string","format":"uuid"}},"music":{"anyOf":[{"type":"boolean"},{"type":"string","minLength":1,"maxLength":2000},{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":2000,"description":"What the music should be. Omit and we write it from your video."},"volume":{"type":"number","minimum":0,"maximum":1,"description":"Mix gain, 0–1. Default 0.25 — the level for music under a voice."}}}],"description":"Background music — ON by default for ads. false: none. A string: your brief. { prompt?, volume? }: a brief plus the mix gain (0–1, default 0.25)."},"captions":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"style":{"type":"string","minLength":1,"maxLength":80,"description":"A caption style to start from: default (Default), simple-plain (Plain), social-bold (Creator), simple-outline (Outline), minimal-lato-accent-2 (Marker), minimal-simple-2 (Boxed), modern-inter-clean (Clean), viral-bangers-red (Pop), simple-subtitle (Subtitle), minimal-lato-accent (Spotlight), viral-bebas-punch (Punch), bold-impact-red (Impact), minimal-simple (Paper), glow-neon (Neon), elegant-editorial (Editorial), elegant-silk (Silk), viral-bangers-classic (Comic), handwritten-caveat-simple (Notebook), elegant-cinema (Cinema), gaming-clutch (Clutch), tech-orbitron-clean (Circuit), tech-terminal (Terminal), gaming-arcade (Arcade), mystery-case-file (Case File), horror-ghost (Ghost), horror-creepster-classic (Slime), horror-nosifer-blood (Blood). Every other field overrides one setting of it. Defaults to default."},"density":{"type":"string","minLength":1,"maxLength":40,"description":"Words on screen at a time: fast (1-2 words), medium (3-4 words), slow (5-6 words, 9-11 words on a widescreen video), cinematic (8-10 words, 18-22 words on a widescreen video). Defaults to the style's own."},"position":{"anyOf":[{"type":"string","enum":["top","middle","bottom"]},{"type":"number","minimum":0,"maximum":100}],"description":"Where the captions sit: top, middle or bottom, or a number from 0 (top edge) to 100 (bottom edge). Defaults to bottom."},"font":{"type":"string","enum":["Arial","Times New Roman","Courier New","Inter","Instrument Serif","Roboto","Montserrat","Poppins","Oswald","Raleway","Lato","Bebas Neue","Bangers","Creepster","Press Start 2P","Playfair Display","Permanent Marker","Orbitron","Caveat","Anton","Audiowide","Black Ops One","Bungee","Butcherman","Cinzel","Cormorant Garamond","Dancing Script","Eater","Exo 2","Indie Flower","Nosifer","Nunito","Open Sans","Pacifico","Rajdhani","Righteous","Shadows Into Light","Source Sans 3","Space Mono","Special Elite","VT323","Work Sans","Manrope","Fraunces","Syne","Albert Sans","Merriweather","Tenor Sans","Space Grotesk","IBM Plex Sans"],"description":"The caption font."},"size":{"anyOf":[{"type":"string","enum":["small","medium","large"]},{"type":"number","minimum":12,"maximum":120}],"description":"Text size: small, medium or large, or pixels on a 1080-pixel-wide frame (12-120). Defaults to the style's own."},"bold":{"type":"boolean","description":"Bold text."},"italic":{"type":"boolean","description":"Italic text."},"textCase":{"type":"string","enum":["normal","uppercase","lowercase","titlecase"],"description":"Letter case. normal shows the words as spoken."},"textColor":{"type":"string","description":"Text colour. A colour: hex (#FFD700), rgb() or rgba()."},"highlight":{"type":"string","enum":["word","box","none"],"description":"How the word being spoken is marked: word (its own colour), box (a box behind it) or none."},"highlightColor":{"type":"string","description":"The highlight colour: the spoken word's colour (word) or the box behind it (box). A colour: hex (#FFD700), rgb() or rgba()."},"outlineWidth":{"type":"number","minimum":0,"maximum":20,"description":"Text outline thickness, 0-20. 0 for none."},"outlineColor":{"type":"string","description":"Outline colour. A colour: hex (#FFD700), rgb() or rgba()."},"shadow":{"type":"number","minimum":0,"maximum":10,"description":"Drop shadow strength, 0-10. 0 for none."},"glow":{"type":"number","minimum":0,"maximum":10,"description":"Strength of a coloured glow around the text, 0-10. 0 for none."},"glowColor":{"type":"string","description":"Glow colour. A colour: hex (#FFD700), rgb() or rgba()."},"background":{"type":"string","enum":["none","box","bar"],"description":"Behind the text: none, box (hugs the words) or bar (full width)."},"backgroundColor":{"type":"string","description":"Box or bar colour; rgba() makes it see-through. A colour: hex (#FFD700), rgb() or rgba()."},"animation":{"type":"string","enum":["none","fade","zoom","slide_up","slide_down","slide_left","slide_right","bounce","pop","blur_in","rotate_in"],"description":"How each caption comes in."},"language":{"type":"string","pattern":"^[a-zA-Z]{2,3}$","description":"The spoken language as an ISO 639 code (en, es, pt…), for transcribed captions. Detected automatically when omitted."}}}],"description":"Words on screen — off unless set. true, or { density?, style? }. On an ad the clips are transcribed, so captions cost a little."},"teamId":{"type":"string","format":"uuid","description":"The team workspace to create in — an id from GET /teams. Omit for your default team."}},"required":["brief"]},"AdAnswersRequest":{"type":"object","properties":{"answers":{"type":"array","items":{"type":"object","properties":{"questionId":{"type":"string","minLength":1,"description":"The question's id, from GET /ad/{id}/next."},"answer":{"type":"string","maxLength":2000,"description":"Your reply: free text, a suggestion's label, or one of the question's options[].value."}},"required":["questionId","answer"]},"maxItems":8}},"required":["answers"]},"AdReviseRequest":{"type":"object","properties":{"shots":{"type":"array","items":{"type":"object","properties":{"sceneIndex":{"type":"integer","minimum":0,"description":"The shot to change, from GET /ad/{id}/plan."},"mediaDescription":{"type":"string","minLength":1,"maxLength":30000,"description":"The shot's new direction. It replaces the whole thing, spoken line included."}},"required":["sceneIndex","mediaDescription"]},"minItems":1}},"required":["shots"]},"SlideshowCreateRequest":{"type":"object","properties":{"title":{"type":"string","minLength":1,"description":"The slideshow's title."},"description":{"type":"string"},"characterLock":{"type":"object","properties":{"imageDescription":{"type":"string","minLength":1,"description":"Describe them closely — age, build, hair, skin, eyes, clothing, accessories, vibe."}},"required":["imageDescription"],"description":"Keep one person the same on every slide marked hasCharacter. Adds one reference image."},"slides":{"type":"array","items":{"type":"object","properties":{"slideNumber":{"type":"integer","minimum":1,"description":"The slide's position, counting from 1."},"title":{"type":"string","description":"The headline on the slide."},"body":{"type":"string","minLength":1,"description":"The text under the headline. Leave it off an overlay slide."},"imageDescription":{"type":"string","description":"What the slide's image shows. Leave it off any slide and the AI art-directs every slide (one extra text call)."},"referenceImage":{"type":"string","format":"uri","description":"An image composited into the slide, such as an app screenshot on a phone. Takes the character lock’s place on that slide."},"imageOverlay":{"type":"string","format":"uri","description":"A PNG laid on top of the slide, such as a branded end card."},"hasCharacter":{"type":"boolean","description":"true: the characterLock person appears on this slide."}},"required":["slideNumber"]},"minItems":1},"settings":{"type":"object","properties":{"aspectRatio":{"type":"string","description":"The slide shape, such as 9:16."},"imageModel":{"type":"string","description":"An image model id from GET /models?for=slideshow."},"imageSize":{"type":"string","description":"One of the image model's capabilities.sizeTiers. Changes the price."},"imageQuality":{"type":"string","description":"One of the image model's capabilities.qualities. Changes the price."},"style":{"type":"string","description":"A style name from GET /styles. An unknown name is a 400."},"styleId":{"type":"string","format":"uuid"},"imageStyle":{"type":"string","description":"Free-text direction for the look of the images."},"textModel":{"type":"string","description":"The text model that art-directs slides left without an imageDescription."},"captionStyles":{"type":"object","additionalProperties":{}}}},"teamId":{"type":"string","format":"uuid","description":"The team workspace to create in — an id from GET /teams. Omit for your default team."}},"required":["title","slides"]},"CaptionsRequest":{"type":"object","properties":{"videoUrl":{"type":"string","format":"uri","description":"The video to caption: a public https link, or the url the upload flow returns. MP4, MOV or WebM with speech, up to 10 minutes and 500 MB."},"captions":{"type":"object","properties":{"style":{"anyOf":[{"type":"string","enum":["default","simple-plain","social-bold","simple-outline","minimal-lato-accent-2","minimal-simple-2","modern-inter-clean","viral-bangers-red","simple-subtitle","minimal-lato-accent","viral-bebas-punch","bold-impact-red","minimal-simple","glow-neon","elegant-editorial","elegant-silk","viral-bangers-classic","handwritten-caveat-simple","elegant-cinema","gaming-clutch","tech-orbitron-clean","tech-terminal","gaming-arcade","mystery-case-file","horror-ghost","horror-creepster-classic","horror-nosifer-blood"]},{"type":"string","format":"uuid"}],"description":"A caption style to start from: default (Default), simple-plain (Plain), social-bold (Creator), simple-outline (Outline), minimal-lato-accent-2 (Marker), minimal-simple-2 (Boxed), modern-inter-clean (Clean), viral-bangers-red (Pop), simple-subtitle (Subtitle), minimal-lato-accent (Spotlight), viral-bebas-punch (Punch), bold-impact-red (Impact), minimal-simple (Paper), glow-neon (Neon), elegant-editorial (Editorial), elegant-silk (Silk), viral-bangers-classic (Comic), handwritten-caveat-simple (Notebook), elegant-cinema (Cinema), gaming-clutch (Clutch), tech-orbitron-clean (Circuit), tech-terminal (Terminal), gaming-arcade (Arcade), mystery-case-file (Case File), horror-ghost (Ghost), horror-creepster-classic (Slime), horror-nosifer-blood (Blood), or the id of one of your saved caption presets. Every other field overrides one setting of it. Defaults to default."},"density":{"type":"string","enum":["fast","medium","slow","cinematic"],"description":"Words on screen at a time: fast (1-2 words), medium (3-4 words), slow (5-6 words, 9-11 words on a widescreen video), cinematic (8-10 words, 18-22 words on a widescreen video). Defaults to the style's own."},"position":{"anyOf":[{"type":"string","enum":["top","middle","bottom"]},{"type":"number","minimum":0,"maximum":100}],"description":"Where the captions sit: top, middle or bottom, or a number from 0 (top edge) to 100 (bottom edge). Defaults to bottom."},"font":{"type":"string","enum":["Arial","Times New Roman","Courier New","Inter","Instrument Serif","Roboto","Montserrat","Poppins","Oswald","Raleway","Lato","Bebas Neue","Bangers","Creepster","Press Start 2P","Playfair Display","Permanent Marker","Orbitron","Caveat","Anton","Audiowide","Black Ops One","Bungee","Butcherman","Cinzel","Cormorant Garamond","Dancing Script","Eater","Exo 2","Indie Flower","Nosifer","Nunito","Open Sans","Pacifico","Rajdhani","Righteous","Shadows Into Light","Source Sans 3","Space Mono","Special Elite","VT323","Work Sans","Manrope","Fraunces","Syne","Albert Sans","Merriweather","Tenor Sans","Space Grotesk","IBM Plex Sans"],"description":"The caption font."},"size":{"anyOf":[{"type":"string","enum":["small","medium","large"]},{"type":"number","minimum":12,"maximum":120}],"description":"Text size: small, medium or large, or pixels on a 1080-pixel-wide frame (12-120). Defaults to the style's own."},"bold":{"type":"boolean","description":"Bold text."},"italic":{"type":"boolean","description":"Italic text."},"textCase":{"type":"string","enum":["normal","uppercase","lowercase","titlecase"],"description":"Letter case. normal shows the words as spoken."},"textColor":{"type":"string","description":"Text colour. A colour: hex (#FFD700), rgb() or rgba()."},"highlight":{"type":"string","enum":["word","box","none"],"description":"How the word being spoken is marked: word (its own colour), box (a box behind it) or none."},"highlightColor":{"type":"string","description":"The highlight colour: the spoken word's colour (word) or the box behind it (box). A colour: hex (#FFD700), rgb() or rgba()."},"outlineWidth":{"type":"number","minimum":0,"maximum":20,"description":"Text outline thickness, 0-20. 0 for none."},"outlineColor":{"type":"string","description":"Outline colour. A colour: hex (#FFD700), rgb() or rgba()."},"shadow":{"type":"number","minimum":0,"maximum":10,"description":"Drop shadow strength, 0-10. 0 for none."},"glow":{"type":"number","minimum":0,"maximum":10,"description":"Strength of a coloured glow around the text, 0-10. 0 for none."},"glowColor":{"type":"string","description":"Glow colour. A colour: hex (#FFD700), rgb() or rgba()."},"background":{"type":"string","enum":["none","box","bar"],"description":"Behind the text: none, box (hugs the words) or bar (full width)."},"backgroundColor":{"type":"string","description":"Box or bar colour; rgba() makes it see-through. A colour: hex (#FFD700), rgb() or rgba()."},"animation":{"type":"string","enum":["none","fade","zoom","slide_up","slide_down","slide_left","slide_right","bounce","pop","blur_in","rotate_in"],"description":"How each caption comes in."},"language":{"type":"string","pattern":"^[a-zA-Z]{2,3}$","description":"The spoken language as an ISO 639 code (en, es, pt…), for transcribed captions. Detected automatically when omitted."}},"description":"How the captions look. Omit for the house style."},"title":{"type":"string","minLength":1,"maxLength":200,"description":"A name for the project. Defaults to the video's file name."},"teamId":{"type":"string","format":"uuid","description":"The team workspace to create in — an id from GET /teams. Omit for your default team."}},"required":["videoUrl"]},"UploadUrlRequest":{"type":"object","properties":{"fileName":{"type":"string","minLength":1,"maxLength":200,"description":"The file's name. Its extension sets the type when contentType is omitted."},"contentType":{"type":"string","maxLength":120,"description":"The MIME type, such as image/png. Wins over the extension."}},"required":["fileName"]},"UploadCompleteRequest":{"type":"object","properties":{"storagePath":{"type":"string","minLength":1,"maxLength":500,"description":"Exactly as POST /media/upload-url returned it."},"fileName":{"type":"string","minLength":1,"maxLength":200,"description":"A name for the library entry. Defaults to the file's own name."},"teamId":{"type":"string","format":"uuid","description":"The team workspace to create in — an id from GET /teams. Omit for your default team."}},"required":["storagePath"]}},"parameters":{}},"paths":{"/media/image":{"post":{"operationId":"generateImage","tags":["Images"],"summary":"Generate image","description":"One AI image from the model you choose, saved to your media library. Only `prompt` is required — the request examples show the common options.\n\n- **Model** — `imageModel`, an id from `GET /models?for=image`. Omit it for the default (GPT Image 2.5 Flare).\n- **Shape and detail** — `aspectRatio` (default 1:1), `imageSize` and `imageQuality`, each one of the model's `capabilities` values. Size and quality change the price.\n- **Your images** — `referenceImageUrls` (up to 5) keep a product, a face or a style. Upload local files first (File uploads).\n\nThen poll `GET /jobs/{jobId}` until `completed` — `output.url` is the file (`GET /media/{jobId}` returns the same). Price it first with `POST /media/image/estimate` — the same body, free. Pass `teamId` to create in another of your team workspaces (`GET /teams`); omit it for your default team. A team you’re not a member of is a 403.\n\n**Auth:** any valid API key. What it generates draws on the account’s credits — a plan’s, or pay-as-you-go top-ups.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"One image. Only `prompt` is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageGenerateRequest"},"examples":{"textToImage":{"summary":"Text to image","value":{"prompt":"Studio product shot of a matte-black cold brew bottle on wet slate, soft rim light, condensation beads"}},"modelSizeQuality":{"summary":"GPT Image 2.5 Flare, 4K, xhigh quality","value":{"prompt":"Editorial portrait of a ceramicist at her wheel, window light, clay dust in the air, shallow depth of field","imageModel":"gpt-image-2.5-flare","aspectRatio":"4:5","imageSize":"4K","imageQuality":"xhigh"}},"withReference":{"summary":"Keep your product (reference image)","value":{"prompt":"This exact bottle on a picnic blanket in a sunlit park, a hand reaching for it, natural light","referenceImageUrls":["https://cdn.example.com/cold-brew-bottle.png"]}}}}}},"responses":{"200":{"description":"Job started and credits reserved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"started":{"value":{"success":true,"data":{"jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","mode":"image","creditsReserved":2,"creditsRemaining":498,"nextStep":"Poll GET /jobs/{jobId} until completed — the response then carries output.url, the file."}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/media/image/estimate":{"post":{"operationId":"estimateImage","tags":["Images"],"summary":"Estimate cost","description":"What `POST /media/image` would cost — the same body, free, nothing reserved. Returns `estimate.total` (credits), the line-by-line `breakdown`, and the `models` that would run. A balance that doesn’t cover it is still a 200 — `sufficient: false`, with the cost shown in full. A value the model doesn't offer is a 400, as on the create call.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"One image. Only `prompt` is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageGenerateRequest"},"examples":{"textToImage":{"summary":"Text to image","value":{"prompt":"Studio product shot of a matte-black cold brew bottle on wet slate, soft rim light, condensation beads"}},"modelSizeQuality":{"summary":"GPT Image 2.5 Flare, 4K, xhigh quality","value":{"prompt":"Editorial portrait of a ceramicist at her wheel, window light, clay dust in the air, shallow depth of field","imageModel":"gpt-image-2.5-flare","aspectRatio":"4:5","imageSize":"4K","imageQuality":"xhigh"}},"withReference":{"summary":"Keep your product (reference image)","value":{"prompt":"This exact bottle on a picnic blanket in a sunlit park, a hand reaching for it, natural light","referenceImageUrls":["https://cdn.example.com/cold-brew-bottle.png"]}}}}}},"responses":{"200":{"description":"The cost (nothing is reserved).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"sufficient":{"value":{"success":true,"data":{"sufficient":true,"mode":"image","estimate":{"total":2,"breakdown":[{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.3038,"subtotal":2}]},"models":[{"role":"image","id":"gpt-image-2.5-flare","name":"GPT Image 2.5 Flare"}],"balance":{"available":500,"reserved":0,"used":120,"total":620}}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/media/video":{"post":{"operationId":"generateVideoClip","tags":["Videos"],"summary":"Generate video","description":"One AI video clip straight from the video model you choose, saved to your media library — up to the model's longest take (30 seconds on Seedance 2.5). No script, narration or captions: for a finished video, use `POST /video`. Only `prompt` is required — the request examples show the common options.\n\n- **Model** — `videoModel`, an id from `GET /models?for=clip`. Omit it for the default (Omni 1.1 Flash).\n- **Length and quality** — `duration` (seconds), `resolution` and `aspectRatio` (default 9:16), each one of the model's `capabilities` values. A value the model doesn't offer is a 400 listing the ones it does.\n- **Sound** — `audio: false` for a silent clip, on models with `supportsAudioToggle`.\n- **Frames and references** — `startFrameUrl` animates an image (image-to-video); `endFrameUrl` sets the last frame (models with `supportsEndFrame`); `referenceImageUrls` guide style and subject. Where a model has `combinesFramesWithReferences: false`, use frames or references, not both.\n\nThen poll `GET /jobs/{jobId}` until `completed` — `output.url` is the file (`GET /media/{jobId}` returns the same). A clip takes a minute or more — if your request times out, keep polling the same job rather than creating another. Price it first with `POST /media/video/estimate` — the same body, free. Pass `teamId` to create in another of your team workspaces (`GET /teams`); omit it for your default team. A team you’re not a member of is a 403.\n\n**Auth:** any valid API key. What it generates draws on the account’s credits — a plan’s, or pay-as-you-go top-ups.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"One clip. Only `prompt` is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoGenerateRequest"},"examples":{"textToVideo":{"summary":"Text to video (Omni 1.1 Flash, the default)","value":{"prompt":"Slow push-in on waves breaking over black volcanic rock at golden hour, spray catching the light"}},"longTake":{"summary":"One 30-second 1080p take on Seedance 2.5","value":{"prompt":"A cyclist climbs a switchback mountain road at dawn; the camera tracks alongside, then cranes up to reveal the valley below","videoModel":"seedance-2.5","duration":30,"resolution":"1080p","aspectRatio":"16:9"}},"imageToVideo":{"summary":"Animate an image (start frame)","value":{"prompt":"Steam curls up from the cup as the camera eases in","startFrameUrl":"https://cdn.example.com/coffee-cup.jpg"}},"frameToFrame":{"summary":"From one image to another (MiniMax H3)","value":{"prompt":"The pencil sketch fills with colour until it becomes the finished painting","videoModel":"minimax-h3","startFrameUrl":"https://cdn.example.com/sketch.png","endFrameUrl":"https://cdn.example.com/painting.png"}}}}}},"responses":{"200":{"description":"Job started and credits reserved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"started":{"value":{"success":true,"data":{"jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","mode":"video","creditsReserved":2133,"creditsRemaining":2167,"nextStep":"Poll GET /jobs/{jobId} until completed — the response then carries output.url, the file."}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/media/video/estimate":{"post":{"operationId":"estimateVideoClip","tags":["Videos"],"summary":"Estimate cost","description":"What `POST /media/video` would cost — the same body, free, nothing reserved. Returns `estimate.total` (credits), the line-by-line `breakdown`, and the `models` that would run. A balance that doesn’t cover it is still a 200 — `sufficient: false`, with the cost shown in full. A value the model doesn't offer is a 400, as on the create call.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"One clip. Only `prompt` is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoGenerateRequest"},"examples":{"textToVideo":{"summary":"Text to video (Omni 1.1 Flash, the default)","value":{"prompt":"Slow push-in on waves breaking over black volcanic rock at golden hour, spray catching the light"}},"longTake":{"summary":"One 30-second 1080p take on Seedance 2.5","value":{"prompt":"A cyclist climbs a switchback mountain road at dawn; the camera tracks alongside, then cranes up to reveal the valley below","videoModel":"seedance-2.5","duration":30,"resolution":"1080p","aspectRatio":"16:9"}},"imageToVideo":{"summary":"Animate an image (start frame)","value":{"prompt":"Steam curls up from the cup as the camera eases in","startFrameUrl":"https://cdn.example.com/coffee-cup.jpg"}},"frameToFrame":{"summary":"From one image to another (MiniMax H3)","value":{"prompt":"The pencil sketch fills with colour until it becomes the finished painting","videoModel":"minimax-h3","startFrameUrl":"https://cdn.example.com/sketch.png","endFrameUrl":"https://cdn.example.com/painting.png"}}}}}},"responses":{"200":{"description":"The cost (nothing is reserved).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"sufficient":{"summary":"One 30-second 1080p take on Seedance 2.5","value":{"success":true,"data":{"sufficient":true,"mode":"video","estimate":{"total":2133,"breakdown":[{"action_type":"video_generation","model_id":"seedance-2.5","estimated_units":30,"credits_per_unit":71.075,"subtotal":2133}]},"models":[{"role":"video","id":"seedance-2.5","name":"Seedance 2.5"}],"balance":{"available":4300,"reserved":0,"used":120,"total":4420}}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/media/generate":{"post":{"operationId":"generateMedia","tags":["Images"],"summary":"Generate image or video","description":"The original door to `POST /media/image` and `/media/video`: the same request with `mode: \"image\"` or `mode: \"video\"` in the body. Still served. Then poll `GET /jobs/{jobId}` until `completed` — `output.url` is the file (`GET /media/{jobId}` returns the same).\n\n**Auth:** any valid API key. What it generates draws on the account’s credits — a plan’s, or pay-as-you-go top-ups.","security":[{"apiKey":[]}],"x-scalar-ignore":true,"requestBody":{"required":true,"description":"One image or one clip, with `mode` saying which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGenerateRequest"},"examples":{"image":{"summary":"An image","value":{"mode":"image","prompt":"Studio product shot of a matte-black cold brew bottle on wet slate, soft rim light, condensation beads"}}}}}},"responses":{"200":{"description":"Job started and credits reserved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"started":{"value":{"success":true,"data":{"jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","mode":"image","creditsReserved":2,"creditsRemaining":498}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/media/estimate":{"post":{"operationId":"estimateMedia","tags":["Images"],"summary":"Estimate image or video cost","description":"The cost preview for `POST /media/generate` (`mode` in the body). Still served; `POST /media/image/estimate` and `/media/video/estimate` are the documented doors.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"x-scalar-ignore":true,"requestBody":{"required":true,"description":"One image or one clip, with `mode` saying which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaGenerateRequest"},"examples":{"image":{"summary":"An image","value":{"mode":"image","prompt":"Studio product shot of a matte-black cold brew bottle on wet slate, soft rim light, condensation beads"}}}}}},"responses":{"200":{"description":"The cost (nothing is reserved).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"sufficient":{"value":{"success":true,"data":{"sufficient":true,"mode":"image","estimate":{"total":2,"breakdown":[{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.3038,"subtotal":2}]},"models":[{"role":"image","id":"gpt-image-2.5-flare","name":"GPT Image 2.5 Flare"}],"balance":{"available":500,"reserved":0,"used":120,"total":620}}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/video":{"post":{"operationId":"createVideo","tags":["Narrated videos"],"summary":"Create narrated video","description":"A finished video from one call: the AI writes the script (or narrates yours), voices it, builds every scene and cuts the MP4 — with captions and music if you ask. The request examples show the common setups.\n\n- **What it says** — `mode: \"topic\"` (the default) writes the script from your `prompt`; `mode: \"script\"` narrates your `prompt` as written.\n- **How it looks** — image scenes by default (stills with motion — the cheapest). Name a `settings.videoModel` and every scene renders as an AI-video clip, at `settings.resolution`. `settings.duration` is the total length in seconds; `settings.aspectRatio` the shape.\n- **Sound and words** — narration is on unless `settings.narration: false`. `music` and `captions` are off unless you set them, and captions are free here. Captions need narration: `captions` with `settings.narration: false` is a 400. If the music can't be made the job fails and every credit is released — you never get a silent video you paid to score.\n- **Your images** — `referenceImageUrls` (up to 5) guide style and subject. On AI-video scenes, `startFrameUrl` opens the first scene (and replaces the AI style-anchor image, which is then not billed) and `endFrameUrl` closes the last (models with `supportsEndFrame`). Frames-vs-references rules are per model — a combination it can't take is a 400 with the reason.\n\nThen poll `GET /jobs/{jobId}` until `completed` — `output.videoUrl` is the MP4. Price it first with `POST /video/estimate` — the same body, free. Just want one clip? Use `POST /media/video`. Pass `teamId` to create in another of your team workspaces (`GET /teams`); omit it for your default team. A team you’re not a member of is a 403.\n\n**Auth:** any valid API key. What it generates draws on the account’s credits — a plan’s, or pay-as-you-go top-ups.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"Only `prompt` is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoCreateRequest"},"examples":{"fromTopic":{"summary":"From a topic — image scenes, captioned","value":{"prompt":"Five surprising facts about octopuses","settings":{"aspectRatio":"9:16","duration":30},"captions":true}},"aiVideoScenes":{"summary":"Seedance 2.5 scenes at 1080p","value":{"prompt":"Why the Roman Colosseum is still standing after 2,000 years","settings":{"videoModel":"seedance-2.5","resolution":"1080p","duration":30,"aspectRatio":"9:16"},"captions":{"style":"viral-bangers-classic","density":"fast"}}},"ownScript":{"summary":"Your own script, narrated as written","value":{"mode":"script","prompt":"Most people think sleep is wasted time. It isn’t. While you sleep, your brain clears out the waste it made all day — and skipping that cleanup is why one bad night leaves you foggy.","settings":{"aspectRatio":"9:16"},"captions":true}},"productLaunch":{"summary":"Product launch — Omni 1.1 Flash scenes, music, captions","value":{"prompt":"Launch ad for the Ridgeline Pro hiking jacket — waterproof, featherlight, built for cold alpine starts. Fast cuts of trail life, end on the jacket and the tagline \"Made for the way up.\"","mode":"topic","settings":{"mediaMode":"video","videoModel":"gemini-omni-1.1-flash","aspectRatio":"9:16","duration":20},"referenceImageUrls":["https://cdn.ridgelinegear.com/ridgeline-pro/product.jpg"],"music":"Cinematic ambient bed at 74 BPM — a low string drone under sparse felt-piano notes, lifting through the middle and settling on one held chord, mixed low and clear of the voice.","captions":{"style":"viral-bangers-classic","density":"medium"}}}}}}},"responses":{"200":{"description":"Project created and credits reserved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"created":{"summary":"Product launch — Omni 1.1 Flash scenes, music, captions","value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","processType":"full_pipeline_video","creditsReserved":93,"creditsRemaining":407}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/video/estimate":{"post":{"operationId":"estimateVideo","tags":["Narrated videos"],"summary":"Estimate cost","description":"What `POST /video` would cost — the same body, free, nothing reserved. A balance that doesn’t cover it is still a 200 — `sufficient: false`, with the cost shown in full.\n\n`models` names the text, image and video models that will **actually run** — from the same estimate that bills, so it can’t drift from the charge. Narration is part of the quote unless you send `settings.narration: false`. Change any model under `settings` (`textModel`, `imageModel`, `videoModel` — see `GET /models`) and estimate again.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"The same body as `POST /video`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoCreateRequest"},"examples":{"fromTopic":{"summary":"From a topic — image scenes, captioned","value":{"prompt":"Five surprising facts about octopuses","settings":{"aspectRatio":"9:16","duration":30},"captions":true}},"aiVideoScenes":{"summary":"Seedance 2.5 scenes at 1080p","value":{"prompt":"Why the Roman Colosseum is still standing after 2,000 years","settings":{"videoModel":"seedance-2.5","resolution":"1080p","duration":30,"aspectRatio":"9:16"},"captions":{"style":"viral-bangers-classic","density":"fast"}}},"ownScript":{"summary":"Your own script, narrated as written","value":{"mode":"script","prompt":"Most people think sleep is wasted time. It isn’t. While you sleep, your brain clears out the waste it made all day — and skipping that cleanup is why one bad night leaves you foggy.","settings":{"aspectRatio":"9:16"},"captions":true}},"productLaunch":{"summary":"Product launch — Omni 1.1 Flash scenes, music, captions","value":{"prompt":"Launch ad for the Ridgeline Pro hiking jacket — waterproof, featherlight, built for cold alpine starts. Fast cuts of trail life, end on the jacket and the tagline \"Made for the way up.\"","mode":"topic","settings":{"mediaMode":"video","videoModel":"gemini-omni-1.1-flash","aspectRatio":"9:16","duration":20},"referenceImageUrls":["https://cdn.ridgelinegear.com/ridgeline-pro/product.jpg"],"music":"Cinematic ambient bed at 74 BPM — a low string drone under sparse felt-piano notes, lifting through the middle and settling on one held chord, mixed low and clear of the voice.","captions":{"style":"viral-bangers-classic","density":"medium"}}}}}}},"responses":{"200":{"description":"The cost (nothing is reserved).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"sufficient":{"summary":"Product launch — Omni 1.1 Flash scenes, music, captions","value":{"success":true,"data":{"sufficient":true,"processType":"full_pipeline_video","estimate":{"total":93,"breakdown":[{"action_type":"text_generation","model_id":"claude-sonnet-5-5","estimated_units":4,"credits_per_unit":2.75,"subtotal":11},{"action_type":"tts_generation","model_id":"eleven_flash_v2_5","estimated_units":0.5,"credits_per_unit":5.4,"subtotal":3},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.3038,"subtotal":2},{"action_type":"text_generation","model_id":"claude-sonnet-5-5","estimated_units":1,"credits_per_unit":1,"subtotal":1},{"action_type":"video_generation","model_id":"gemini-omni-1.1-flash","estimated_units":20,"credits_per_unit":3.15,"subtotal":63},{"action_type":"music_generation","model_id":"lyria-3.5","estimated_units":1,"credits_per_unit":10.8,"subtotal":11},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.4602,"subtotal":2}]},"models":[{"role":"text","id":"claude-sonnet-5-5","name":"Sonnet 5.5"},{"role":"image","id":"gpt-image-2.5-flare","name":"GPT Image 2.5 Flare"},{"role":"video","id":"gemini-omni-1.1-flash","name":"Omni 1.1 Flash"}],"balance":{"available":1200,"reserved":0,"used":480,"total":1680}}}},"insufficient":{"summary":"Balance too low","value":{"success":true,"data":{"sufficient":false,"reason":"insufficient_credits","error":"Your TurboClip balance doesn't cover this request: it needs 93 credits ($0.93) and 46 credits ($0.46) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":93,"estimate":{"total":93,"breakdown":[{"action_type":"text_generation","model_id":"claude-sonnet-5-5","estimated_units":4,"credits_per_unit":2.75,"subtotal":11},{"action_type":"tts_generation","model_id":"eleven_flash_v2_5","estimated_units":0.5,"credits_per_unit":5.4,"subtotal":3},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.3038,"subtotal":2},{"action_type":"text_generation","model_id":"claude-sonnet-5-5","estimated_units":1,"credits_per_unit":1,"subtotal":1},{"action_type":"video_generation","model_id":"gemini-omni-1.1-flash","estimated_units":20,"credits_per_unit":3.15,"subtotal":63},{"action_type":"music_generation","model_id":"lyria-3.5","estimated_units":1,"credits_per_unit":10.8,"subtotal":11},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.4602,"subtotal":2}]},"models":[{"role":"text","id":"claude-sonnet-5-5","name":"Sonnet 5.5"},{"role":"image","id":"gpt-image-2.5-flare","name":"GPT Image 2.5 Flare"},{"role":"video","id":"gemini-omni-1.1-flash","name":"Omni 1.1 Flash"}],"balance":{"available":46,"reserved":0,"used":120,"total":620},"processType":"full_pipeline_video"}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/video/{id}/result":{"get":{"operationId":"getVideoResult","tags":["Narrated videos"],"summary":"Get video","description":"The finished MP4 for a project. You rarely need it — `GET /jobs/{id}` already carries `output.videoUrl`.\n\n`videoUrl` is signed and expires after **24 hours** — download it, and fetch again rather than storing the link. While a job on the project is still running this answers 202 with that job’s `status` (`queued`, `processing` or `rendering`); a project with no video and nothing running answers `status: \"no_video\"`.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Ready (or failed — check `status`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"completed":{"value":{"success":true,"data":{"status":"completed","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","videoUrl":"https://…signed…"}}},"failed":{"summary":"Generation failed","value":{"success":true,"data":{"status":"failed","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","error":"Video generation failed"}}},"noVideo":{"summary":"Nothing rendered yet","value":{"success":true,"data":{"status":"no_video","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607"}}}}}}},"202":{"description":"Still generating or rendering — keep polling.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"rendering":{"value":{"success":true,"data":{"status":"rendering","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607"}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/video/{id}/render":{"post":{"operationId":"renderVideo","tags":["Narrated videos"],"summary":"Re-render video","description":"Stitches the project’s current timeline into a fresh MP4 — free. **You almost never need it:** every generation job stitches its own MP4, so its `jobId` reaches `completed` with `output.videoUrl` already there. Use it to re-render after a project's timeline has changed, or when `POST /ad/{id}/generate` reports nothing left to generate. Poll the returned job, then `GET /video/{id}/result`.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Render job started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"dispatched":{"value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","nextStep":"Poll GET /jobs/{jobId} until completed, then GET /video/{projectId}/result."}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"409":{"description":"The project is not in a state this step can act on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"No timeline to render"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad":{"post":{"operationId":"createAd","tags":["UGC ads"],"summary":"Create ad","description":"Brief in, finished ad out — one call, one job. The AI plans the shots with best-guess defaults (no questions), renders them, scores them and cuts the MP4.\n\n- **The presenter speaks** inside the clips — natively voiced and lip-synced, never a TTS narration track.\n- **Lock your real product or presenter** across every shot: `presenterImageUrl` becomes the opening frame, so no seed image is synthesized (or charged); `referenceImageUrls` are bound into the generated seed image, and each one is priced.\n- **Length and shots** — `settings.duration` in seconds (30 if omitted), `settings.shots` to pin the cut count, `settings.videoModel`, `settings.aspectRatio`, `settings.adType`.\n- **Music is on by default** — we write the brief from the finished shots. Pass your own brief as a string, `{ prompt?, volume? }` to also set the mix, or `music: false` for none. If the music can't be made the job fails and every credit is released.\n- **Captions are opt-in** — `captions: true` or `{ density?, style? }`. The clips are transcribed (ElevenLabs Scribe, billed per minute with a 1-minute minimum), so on an ad they cost a little.\n\nThen poll `GET /jobs/{jobId}` until `completed` — `output.videoUrl` is the MP4. Price it first with `POST /ad/estimate` — the same body, free; quote with the same `music` and `captions` you'll send. Want to review the plan before paying for clips? Use `POST /ad/plan`. Pass `teamId` to create in another of your team workspaces (`GET /teams`); omit it for your default team. A team you’re not a member of is a 403.\n\n**Auth:** any valid API key. What it generates draws on the account’s credits — a plan’s, or pay-as-you-go top-ups.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"Only `brief` is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdPlanRequest"},"examples":{"productAd":{"summary":"Product ad — your product shot, scored and captioned","value":{"brief":"Launch ad for the Ridgeline Pro hiking jacket. Follow one hiker from a dark trailhead to a sunrise summit. Waterproof, featherlight, built for cold alpine starts. End on the jacket and the tagline: Made for the way up.","settings":{"adType":"product","videoModel":"gemini-omni-1.1-flash","aspectRatio":"9:16","duration":20,"shots":2},"referenceImageUrls":["https://cdn.ridgelinegear.com/ridgeline-pro/product.jpg"],"music":true,"captions":true}},"presenterPhoto":{"summary":"Your presenter’s photo, 15 seconds","value":{"brief":"A 15-second testimonial for a habit-tracking app: a nurse coming off a night shift says it is the first app that ever stuck. End on the app icon.","settings":{"adType":"app","duration":15},"presenterImageUrl":"https://cdn.example.com/presenter.jpg"}},"briefOnly":{"summary":"Just a brief — every default","value":{"brief":"A UGC ad for my meal-prep delivery service: a busy parent shows a week of ready meals in their fridge and says it gave them their evenings back. End on the box."}}}}}},"responses":{"200":{"description":"Ad started. Credits for the whole ad are reserved up front — the most it can cost; anything the job couldn’t make is refunded when it settles.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"dispatched":{"summary":"Product ad — your product shot, scored and captioned","value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","processType":"full_pipeline_video","creditsReserved":85,"creditsRemaining":415,"nextStep":"Poll GET /jobs/{jobId} until completed — the response then carries output.videoUrl."}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad/estimate":{"post":{"operationId":"estimateAdCost","tags":["UGC ads"],"summary":"Estimate ad cost","description":"What `POST /ad` would cost — the same body, free, nothing reserved, and priced exactly as `POST /ad` reserves: a `presenterImageUrl` drops the seed-image line, and each reference image is priced. A balance that doesn’t cover it is still a 200 — `sufficient: false`, with the cost shown in full.\n\n**Use this, not `/video/estimate`**, which prices a narration track an ad never has (ad audio is native and lip-synced), so it over-quotes. To price an ad you've already planned, use `POST /ad/{id}/estimate`.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"The same body as `POST /ad`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdPlanRequest"},"examples":{"productAd":{"summary":"Product ad — your product shot, scored and captioned","value":{"brief":"Launch ad for the Ridgeline Pro hiking jacket. Follow one hiker from a dark trailhead to a sunrise summit. Waterproof, featherlight, built for cold alpine starts. End on the jacket and the tagline: Made for the way up.","settings":{"adType":"product","videoModel":"gemini-omni-1.1-flash","aspectRatio":"9:16","duration":20,"shots":2},"referenceImageUrls":["https://cdn.ridgelinegear.com/ridgeline-pro/product.jpg"],"music":true,"captions":true}},"presenterPhoto":{"summary":"Your presenter’s photo, 15 seconds","value":{"brief":"A 15-second testimonial for a habit-tracking app: a nurse coming off a night shift says it is the first app that ever stuck. End on the app icon.","settings":{"adType":"app","duration":15},"presenterImageUrl":"https://cdn.example.com/presenter.jpg"}},"briefOnly":{"summary":"Just a brief — every default","value":{"brief":"A UGC ad for my meal-prep delivery service: a busy parent shows a week of ready meals in their fridge and says it gave them their evenings back. End on the box."}}}}}},"responses":{"200":{"description":"The cost (nothing is reserved).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"sufficient":{"summary":"Product ad — your product shot, scored and captioned","value":{"success":true,"data":{"sufficient":true,"processType":"full_pipeline_video","estimate":{"total":85,"breakdown":[{"action_type":"text_generation","model_id":"claude-sonnet-5-5","estimated_units":1,"credits_per_unit":4,"subtotal":4},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.3038,"subtotal":2},{"action_type":"text_generation","model_id":"claude-sonnet-5-5","estimated_units":1,"credits_per_unit":1,"subtotal":1},{"action_type":"video_generation","model_id":"gemini-omni-1.1-flash","estimated_units":20,"credits_per_unit":3.15,"subtotal":63},{"action_type":"text_generation","model_id":"gemini-3-flash-preview","estimated_units":1,"credits_per_unit":1,"subtotal":1},{"action_type":"music_generation","model_id":"lyria-3.5","estimated_units":1,"credits_per_unit":10.8,"subtotal":11},{"action_type":"audio_transcription","model_id":"scribe_v2","estimated_units":1,"credits_per_unit":0.4995,"subtotal":1},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.4602,"subtotal":2}]},"models":[{"role":"text","id":"claude-sonnet-5-5","name":"Sonnet 5.5"},{"role":"text","id":"gemini-3-flash-preview","name":"Gemini 3 Flash"},{"role":"image","id":"gpt-image-2.5-flare","name":"GPT Image 2.5 Flare"},{"role":"video","id":"gemini-omni-1.1-flash","name":"Omni 1.1 Flash"}],"balance":{"available":500,"reserved":0,"used":120,"total":620}}}},"insufficient":{"summary":"Balance too low — the breakdown still shows the cost","value":{"success":true,"data":{"sufficient":false,"reason":"insufficient_credits","error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 42 credits ($0.42) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"estimate":{"total":85,"breakdown":[{"action_type":"text_generation","model_id":"claude-sonnet-5-5","estimated_units":1,"credits_per_unit":4,"subtotal":4},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.3038,"subtotal":2},{"action_type":"text_generation","model_id":"claude-sonnet-5-5","estimated_units":1,"credits_per_unit":1,"subtotal":1},{"action_type":"video_generation","model_id":"gemini-omni-1.1-flash","estimated_units":20,"credits_per_unit":3.15,"subtotal":63},{"action_type":"text_generation","model_id":"gemini-3-flash-preview","estimated_units":1,"credits_per_unit":1,"subtotal":1},{"action_type":"music_generation","model_id":"lyria-3.5","estimated_units":1,"credits_per_unit":10.8,"subtotal":11},{"action_type":"audio_transcription","model_id":"scribe_v2","estimated_units":1,"credits_per_unit":0.4995,"subtotal":1},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.4602,"subtotal":2}]},"models":[{"role":"text","id":"claude-sonnet-5-5","name":"Sonnet 5.5"},{"role":"text","id":"gemini-3-flash-preview","name":"Gemini 3 Flash"},{"role":"image","id":"gpt-image-2.5-flare","name":"GPT Image 2.5 Flare"},{"role":"video","id":"gemini-omni-1.1-flash","name":"Omni 1.1 Flash"}],"balance":{"available":42,"reserved":0,"used":120,"total":620}}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad/plan":{"post":{"operationId":"planAd","tags":["UGC ads"],"summary":"Plan ad","description":"Starts an ad you review before paying for clips — the same pre-flight the web app runs. The AI asks what it's unsure about, proposes a creative direction, then writes a shot list you can read and edit. Nothing renders until you call `POST /ad/{id}/generate`.\n\n```\nPOST /ad/plan  →  GET /ad/{id}/next  →  POST /ad/{id}/answers  →  (repeat)\n                        ↓ step: ready\n                  GET /ad/{id}/plan → POST /ad/{id}/generate\n```\n\n1. Poll `GET /jobs/{planJobId}` until `completed`, then `GET /ad/{id}/next` — it hands you questions.\n2. Put them to your user in their own words, send the replies to `POST /ad/{id}/answers`, and poll `next` again. The first round (framing) always asks — answer it, or send `{\"answers\": []}` to take every recommendation. A later round with no questions moves on by itself the next time you poll.\n3. At `step: \"ready\"`, read the shot list (`GET /ad/{id}/plan`), edit it if you like (`POST /ad/{id}/revise`), price it (`POST /ad/{id}/estimate`) and render it (`POST /ad/{id}/generate`).\n\nThe planner writes the visuals and the line your presenter speaks — both carried in each shot's `mediaDescription`, voiced natively and lip-synced. **Just want the video?** `POST /ad` does all of this with best-guess defaults.\n\n- **Cost** — an account that has never paid plans FREE (the \"taste\") against a lifetime allowance — a 402 with `reason: \"taste_limit\"` once it's used up; if it holds credits, its seed image is paid for when the shot list is made. Once an account has paid (a plan or a top-up), planning costs three small text calls — the framing here, then the Director and the shot list as `next` / `answers` move on — plus the seed image unless you send `presenterImageUrl`. Accounts that have paid are also checked up front for being able to afford the whole ad. The clips are paid for at generate.\n- **Length** — `settings.duration` bounds the plan (30 seconds if omitted).\n- **Shots** — `settings.shots` pins how many shots it cuts between. Omit it and the planner cuts the fewest, longest clips the model allows (a 20-second ad on Omni 1.1 Flash is two 10-second takes); a count the clip lengths can't fit is clamped rather than rejected.\n- **Shape** — `settings.adType` steers the planner; `settings.linkedMode: true` renders one seamless take; `endFrameUrl` is the frame the ad ends on (models with `supportsEndFrame` — frames-vs-references rules apply, a 400 with the reason).\n- **Music and captions** are set here: `POST /ad/{id}/generate` takes no body.\n- **Lock your real product or presenter** across every shot: `presenterImageUrl` becomes the opening frame, so no seed image is synthesized (or charged); `referenceImageUrls` are bound into the generated seed image, and each one is priced.\n- Pass `teamId` to create in another of your team workspaces (`GET /teams`); omit it for your default team. A team you’re not a member of is a 403.\n\n**Auth:** any valid API key. Free for an account that has never paid, within its lifetime free ad-plan allowance (the \"taste\"); otherwise it draws on credits.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"Only `brief` is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdPlanRequest"},"examples":{"withProductShot":{"summary":"15 seconds, with your product shot","value":{"brief":"A 15-second UGC ad for my cold brew coffee subscription — energetic, morning-routine vibe, end on the product.","settings":{"duration":15,"adType":"product"},"presenterImageUrl":"https://example.com/product-shot.png"}},"briefOnly":{"summary":"Just a brief — every default","value":{"brief":"A UGC ad for my meal-prep delivery service: a busy parent shows a week of ready meals in their fridge and says it gave them their evenings back. End on the box."}}}}}},"responses":{"200":{"description":"Planning started (the framing step). A taster that has never paid plans free; an account that has paid reserves the framing call here — the Director and shot-list calls are reserved as `next` / `answers` advance them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"taster":{"summary":"Never paid — plans free","value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","planJobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","creditsReserved":0,"creditsRemaining":500,"nextStep":"Poll GET /jobs/{planJobId} until completed, then GET /ad/{projectId}/next — it hands you the questions to answer, and advances the plan as you go."}}},"subscriber":{"summary":"Has paid, with a product shot supplied (no seed image)","value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","planJobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","creditsReserved":2,"creditsRemaining":498,"nextStep":"Poll GET /jobs/{planJobId} until completed, then GET /ad/{projectId}/next — it hands you the questions to answer, and advances the plan as you go."}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad/{id}/next":{"get":{"operationId":"getAdNext","tags":["UGC ads"],"summary":"Get next step","description":"The one poll for a planned ad: what the AI needs from you now. Put it to your user, send the reply to `POST /ad/{id}/answers`, poll again.\n\n`step` is one of:\n- `processing` — a model is thinking; poll again in 5–10 seconds\n- `framing` — open questions about the brief (`suggestions` are one-tap answers). This round always asks.\n- `director` — a proposed creative `direction`, plus the axes it's genuinely unsure about (each question carries `options` and a `recommendedOptionIndex`)\n- `ready` — the shot list exists: `GET /ad/{id}/plan`, then `POST /ad/{id}/generate`\n- `failed` — planning couldn't get going (no step has succeeded yet); start again with `POST /ad/plan`. If a later step fails, `next` re-asks that round or retries it instead.\n\n**You never need to know the sequence.** A Director round with no questions moves on by itself the next time you poll. This GET also moves the flow forward: for an account that has paid, the Director and shot-list steps it starts reserve their (small) cost.\n\n**Auth:** any valid API key. Free for an account that has never paid, within its lifetime free ad-plan allowance (the \"taste\"); otherwise it draws on credits.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId from POST /ad/plan."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Where the conversation is.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"framing":{"summary":"Open questions about the brief","value":{"success":true,"data":{"step":"framing","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","questions":[{"id":"q1","prompt":"Who is this jacket for — alpine mountaineers, or weekend hikers who feel the cold?","suggestions":[{"label":"Weekend hikers who feel the cold"},{"label":"Serious alpine mountaineers"}]}],"nextStep":"Answer with POST /ad/{projectId}/answers — {\"answers\":[{\"questionId\":\"…\",\"answer\":\"…\"}]}."}}},"director":{"summary":"A proposed direction + the axes it is unsure about","value":{"success":true,"data":{"step":"director","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","direction":{"formatArchetype":"presenter-led","voiceMode":"lip-sync","subjectAnchor":"person","resolvedAdType":"product","coreAngle":"Warmth without bulk — the jacket you forget you are wearing","tone":"Understated, earned confidence","proofType":"live-result","slotPlan":[{"beat":"Hook","role":"generate","label":"Cold trailhead"}]},"questions":[{"id":"d1","axis":"proofType","prompt":"How should the ad prove the claim?","options":[{"label":"Show it in brutal conditions","value":"live-result"},{"label":"A customer testimonial","value":"testimonial"}],"recommendedOptionIndex":0,"impact":0.8,"uncertainty":0.6}],"nextStep":"Answer with POST /ad/{projectId}/answers; omit a question to accept the recommendation."}}},"ready":{"summary":"Shot list ready to review + generate","value":{"success":true,"data":{"step":"ready","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","nextStep":"GET /ad/{projectId}/plan to review, then POST /ad/{projectId}/generate to render."}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad/{id}/answers":{"post":{"operationId":"answerAdQuestions","tags":["UGC ads"],"summary":"Answer questions","description":"Reply to whatever `GET /ad/{id}/next` just asked — the same shape for both rounds.\n\n- **Framing** answers are free text (a suggestion's `label` is a perfectly good answer). They're added to the brief the Director plans from.\n- **Director** answers are one of that question's `options[].value` — except the free-text axes (`coreAngle`, `tone`), which take prose. They're applied to the proposed direction for you; you never rebuild the direction object.\n\nLeave a question out to accept the AI's recommendation, or send `{\"answers\": []}` to accept them all. Returns the new state, so you can go straight back to polling.\n\n**Auth:** any valid API key. Free for an account that has never paid, within its lifetime free ad-plan allowance (the \"taste\"); otherwise it draws on credits.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId."},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"description":"Answers keyed by the question ids from GET /ad/{id}/next.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdAnswersRequest"},"examples":{"answer":{"summary":"Answer a question","value":{"answers":[{"questionId":"q1","answer":"Weekend hikers who feel the cold, not alpinists"}]}},"acceptAll":{"summary":"Take every recommendation","value":{"answers":[]}}}}}},"responses":{"200":{"description":"Answers applied; the flow moved on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"advanced":{"value":{"success":true,"data":{"step":"processing","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","nextStep":"Poll GET /ad/{projectId}/next again in 5–10 seconds."}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad/{id}/plan":{"get":{"operationId":"getAdPlan","tags":["UGC ads"],"summary":"Get shot list","description":"The shot list to review (with your user, if you're an agent) before generating. Each shot's `mediaDescription` is what it renders: the visual direction, then the line the presenter speaks (a `Spoken aloud by the person on camera, lip-synced: \"…\"` block, voiced natively). `dialogue` is reserved and currently always `null`. `placeholder: true` shots are reserved footage slots the user fills (`placeholderLabel` says with what) — they are never billed.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId from POST /ad/plan."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The shot list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"planned":{"value":{"success":true,"data":{"status":"planned","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","shots":[{"sceneIndex":0,"beatName":"Hook","sceneRole":"hook","mediaDescription":"Presenter holds the cold brew bottle to camera in a bright kitchen, morning light.\nSpoken aloud by the person on camera, lip-synced: \"I swapped my coffee run for this.\"","dialogue":null,"durationSeconds":4,"type":"video","placeholder":false,"placeholderLabel":null,"hasMedia":false}]}}}}}}},"202":{"description":"No shot list yet. `status` is the running step’s (`queued`, `processing` or `rendering`), or `pending` when the flow is waiting on you — poll `GET /ad/{id}/next`, which hands you the questions and moves the plan on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"running":{"value":{"success":true,"data":{"status":"processing","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","shots":[]}}},"waiting":{"summary":"Waiting on your answers","value":{"success":true,"data":{"status":"pending","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","shots":[]}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad/{id}/revise":{"post":{"operationId":"reviseAdPlan","tags":["UGC ads"],"summary":"Edit shots","description":"Rewrite shots before generating — free. Your `mediaDescription` REPLACES the shot's whole direction, the presenter's spoken line (`Spoken aloud…`) included, so carry that block over unchanged unless you mean to change what is said.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId."},"required":true,"name":"id","in":"path"}],"requestBody":{"required":true,"description":"Shot edits keyed by sceneIndex.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdReviseRequest"},"examples":{"reviseShot":{"summary":"Rewrite one shot","value":{"shots":[{"sceneIndex":1,"mediaDescription":"Close-up pour over ice in a glass tumbler, condensation visible, warm kitchen light"}]}}}}}},"responses":{"200":{"description":"Edits applied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"revised":{"value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","shotsUpdated":1}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad/{id}/estimate":{"post":{"operationId":"estimateAd","tags":["UGC ads"],"summary":"Estimate render cost","description":"What `POST /ad/{id}/generate` would cost for a planned ad — free, no body: it's priced from the stored plan. Only ungenerated, non-placeholder shots are priced; when every shot already has media it answers `alreadyGenerated: true` with a zero total. **Use this, not `/video/estimate`**, which would price a narration track an ad never has. (The example prices clips only, as for an ad planned with `music: false`; a scored ad — the default — adds its music lines.)\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The cost.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"sufficient":{"value":{"success":true,"data":{"sufficient":true,"estimate":{"total":245,"breakdown":[{"action_type":"video_generation","model_id":"kling-v3","estimated_units":15,"credits_per_unit":16.296,"subtotal":245}]},"models":[{"role":"video","id":"kling-v3","name":"Kling V3"}],"balance":{"available":1500,"reserved":12,"used":120,"total":1632},"shotsToGenerate":4}}},"insufficient":{"summary":"Balance too low","value":{"success":true,"data":{"sufficient":false,"reason":"insufficient_credits","error":"Your TurboClip balance doesn't cover this request: it needs 245 credits ($2.45) and 122 credits ($1.22) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":245,"estimate":{"total":245,"breakdown":[{"action_type":"video_generation","model_id":"kling-v3","estimated_units":15,"credits_per_unit":16.296,"subtotal":245}]},"models":[{"role":"video","id":"kling-v3","name":"Kling V3"}],"balance":{"available":122,"reserved":0,"used":120,"total":620},"shotsToGenerate":4}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"409":{"description":"The project is not in a state this step can act on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"No timeline to render"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/ad/{id}/generate":{"post":{"operationId":"generateAd","tags":["UGC ads"],"summary":"Render ad","description":"Render the approved shot list — the billable step. No body: everything comes from the stored plan. Only ungenerated shots are charged, so once a run has finished, calling again charges nothing for the shots it made; don't call again while a run is still going, or its shots are reserved twice.\n\nThen poll `GET /jobs/{jobId}` until `completed`: the clips AND the final stitched MP4 are made in that one job, so the response carries `output.videoUrl`.\n\n**Auth:** any valid API key. What it generates draws on the account’s credits — a plan’s, or pay-as-you-go top-ups.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Render started (or nothing left to render).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"dispatched":{"value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","creditsReserved":245,"creditsRemaining":1255,"nextStep":"Poll GET /jobs/{jobId} until completed — the response then carries output.videoUrl."}}},"nothingToDo":{"summary":"All shots already generated","value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":null,"creditsReserved":0,"nothingToDo":true,"message":"Every shot already has media. If you still need the MP4, POST /video/{projectId}/render (free)."}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"409":{"description":"The project is not in a state this step can act on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"No timeline to render"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/slideshow":{"post":{"operationId":"createSlideshow","tags":["Slideshows"],"summary":"Create slideshow","description":"Slides from your copy: you write each slide's `title` and `body`, TurboClip makes the images.\n\n- **The images** — describe each one in `imageDescription`, or leave it off any slide and the AI art-directs them all (one extra text call).\n- **The same person on every slide** — `characterLock.imageDescription` (adds one reference image). Be specific — age, build, hair, skin, eyes, clothing, accessories, vibe — and mark the slides they appear on with `hasCharacter: true`.\n- **Your own images** — `slides[].referenceImage` composites an image INTO a slide (an app screenshot on a phone, say) and takes the character lock's place there; `slides[].imageOverlay` lays a PNG ON TOP (a branded end card — leave `title`/`body` off). They follow `slideNumber`, so number slides by position (1, 2, 3…).\n- **The look and the price** — `settings.imageModel`, `imageSize`, `imageQuality`, `aspectRatio` (and `textModel` when the AI art-directs). Unlike video and media, slideshow settings aren't checked against the model's capabilities. `settings.style` must be a name from `GET /styles`.\n\nThen poll `GET /jobs/{jobId}` until `completed` — `output.slides` carries the images (`GET /slideshow/{id}/assets` returns the same). Price it first with `POST /slideshow/estimate` — the same body, free. Pass `teamId` to create in another of your team workspaces (`GET /teams`); omit it for your default team. A team you’re not a member of is a 403.\n\n**Auth:** any valid API key. What it generates draws on the account’s credits — a plan’s, or pay-as-you-go top-ups.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"`title` and `slides` are required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlideshowCreateRequest"},"examples":{"characterLock":{"summary":"The same person on every slide","value":{"title":"5 Mindset Shifts That Unlock Six-Figure Coaching","characterLock":{"imageDescription":"A confident business coach in her late 30s with shoulder-length auburn hair, warm hazel eyes, light freckles, wearing a tailored cream blazer over a white tee and gold hoop earrings. Polished but approachable — the mentor you actually trust, not a stock-photo executive."},"slides":[{"slideNumber":1,"title":"Stop selling hours.","body":"Your calendar is not your product. Your transformation is.","imageDescription":"The coach mid-gesture at a whiteboard with a simple upward arrow, bright modern studio office, candid coaching-session energy","hasCharacter":true},{"slideNumber":2,"title":"Raise the bar, not the volume.","body":"Ten dream clients beat a hundred maybes. Positioning does the filtering.","imageDescription":"The coach reviewing a shortlist on a tablet at a sunlit desk, calm and decisive, warm morning light","hasCharacter":true},{"slideNumber":3,"title":"Ready to shift?","body":"DM “SHIFT” and I’ll send you the framework my clients use.","imageDescription":"Close-up of a clean notebook page with the handwritten word SHIFT circled in gold ink, coffee cup beside it, soft depth of field","hasCharacter":false}],"settings":{"aspectRatio":"9:16","style":"#photoreal"}}},"copyOnly":{"summary":"Just your copy — the AI art-directs","value":{"title":"3 Pantry Swaps That Cut Sugar","slides":[{"slideNumber":1,"title":"Swap 1: flavoured yoghurt","body":"Plain Greek yoghurt + berries. Same creaminess, a fraction of the sugar."},{"slideNumber":2,"title":"Swap 2: granola","body":"Toast your own oats with cinnamon. Sweet without the syrup."},{"slideNumber":3,"title":"Swap 3: soft drinks","body":"Sparkling water with citrus. The fizz is the habit, not the sugar."}],"settings":{"aspectRatio":"9:16"}}}}}}},"responses":{"200":{"description":"Project created and credits reserved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"created":{"summary":"The same person on every slide","value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","processType":"slideshow_agent_pipeline","creditsReserved":9,"creditsRemaining":491}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/slideshow/estimate":{"post":{"operationId":"estimateSlideshow","tags":["Slideshows"],"summary":"Estimate cost","description":"What `POST /slideshow` would cost — the same body, free, nothing reserved. A balance that doesn’t cover it is still a 200 — `sufficient: false`, with the cost shown in full. An `imageDescription` on every slide drops the text line; `characterLock` adds one reference image.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"The same body as `POST /slideshow`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SlideshowCreateRequest"},"examples":{"characterLock":{"summary":"The same person on every slide","value":{"title":"5 Mindset Shifts That Unlock Six-Figure Coaching","characterLock":{"imageDescription":"A confident business coach in her late 30s with shoulder-length auburn hair, warm hazel eyes, light freckles, wearing a tailored cream blazer over a white tee and gold hoop earrings. Polished but approachable — the mentor you actually trust, not a stock-photo executive."},"slides":[{"slideNumber":1,"title":"Stop selling hours.","body":"Your calendar is not your product. Your transformation is.","imageDescription":"The coach mid-gesture at a whiteboard with a simple upward arrow, bright modern studio office, candid coaching-session energy","hasCharacter":true},{"slideNumber":2,"title":"Raise the bar, not the volume.","body":"Ten dream clients beat a hundred maybes. Positioning does the filtering.","imageDescription":"The coach reviewing a shortlist on a tablet at a sunlit desk, calm and decisive, warm morning light","hasCharacter":true},{"slideNumber":3,"title":"Ready to shift?","body":"DM “SHIFT” and I’ll send you the framework my clients use.","imageDescription":"Close-up of a clean notebook page with the handwritten word SHIFT circled in gold ink, coffee cup beside it, soft depth of field","hasCharacter":false}],"settings":{"aspectRatio":"9:16","style":"#photoreal"}}},"copyOnly":{"summary":"Just your copy — the AI art-directs","value":{"title":"3 Pantry Swaps That Cut Sugar","slides":[{"slideNumber":1,"title":"Swap 1: flavoured yoghurt","body":"Plain Greek yoghurt + berries. Same creaminess, a fraction of the sugar."},{"slideNumber":2,"title":"Swap 2: granola","body":"Toast your own oats with cinnamon. Sweet without the syrup."},{"slideNumber":3,"title":"Swap 3: soft drinks","body":"Sparkling water with citrus. The fizz is the habit, not the sugar."}],"settings":{"aspectRatio":"9:16"}}}}}}},"responses":{"200":{"description":"The cost (nothing is reserved).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"sufficient":{"summary":"Every slide carries an imageDescription (no text line); characterLock adds one reference image","value":{"success":true,"data":{"sufficient":true,"estimate":{"total":9,"breakdown":[{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.3038,"subtotal":2},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":3,"credits_per_unit":1.3038,"subtotal":4},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":2,"credits_per_unit":1.4602,"subtotal":3}]},"models":[{"role":"image","id":"gpt-image-2.5-flare","name":"GPT Image 2.5 Flare"}],"balance":{"available":500,"reserved":0,"used":120,"total":620},"slideCount":3}}},"insufficient":{"summary":"Balance too low","value":{"success":true,"data":{"sufficient":false,"reason":"insufficient_credits","error":"Your TurboClip balance doesn't cover this request: it needs 9 credits ($0.09) and 4 credits ($0.04) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":9,"estimate":{"total":9,"breakdown":[{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":1,"credits_per_unit":1.3038,"subtotal":2},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":3,"credits_per_unit":1.3038,"subtotal":4},{"action_type":"image_generation","model_id":"gpt-image-2.5-flare","estimated_units":2,"credits_per_unit":1.4602,"subtotal":3}]},"models":[{"role":"image","id":"gpt-image-2.5-flare","name":"GPT Image 2.5 Flare"}],"balance":{"available":4,"reserved":0,"used":120,"total":620},"slideCount":3}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/slideshow/{id}/assets":{"get":{"operationId":"getSlideshowAssets","tags":["Slideshows"],"summary":"Get slides","description":"The finished slides, in order. `url` is the generated background image; `texts` is the slide’s text, laid over it rather than baked in. Image URLs are signed and expire after **24 hours** — download them, and fetch again rather than storing the links.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The projectId."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Slides ready (or failed — check `status`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"completed":{"value":{"success":true,"data":{"status":"completed","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","config":{"width":1080,"height":1920},"slides":[{"order":0,"url":"https://…signed…","description":"The coach mid-gesture at a whiteboard…","texts":["Stop selling hours.","Your calendar is not your product. Your transformation is."],"imageOverlays":[]}]}}}}}}},"202":{"description":"Still generating — keep polling.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"processing":{"value":{"success":true,"data":{"status":"processing","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","slides":[]}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/captions":{"post":{"operationId":"captionVideo","tags":["Captions"],"summary":"Caption a video","description":"Burn captions into a video you already have. Send `videoUrl` — a public link to an MP4, MOV or WebM with speech (up to 10 minutes and 500.0 MB), or the `url` from the upload flow for a file on your computer — and, if you like, a `captions` look (see **Caption styles**). We read the video's length from its header, transcribe it, break the words into captions and render one MP4.\n\n- **Cost** — transcription and caption phrasing by the minute, plus the render by the minute. Price it first with `POST /captions/estimate` — the same body, free.\n- **Shape** — the MP4 keeps your video's shape, at the closest frame we render (9:16, 16:9, 1:1, 4:5…); a source between those is cropped to fill it.\n- **Sound** — your video's own audio, untouched.\n- **Edit it later** — `studioUrl` opens the project in the app, where every caption can be edited and exported again for free.\n\nThen poll `GET /jobs/{jobId}` until `completed` — `output.videoUrl` is the MP4. A video with no speech fails its job and releases every credit. Pass `teamId` to create in another of your team workspaces (`GET /teams`); omit it for your default team. A team you’re not a member of is a 403.\n\n**Auth:** any valid API key. What it generates draws on the account’s credits — a plan’s, or pay-as-you-go top-ups.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"Only `videoUrl` is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptionsRequest"},"examples":{"houseStyle":{"summary":"A video link, house style","value":{"videoUrl":"https://cdn.ridgelinegear.com/founder-talk.mp4"}},"preset":{"summary":"A preset, punchy","value":{"videoUrl":"https://cdn.ridgelinegear.com/founder-talk.mp4","captions":{"style":"viral-bangers-classic","density":"fast"}}},"custom":{"summary":"Every setting","value":{"videoUrl":"https://cdn.ridgelinegear.com/founder-talk.mp4","title":"Founder talk — captioned","captions":{"style":"default","density":"medium","position":"top","font":"Bebas Neue","size":"large","bold":true,"italic":false,"textCase":"uppercase","textColor":"#FFFFFF","highlight":"word","highlightColor":"#00E5FF","outlineWidth":4,"outlineColor":"#000000","shadow":2,"background":"none","backgroundColor":"rgba(0,0,0,0.5)","animation":"pop","language":"en"}}},"uploaded":{"summary":"A file you uploaded","value":{"videoUrl":"https://abcd.supabase.co/storage/v1/object/sign/media/users/…/media-library/1720000000-a1b2c3.mp4?token=…","captions":{"style":"minimal-simple"}}}}}}},"responses":{"200":{"description":"Project created and credits reserved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"created":{"summary":"Every setting","value":{"success":true,"data":{"projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","processType":"add_captions","creditsReserved":9,"creditsRemaining":491,"source":{"durationSeconds":45,"width":1080,"height":1920,"aspectRatio":"9:16"},"studioUrl":"https://platform.turboclip.ai/project/3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607/timeline","nextStep":"Poll GET /jobs/{jobId} until completed — output.videoUrl is the captioned MP4. studioUrl opens the project to fine-tune the captions."}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"402":{"description":"Not enough credits — the reservation could not be made. `error` says by how much and where to top up (Account → Billing); `topUpUrl` opens it with the top-up that covers the shortfall; `required` and `available` carry the numbers. `reason: \"payment_failed\"` is a declined card (update it there); `reason: \"taste_limit\"` is an account that never paid, out of its lifetime free-plan allowance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditError"},"example":{"success":false,"error":"Your TurboClip balance doesn't cover this request: it needs 85 credits ($0.85) and 21 credits ($0.21) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":85,"available":21,"reason":"insufficient_credits"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"413":{"description":"Uploaded file exceeds the size cap (it is removed from storage).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"File exceeds the 500 MB upload limit and was removed."}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/captions/estimate":{"post":{"operationId":"estimateCaptions","tags":["Captions"],"summary":"Estimate cost","description":"What `POST /captions` would cost — the same body, free, nothing reserved. It reads the video the same way, so a link that can't be read fails here too.\n\nIt also shows what it read and what you'll get: `source` is the video's length and frame, and `captions` is the full look your request resolves to, in the request's own fields — change any of them and estimate again. A balance that doesn’t cover it is still a 200 — `sufficient: false`, with the cost shown in full.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"The same body as `POST /captions`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CaptionsRequest"},"examples":{"houseStyle":{"summary":"A video link, house style","value":{"videoUrl":"https://cdn.ridgelinegear.com/founder-talk.mp4"}},"preset":{"summary":"A preset, punchy","value":{"videoUrl":"https://cdn.ridgelinegear.com/founder-talk.mp4","captions":{"style":"viral-bangers-classic","density":"fast"}}},"custom":{"summary":"Every setting","value":{"videoUrl":"https://cdn.ridgelinegear.com/founder-talk.mp4","title":"Founder talk — captioned","captions":{"style":"default","density":"medium","position":"top","font":"Bebas Neue","size":"large","bold":true,"italic":false,"textCase":"uppercase","textColor":"#FFFFFF","highlight":"word","highlightColor":"#00E5FF","outlineWidth":4,"outlineColor":"#000000","shadow":2,"background":"none","backgroundColor":"rgba(0,0,0,0.5)","animation":"pop","language":"en"}}},"uploaded":{"summary":"A file you uploaded","value":{"videoUrl":"https://abcd.supabase.co/storage/v1/object/sign/media/users/…/media-library/1720000000-a1b2c3.mp4?token=…","captions":{"style":"minimal-simple"}}}}}}},"responses":{"200":{"description":"The cost (nothing is reserved).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"sufficient":{"summary":"Every setting","value":{"success":true,"data":{"sufficient":true,"processType":"add_captions","estimate":{"total":9,"breakdown":[{"action_type":"audio_transcription","model_id":"scribe_v2","estimated_units":1,"credits_per_unit":0.4995,"subtotal":1},{"action_type":"text_generation","model_id":"gemini-2.5-flash","estimated_units":1,"credits_per_unit":1,"subtotal":1},{"action_type":"video_render","model_id":"turboclip_render","estimated_units":1,"credits_per_unit":6.75,"subtotal":7}]},"models":[{"role":"text","id":"gemini-2.5-flash","name":"gemini-2.5-flash"}],"source":{"durationSeconds":45,"width":1080,"height":1920,"aspectRatio":"9:16"},"captions":{"style":"default","density":"medium","position":"top","font":"Bebas Neue","size":92,"bold":true,"italic":false,"textCase":"uppercase","textColor":"#FFFFFF","highlight":"word","highlightColor":"#00E5FF","outlineWidth":4,"outlineColor":"#000000","shadow":2,"glow":0,"glowColor":"#FFFFFF","background":"none","backgroundColor":"rgba(0,0,0,0.5)","animation":"pop","language":"en"},"balance":{"available":500,"reserved":0,"used":120,"total":620}}}},"insufficient":{"summary":"Balance too low","value":{"success":true,"data":{"sufficient":false,"reason":"insufficient_credits","error":"Your TurboClip balance doesn't cover this request: it needs 9 credits ($0.09) and 4 credits ($0.04) are available. Top it up in Account → Billing: https://platform.turboclip.ai/account/billing?topup=5","topUpUrl":"https://platform.turboclip.ai/account/billing?topup=5","required":9,"estimate":{"total":9,"breakdown":[{"action_type":"audio_transcription","model_id":"scribe_v2","estimated_units":1,"credits_per_unit":0.4995,"subtotal":1},{"action_type":"text_generation","model_id":"gemini-2.5-flash","estimated_units":1,"credits_per_unit":1,"subtotal":1},{"action_type":"video_render","model_id":"turboclip_render","estimated_units":1,"credits_per_unit":6.75,"subtotal":7}]},"models":[{"role":"text","id":"gemini-2.5-flash","name":"gemini-2.5-flash"}],"balance":{"available":4,"reserved":0,"used":120,"total":620},"processType":"add_captions","source":{"durationSeconds":45,"width":1080,"height":1920,"aspectRatio":"9:16"},"captions":{"style":"default","density":"medium","position":"top","font":"Bebas Neue","size":92,"bold":true,"italic":false,"textCase":"uppercase","textColor":"#FFFFFF","highlight":"word","highlightColor":"#00E5FF","outlineWidth":4,"outlineColor":"#000000","shadow":2,"glow":0,"glowColor":"#FFFFFF","background":"none","backgroundColor":"rgba(0,0,0,0.5)","animation":"pop","language":"en"}}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"413":{"description":"Uploaded file exceeds the size cap (it is removed from storage).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"File exceeds the 500 MB upload limit and was removed."}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/media/upload-url":{"post":{"operationId":"createUploadUrl","tags":["File uploads"],"summary":"Get upload URL","description":"Step 1 of 2 for using a LOCAL file — a reference image, a start or end frame, a presenter photo, a clip — without hosting it anywhere public. Free.\n\nAccepted: images (.png .jpg .jpeg .webp; .gif uploads but isn't usable as a model reference, and avif/heic are rejected because the AI models refuse them), any video type (.mp4 .webm .mov…) and any audio type (.mp3 .wav .m4a .aac .ogg…). A `contentType` you send wins over the file's extension.\n\nThen PUT the raw bytes to `uploadUrl` — **no auth headers**: the URL is pre-authorized (~2h), and the bytes go straight to storage (large videos are fine):\n\n```bash\ncurl -X PUT -H \"Content-Type: image/png\" --data-binary @product-shot.png \"<uploadUrl>\"\n```\n\nFinish with `POST /media/upload-complete`.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"The file name (its extension resolves the type when contentType is omitted).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlRequest"},"examples":{"image":{"summary":"A product photo","value":{"fileName":"ridgeline-pro-product.jpg"}}}}}},"responses":{"200":{"description":"Where to PUT the file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"minted":{"value":{"success":true,"data":{"uploadUrl":"https://…supabase…/upload/sign/…?token=…","storagePath":"media/users/…/media-library/1720000000-a1b2c3.jpg","method":"PUT","headers":{"Content-Type":"image/jpeg"},"expiresInSeconds":7200,"maxFileBytes":524288000,"nextStep":"PUT the RAW file bytes to uploadUrl (no auth headers), then POST /media/upload-complete with storagePath."}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/media/upload-complete":{"post":{"operationId":"completeUpload","tags":["File uploads"],"summary":"Complete upload","description":"Step 2 of 2: checks the file actually landed (404 if the PUT hasn’t happened), enforces the size limit and stored content type, adds the file to your media library, and returns a signed `url` you can pass as `referenceImageUrls`, `startFrameUrl`, `endFrameUrl` or `presenterImageUrl` on any generate call — or, for a video, as `videoUrl` on `POST /captions`. Free.\n\n**Safe to repeat** — call again with the same `storagePath` for a fresh `url` (links expire after 24h); a repeat answers `alreadyRegistered: true` and keeps the file in the team it was first added to. Video, ad and media requests take what they need from the link when you submit, so it expiring later doesn't matter; a slideshow fetches its slide images when its job runs (normally straight away), so start it while the link is fresh. Pass `teamId` to create in another of your team workspaces (`GET /teams`); omit it for your default team. A team you’re not a member of is a 403.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"requestBody":{"required":true,"description":"`storagePath` exactly as returned by /media/upload-url.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadCompleteRequest"},"examples":{"complete":{"summary":"Finish the upload","value":{"storagePath":"media/users/…/media-library/1720000000-a1b2c3.jpg","fileName":"ridgeline-pro-product.jpg"}}}}}},"responses":{"200":{"description":"File added (or already added).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"registered":{"value":{"success":true,"data":{"mediaId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","url":"https://…signed…","urlExpiresInSeconds":86400,"storagePath":"media/users/…/media-library/1720000000-a1b2c3.jpg","fileName":"ridgeline-pro-product.jpg","mimeType":"image/jpeg","sizeBytes":482133,"nextStep":"Pass url as referenceImageUrls / startFrameUrl / endFrameUrl / presenterImageUrl on any generate call, or a video's url as videoUrl on POST /captions."}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"403":{"description":"Not allowed — a `teamId` this account isn’t a member of (`GET /teams` lists yours).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierGateError"},"example":{"success":false,"error":"Not a member of this team"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"413":{"description":"Uploaded file exceeds the size cap (it is removed from storage).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"File exceeds the 500 MB upload limit and was removed."}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/jobs/{id}":{"get":{"operationId":"getJob","tags":["Jobs"],"summary":"Get job","description":"**The only thing you need to poll.** Status flow: `draft` → `queued` → `processing` → `rendering` → `completed` | `failed`.\n\nOn `completed` the response carries **`output`** — the finished deliverable, by `type`:\n- `{ type: \"media\", mode, url }` — an image or a clip\n- `{ type: \"video\", videoUrl }` — a narrated video or an ad\n- `{ type: \"slideshow\", slides: [...] }` — slides\n\nVideos stitch their final MP4 inside the job, so `completed` always means the file exists — no render call. On `failed` it carries **`failure`**: `message`, a `reason`, `retryable` (false when only a change to your request can help — a content-policy refusal, say) and `creditsReleased` (all of the reservation: a failed job costs nothing).\n\n`output` URLs are signed and expire: fetch the job again rather than storing them. `creditsConsumed` is filled in when the job settles — at most `creditsReserved`, less when part of the work couldn't be made. Poll every 5–10 seconds; polls count toward the rate limit.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The jobId from any create call."},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The job — with `output` once it has completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"completedMedia":{"summary":"Completed image — the file is right here","value":{"success":true,"data":{"jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","projectId":null,"processType":"single_image_only","status":"completed","error":null,"creditsReserved":2,"creditsConsumed":2,"createdAt":"2026-07-03T10:00:00Z","updatedAt":"2026-07-03T10:00:22Z","output":{"type":"media","mode":"image","url":"https://…/cold-brew-bottle.png"}}}},"completedVideo":{"summary":"Completed video or ad — the MP4 is right here","value":{"success":true,"data":{"jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","processType":"full_pipeline_video","status":"completed","error":null,"creditsReserved":85,"creditsConsumed":85,"createdAt":"2026-07-03T10:00:00Z","updatedAt":"2026-07-03T10:06:12Z","output":{"type":"video","videoUrl":"https://…/ridgeline-pro-launch.mp4"}}}},"failed":{"summary":"Failed — nothing charged","value":{"success":true,"data":{"jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","processType":"full_pipeline_video","status":"failed","error":"Your request was declined by the AI model’s content/safety filter — usually a real person, brand or logo it can’t depict, a flagged subject, or sensitive content. Try rephrasing or removing it. Your credits have been refunded.","failure":{"message":"Your request was declined by the AI model’s content/safety filter — usually a real person, brand or logo it can’t depict, a flagged subject, or sensitive content. Try rephrasing or removing it. Your credits have been refunded.","reason":"content_policy","retryable":false,"creditsReleased":85},"creditsReserved":85,"creditsConsumed":0,"createdAt":"2026-07-03T10:00:00Z","updatedAt":"2026-07-03T10:01:20Z"}}},"rendering":{"summary":"Clips are made; the final cut is being stitched","value":{"success":true,"data":{"jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","processType":"full_pipeline_video","status":"rendering","error":null,"creditsReserved":85,"creditsConsumed":null,"createdAt":"2026-07-03T10:00:00Z","updatedAt":"2026-07-03T10:05:03Z"}}},"completedSlideshow":{"summary":"Completed slideshow — slides instead of a video","value":{"success":true,"data":{"jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","projectId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","processType":"slideshow_agent_pipeline","status":"completed","error":null,"creditsReserved":9,"creditsConsumed":9,"createdAt":"2026-07-03T10:00:00Z","updatedAt":"2026-07-03T10:02:41Z","output":{"type":"slideshow","slides":[{"order":0,"url":"https://…/slide-1.png","description":"The coach mid-gesture at a whiteboard","texts":["Stop selling hours."],"imageOverlays":[]}]}}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/media/{jobId}":{"get":{"operationId":"getMediaResult","tags":["Jobs"],"summary":"Get media file","description":"The finished file for a `POST /media/image` or `/media/video` job — the same `url` `GET /jobs/{id}` returns in `output`. Signed, and expires after **24 hours** — download it, and fetch again rather than storing the link.\n\n**Auth:** any valid API key. Free — nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","format":"uuid","description":"The jobId from POST /media/image or /media/video."},"required":true,"name":"jobId","in":"path"}],"responses":{"200":{"description":"Ready (or failed — check `status`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"completed":{"value":{"success":true,"data":{"status":"completed","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","mode":"image","url":"https://…signed…"}}},"failed":{"summary":"Generation failed","value":{"success":true,"data":{"status":"failed","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","mode":"image","error":"Media generation failed"}}}}}}},"202":{"description":"Still generating — keep polling.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"processing":{"value":{"success":true,"data":{"status":"processing","jobId":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","mode":"image"}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"404":{"description":"Unknown resource, a resource owned by another account, or an id that isn’t a UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Project not found"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/models":{"get":{"operationId":"listModels","tags":["Models"],"summary":"List models","description":"The models you can pick, by role — text, image, video — each with a friendly `name`, a `description`, whether it’s `premium`, whether it’s the default, and (image and video) its `capabilities`: the rules the app itself enforces. Use them instead of guessing:\n\n- `aspectRatios` — what `aspectRatio` can be (checked on video, ad and media requests; slideshows aren’t checked).\n- `durations`, `resolutions`, `defaultResolution` — the lengths and resolutions a video model makes.\n- `supportsEndFrame`, `maxReferenceImages`, `combinesFramesWithReferences` (`false` ⇒ a start/end frame and reference images can't be combined), `supportsAudioToggle` (whether `audio` can be switched).\n- Image models: `sizeTiers` and `defaultSizeTier`, `supportsQualityToggle` with `qualities` and `defaultQuality` (both change the price).\n\n**To use a model**, pass its `id` under the group's `settingsKey` — inside `settings` on `/video`, `/ad` and `/slideshow` (e.g. `settings.videoModel`), or as a top-level field on `/media/image` and `/media/video` (e.g. `videoModel`) — then estimate again.\n\n`isDefault` marks the default for an account on a plan. With no `textModel` set, an account without a live plan (pay-as-you-go, or never paid) runs Sonnet 5.5 for text instead — an estimate’s `models` always names what will actually run.\n\n**Auth:** any valid API key; nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","enum":["video","slideshow","ad","image","clip"],"description":"Only the roles that kind of job uses (a slideshow has no video model): `image` and `clip` are the models for `POST /media/image` and `/media/video`. Omit for all three roles."},"required":false,"name":"for","in":"query"}],"responses":{"200":{"description":"Model groups by role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"models":{"summary":"GET /models?for=clip (the default model shown)","value":{"success":true,"data":{"models":[{"role":"video","settingsKey":"videoModel","models":[{"id":"gemini-omni-1.1-flash","name":"Omni 1.1 Flash","description":"Google frontier — native audio, cheapest premium tier","isDefault":true,"premium":true,"capabilities":{"aspectRatios":["9:16","16:9"],"durations":[3,4,5,6,7,8,9,10],"resolutions":["360p","720p","1080p","4k"],"defaultResolution":"720p","supportsEndFrame":false,"maxReferenceImages":3,"combinesFramesWithReferences":false,"supportsAudioToggle":false}}]}]}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/styles":{"get":{"operationId":"listStyles","tags":["Models"],"summary":"List styles","description":"The names `settings.style` takes on slideshows, videos and ads — the built-in styles plus your own. An unknown name is a 400.\n\n`?for=captions` lists the caption styles `captions.style` takes instead — the presets plus your saved ones — each with its `look` in the same fields the `captions` object takes, so you can start from one and change what you like.\n\n**Auth:** any valid API key; nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","enum":["visual","captions"],"description":"`visual` (the default): image styles for `settings.style`. `captions`: caption styles for `captions.style`."},"required":false,"name":"for","in":"query"}],"responses":{"200":{"description":"The styles.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"styles":{"value":{"success":true,"data":{"styles":[{"name":"#photoreal","category":"photography","isSystem":true},{"name":"#my-brand-style","category":"illustration","isSystem":false}]}}},"captionStyles":{"summary":"GET /styles?for=captions (two shown)","value":{"success":true,"data":{"styles":[{"id":"default","name":"Default","description":"Clean Lato on a soft dark pill, amber word highlight — readable on anything","isSystem":true,"look":{"style":"default","density":"medium","position":"bottom","font":"Lato","size":60,"bold":true,"italic":false,"textCase":"normal","textColor":"#FFFFFF","highlight":"word","highlightColor":"#fcc400","outlineWidth":0,"outlineColor":"rgba(0,0,0,0.4)","shadow":3,"glow":0,"glowColor":"#FFFFFF","background":"box","backgroundColor":"rgba(0,0,0,0.5)","animation":"slide_up"}},{"id":"simple-plain","name":"Plain","description":"White text and a soft shadow — nothing else","isSystem":true,"look":{"style":"simple-plain","density":"medium","position":"bottom","font":"Montserrat","size":64,"bold":true,"italic":false,"textCase":"normal","textColor":"#FFFFFF","highlight":"none","highlightColor":"#fcc400","outlineWidth":0,"outlineColor":"#000000","shadow":5,"glow":0,"glowColor":"#FFFFFF","background":"none","backgroundColor":"rgba(0,0,0,0.6)","animation":"fade"}}]}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/voices":{"get":{"operationId":"listVoices","tags":["Models"],"summary":"List voices","description":"The narrator voices `settings.voiceId` takes, most popular first. Search by description (`q=warm storyteller`) and filter by trait to find one that fits, then pass its `voiceId`. Omit `settings.voiceId` for the default narrator.\n\n`models` lists the voice models (`settings.voiceModel`) each voice is verified on; `languages`, the languages it speaks well. Pages come 40 at a time: pass `nextCursor` back as `cursor` for more.\n\n**Auth:** any valid API key; nothing is spent.","security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","description":"Words to search names, traits and descriptions for."},"required":false,"name":"q","in":"query"},{"schema":{"type":"string","enum":["female","male","neutral"],"description":"A narrator gender."},"required":false,"name":"gender","in":"query"},{"schema":{"type":"string","description":"An ISO 639-1 code the voice speaks, e.g. `en`, `es`."},"required":false,"name":"language","in":"query"},{"schema":{"type":"string","description":"An accent, e.g. `british`, `irish`."},"required":false,"name":"accent","in":"query"},{"schema":{"type":"string","enum":["young","middle_aged","old"],"description":"An age range."},"required":false,"name":"age","in":"query"},{"schema":{"type":"string","description":"What the voice suits, e.g. `narrative_story`, `informative_educational`."},"required":false,"name":"useCase","in":"query"},{"schema":{"type":"string","enum":["eleven_flash_v2_5","eleven_multilingual_v2","eleven_v3"],"description":"Only voices verified on this voice model."},"required":false,"name":"model","in":"query"},{"schema":{"type":"string","enum":["all","saved","recent"],"description":"`saved` or `recent`: your own saved voices, or the ones you narrated with lately."},"required":false,"name":"tab","in":"query"},{"schema":{"type":"string","description":"The `nextCursor` from the previous page."},"required":false,"name":"cursor","in":"query"}],"responses":{"200":{"description":"A page of voices.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"voices":{"value":{"success":true,"data":{"voices":[{"voiceId":"AaOhDHYJ1XLZk74lXhdE","name":"Caleb - Trusted Guide","displayName":"Caleb","title":"Trusted Guide","description":"A warm, steady American narrator for explainers.","previewUrl":"https://storage.googleapis.com/eleven-public-prod/voices/caleb.mp3","gender":"male","age":"middle_aged","accent":"american","language":"en","locale":"en-US","descriptive":"confident","useCase":"informative_educational","models":["eleven_flash_v2_5","eleven_multilingual_v2"],"languages":["en"]}],"nextCursor":40}}}}}}},"400":{"description":"Validation error — a body that isn’t valid JSON or doesn’t match the schema, an unknown style, or a model-capability violation (unsupported aspect ratio, off-grid clip duration, invalid frame + reference combo). The message lists the allowed values where relevant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Aspect ratio \"3:7\" isn’t supported by kling-v3. Allowed: 9:16, 16:9, 1:1."}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/credits":{"get":{"operationId":"getCredits","tags":["Balance"],"summary":"Get balance","description":"Your credits, and whether you can generate right now.\n\n- `available` — credits you can spend; `availableDollars` is what they're worth at pay-as-you-go rates. `reserved` is held by jobs still running.\n- `canGenerate` — true when a generate call would go through now: a connection allowed to create, credits to spend, and no failed plan payment freezing spend.\n- `canCreate` — false only on an MCP connection approved to look without creating (API keys can always create).\n- `workspace` — the team creations land in when a call names none (`null` only for an account with no team).\n- `canStartFreePlan` — true when an account that has never paid can still plan a UGC ad for free (`freePlansUsed` of `freePlanCap`).\n\n**Auth:** any valid API key; nothing is spent.","security":[{"apiKey":[]}],"responses":{"200":{"description":"The account status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"subscribed":{"value":{"success":true,"data":{"available":500,"availableDollars":5,"reserved":78,"used":120,"total":698,"subscription":{"active":true,"plan":"creator"},"canGenerate":true,"canCreate":true,"workspace":{"id":"6f1c2e8a-3b7d-4c51-9a0e-2d4b8f7c1a35","name":"Personal"},"canStartFreePlan":false,"freePlansUsed":0,"freePlanCap":5}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}},"/teams":{"get":{"operationId":"listTeams","tags":["Teams"],"summary":"List teams","description":"Everything you create lands in a team — the **default team** (`isDefault: true`) unless you pass a `teamId` on a create call (`POST /media/image`, `/media/video`, `/video`, `/ad`, `/ad/plan`, `/slideshow`, `/media/upload-complete`). The default is your first team (the personal one made at sign-up) — or, over an MCP connection, the workspace picked when it was approved. A `teamId` you’re not a member of is a 403.\n\n**Auth:** any valid API key; nothing is spent.","security":[{"apiKey":[]}],"responses":{"200":{"description":"Your teams.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"},"examples":{"teams":{"value":{"success":true,"data":{"teams":[{"id":"3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607","name":"Personal","isDefault":true},{"id":"9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d","name":"Marketing","isDefault":false}],"nextStep":"Pass a teamId on create calls (slideshow, video, ad, ad plan, media generate, upload-complete) to act in that team; omit it to use the default."}}}}}}},"401":{"description":"Missing `x-api-key` (\"Authentication required\"), or a key that isn’t valid or was revoked (\"Invalid API key\").","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Authentication required"}}}},"429":{"description":"Too many requests — 30/min per API key or 300/min per IP (polling counts) — or, with `reason: \"too_many_active_jobs\"`, 30 of the account’s jobs already in flight. Honor the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"example":{"success":false,"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"example":{"success":false,"error":"Internal server error"}}}}}}}},"webhooks":{}}