Clipwright API
One HTTP API that turns a script into a lip-synced UGC video, or a script or a short brief into a faceless video: narration over an opening animated clip and image scenes. It is built to be driven by an agent: every call is a single JSON request, every refusal says what to do next, and nothing is published anywhere. Every word of this page is also one markdown file at https://clipwright.io/docs.md and a short contract for agents at https://clipwright.io/llms.txt for agents that read text. The OpenAPI 3.1 file at https://api.clipwright.io/openapi.json is the machine-readable REST contract, built from the schemas the API validates with.
Authentication
Every call goes to https://api.clipwright.io and carries the key in one header:
Authorization: Bearer cw_your_key_here- Keys start with cw_ and are shown once, when they are issued. We keep only a digest, so a lost key is replaced, never recovered.
- Issue and revoke keys at https://app.clipwright.io/api-keys in the dashboard. Revocation takes effect on the next request.
- @clipwright/cli and @clipwright/mcp-server read the key from the environment variable CLIPWRIGHT_API_KEY; @clipwright/sdk takes it as an argument.
- A call without a key, or with a revoked one, is refused with 401 before anything is charged.
What it costs
- make_ugc from a plain script: 10 credits for every second of finished video, rounded up to the whole second, at least 50 on a video we delivered.
- make_ugc with segments or inserts: 9 credits for every second a face is on screen (22 on a take rendered on OmniHuman), plus 1 for every second of the video and 400 for each live-avatar call still, and at least 50 credits on a video we delivered. A face covered by an insert, overlay, window or template still counts, and a run that delivers no file costs nothing at all. Face time is added up across the whole video and rounded up once, not per segment. These fields need long-form qualification; where it is off they are refused by name before any charge.
- create_actor at quality medium: 4 credits for the portrait (none when it is your own image, passed in portrait; 4 for a view of an existing actor made with from_actor_id) and 8 for each additional format.
- create_actor at quality high: 45 credits for the portrait (none when it is your own image, passed in portrait; 12 for a view of an existing actor made with from_actor_id) and 18 for each additional format.
- make_faceless: 7 credits per picture we generate, 49 for the opening clip and 1 per second of the delivered video, at least 50. The quote for your duration_seconds is the most you pay: 212 at 30 seconds, 356 at 90. Your own pictures are not charged, and captions do not change the price.
- Credits are bought in packs: 1000 credits for $10.00, 2750 credits for $25.00, 6000 credits for $50.00. One pack per payment, no subscription.
Ask before you spend: the quote endpoint of every skill charges nothing. What its answer is worth differs by skill.
- make_ugc: the quote is an estimate read off the words of the script. The charge follows what was measured in the finished video — its duration on the plain-script meter, its face seconds on the face meter — so the bill can land above or below the quote.
- create_actor: the quote prices every format you asked for, which is the most you can pay. You are charged for the portrait and for the variants actually published; a format that did not come out is named in warnings[] and costs nothing.
- make_faceless: the quote prices the duration_seconds you asked for, which is the most you can pay. The video runs as long as the narration needs, at least 25 seconds (an explainer, just its narration) and usually at most a quarter past that length and 90 seconds; a longer narration runs to its end. You are charged for the length actually delivered, never more than the quote.
What a failed run costs differs by skill too:
- make_ugc from a plain script: a run that failed after the video was ordered is charged. One that failed before it costs nothing, and so does one we stopped, lost or refused ourselves. On the face meter no failure is charged at all.
- create_actor: a failed run costs nothing at all, because no actor reached you.
- make_faceless: a failed run costs nothing, including one we stop ourselves because a step did not meet our checks. A run held for review keeps its credit hold until it is resolved.
Editing prices
A revision of a faceless video is a new run made from an edited document: POST /v1/runs/{id}/revisions/quote prices it for nothing, POST /v1/runs/{id}/revisions starts it.
- A new picture: 7 credits.
- A new opening clip: 49 credits.
- A new opening still and its clip: 56 credits.
- The render of a revision that generates anything: 13 credits, once per revision.
- The render of a revision that generates nothing (a retime, a deletion or your own upload): 13 credits.
- Once paid edits are open, an original can be edited for money for 364 days after it finished.
- Revisions of one original running at once: at most 1. Failed or unfinished: at most 3.
- A revision that delivers is charged its quoted total; one that fails is charged nothing.
- While paid edits are closed, both revision routes answer editing_disabled and charge nothing.
Shot candidate prices
A shot candidate is one picture or clip made for one shot of a faceless video, to keep or discard in a revision: POST /v1/runs/{id}/candidates/quote prices it for nothing, POST /v1/runs/{id}/candidates makes it.
- A candidate picture: 7 credits.
- A candidate opening clip: 49 credits.
- A candidate opening still and its clip: 56 credits.
- Shot candidates of one original running at once: at most 1. Failed or unfinished: at most 3. Revisions are counted apart.
- A delivered candidate is charged its one unit whether it is kept or discarded; one that fails is charged nothing.
- While shot candidates are closed, the three candidate routes answer editing_disabled and charge nothing.
Endpoints
| Endpoint | Costs credits | What it does |
|---|---|---|
| POST /v1/skills/make_faceless/quote | no | Prices a make_faceless call with this input. Charges nothing. |
| POST /v1/skills/make_faceless/run | yes | Starts a faceless video run and answers at once with a run_id. Poll the run for the result. |
| GET /health | no | Liveness of the API itself. Answers without a key. |
| GET / | no | Names the service and points to this document. Answers without a key. |
| GET /robots.txt | no | Tells crawlers not to index the API host. Answers without a key. |
| GET /openapi.json | no | The OpenAPI 3.1 description of this API, built from the same schemas the API validates with. Answers without a key. |
| GET /v1/voices | no | Voices you can name in voice or voice_id for make_ugc; skill=make_faceless lists the narration voices of make_faceless instead. |
| GET /v1/template-sounds | no | The sounds make_ugc templates play, by beat: where each beat plays, its default and every name template_sounds takes. Free. |
| GET /v1/moves | no | The catalogue of moves to put on lines of a script as moves of make_ugc, make_faceless and a reference remake: what each does for the viewer, the lines it serves and its credits per use, filtered and paged; names gives full lines. Free. |
| GET /v1/account | no | Balance, debt and holds of the account behind the key, and our replies to its support requests and feedback. |
| POST /v1/skills/make_ugc/quote | no | Prices a make_ugc call with this input. Charges nothing. |
| GET /v1/runs/{id} | no | State of one run of any skill, its warnings and its video url. |
| GET /v1/runs/{id}/shots | no | The shots of one run: each shot's span, words, description and preview, and whether the run can be edited. |
| GET /v1/runs/{id}/edit | no | The saved draft of an edit of one run, with its version. Version 0 is a run with no draft: its document changes nothing. |
| POST /v1/runs/{id}/edit | no | Saves the draft of an edit against the version it was read at. Charges nothing and renders nothing. |
| POST /v1/runs/{id}/revisions/quote | no | Prices a revision of a faceless run: what the document or the list of shot changes generates and what it costs. Charges nothing. A revision that delivers is charged its quoted total; one that fails is charged nothing. While paid edits are closed it answers editing_disabled. |
| POST /v1/runs/{id}/revisions | yes | Starts a revision, a new faceless run made from the document, and holds the quoted credits. The same Idempotency-Key with the same request returns the first revision; another key is another revision, charged again. A revision that delivers is charged its quoted total; one that fails is charged nothing. While paid edits are closed it answers editing_disabled and charges nothing. |
| POST /v1/runs/{id}/candidates/quote | no | Prices one shot candidate of a faceless run: one picture or clip made for one shot, to keep or discard. Charges nothing. A delivered candidate is charged its one unit whether it is kept or discarded; one that fails is charged nothing. While shot candidates are closed it answers editing_disabled. |
| POST /v1/runs/{id}/candidates | yes | Starts a shot candidate and holds the price of its one unit. A delivered candidate is charged its one unit whether it is kept or discarded; one that fails is charged nothing. The same Idempotency-Key with the same request and take returns the first candidate; take + 1 makes another, charged again. While shot candidates are closed it answers editing_disabled and charges nothing. |
| GET /v1/runs/{id}/candidates | no | The shot candidates of a faceless run's family, oldest first, each with its state, its charge and a preview once made. While shot candidates are closed it answers editing_disabled. |
| DELETE /v1/runs/{id}/media | no | Deletes the files of a finished run: no link to them is given from now on, and they are erased from storage after 7 days. The run and its charge stay. Refused with run_media_in_use while another unfinished run uses them. Free. |
| POST /v1/skills/make_ugc/run | yes | Starts a video run and answers at once with a run_id. Poll the run for the result. |
| GET /v1/public/skills | no | Catalogue of skills and their input, without a key. |
| GET /v1/public/prices | no | The price of every action in credits: a fixed number per unit, or a formula where the price depends on the input; the quote gives the exact number. Free, without a key. |
| GET /v1/actors | no | Actors saved on the account, with the id make_ugc takes. |
| GET /v1/public/actor-previews/{actor_id}/{version}/{file} | no | A library actor's picture as PNG, the link list_actors and quotes give. Answers without a key. |
| DELETE /v1/actors/{id} | no | Forgets a saved actor. An actor used by a live run is kept. |
| GET /v1/actors/{id}/defaults | no | Reads the saved actor's default policy for people in inserts. |
| POST /v1/actors/{id}/defaults | no | Sets the saved actor's default policy for people in inserts. A run can override it. |
| GET /v1/brand-kit | no | Reads the account's saved design: colours, fonts, logo, corners and motion. Null when none is saved. |
| POST /v1/brand-kit | no | Saves the account's design, or clears it with null. A video that names neither design nor template_theme draws in it. Free. |
| POST /v1/brand/extract | no | Reads a public site's colours, fonts, logo and corners into a proposed design with readable text, for you to confirm. Free; counts towards the hourly url downloads. |
| POST /v1/skills/create_actor/quote | no | Prices a create_actor call with this input. Charges nothing. |
| POST /v1/skills/create_actor/run | yes | Starts an actor run and answers at once with a run_id. Poll the run for the result. |
| POST /v1/uploads | no | Takes image bytes and returns a signed https url, upload_id and content_digest (SHA-256 of the original bytes). Use upload_id as a workflow asset_id and copy content_digest directly; native video and actor calls use url. |
| GET /v1/uploads | no | Your uploads a revision still takes, newest first and one per picture, each with upload_id, the original content_digest, a freshly signed url and the moment it stops being usable. Free. |
| POST /v1/uploads/import | no | Downloads a public https PNG or JPEG (up to 10 MB, 20 seconds, 3 redirects) and returns the same upload shape. Private addresses are refused; 30 downloads per hour per account. Free. |
| POST /v1/uploads/intents | no | Returns a presigned PUT url for a local PNG or JPEG of the declared type, size and sha256. The upload_id is usable once the PUT lands; before that generations answer upload_not_finished. Free. |
| GET /v1/uploads/{id} | no | One upload with a freshly signed url and its usable_until. After a presigned PUT it confirms the file: upload_not_finished until it is there, upload_expired after 24 hours. Free. |
| DELETE /v1/uploads/{id} | no | Deletes an upload: no call takes it from now on, and its file is erased from storage after 7 days unless another upload of the same picture is kept. Refused with upload_in_use while an unfinished run uses it. Free. |
| POST /v1/assets | no | Saves a picture as an account asset, kept until you delete it: from a run_id, an upload_id, a public https url or base64. Your own material needs rights_confirmed: true, or analysis_only: true for a reference kept for analyze_reference alone, which no render or generation takes. 2000 assets and 1 GB per account, 20 GB once it has bought credits, 100 new ones per day. Free. |
| POST /v1/assets/bytes | no | Saves the PNG or JPEG in the body (up to 10 MB) as an asset; role, name and rights_confirmed=true (or analysis_only=true, for analyze_reference alone) travel in the query. Free. |
| GET /v1/assets | no | The account's live assets, newest first, each with a link signed for an hour, and the usage of the caps; filters role and kind, paged by cursor. Free. |
| GET /v1/assets/{id} | no | One live asset with a link signed for an hour. Free. |
| POST /v1/assets/{id} | no | Renames an asset or changes its role. Free. |
| DELETE /v1/assets/{id} | no | Deletes an asset: it leaves the list and generations at once, and a daily cleanup removes its file once 7 days have passed. Free. |
| GET /v1/public/workflows | no | Lists public workflow methods and whether each is available now, without account plans or an account admission claim. |
| GET /v1/public/workflows/{id} | no | Reads a method's guidance, limitations and what it can do right now. |
| POST /v1/workflow-plans | no | Creates an immutable account-owned workflow draft. Charges nothing and starts no render. |
| GET /v1/workflow-plans/{id} | no | Reads the account's current workflow revision and readiness. |
| POST /v1/workflow-plans/reference-remake | no | Compiles a reference profile and the accepted script into a new reference-remake draft; its warnings name what the plan does not reproduce. Charges nothing, generates nothing and starts no render. |
| POST /v1/workflow-plans/{id}/revisions | no | Saves a new draft against base_version. A matching lost-response retry returns that revision. |
| POST /v1/workflow-plans/{id}/approve | no | Approves a ready revision and its digest. Charges nothing and starts no render. |
| POST /v1/workflow-plans/{id}/quote | no | Quotes an approved workflow through native preparation. The quote expires after five minutes; no credits are reserved. Its launch is the ready body of the run request, with a fresh idempotency_key. |
| POST /v1/workflow-plans/{id}/run | yes | Admits the exact quoted native request once, returning a receipt and the existing native Run. Send the quote's launch unchanged as the body, with the Idempotency-Key header equal to launch.idempotency_key; the same accepted request replays its run. |
| GET /v1/generations/models | no | The models generate_image and generate_video take, with their measured modes, aspect ratios, resolutions, durations, output size and credits per image or per second. Free. While generations are closed it answers generations_disabled. |
| POST /v1/skills/generate_image/quote | no | Prices one image with this request: credits, width, height and warnings. Charges nothing. |
| POST /v1/skills/generate_image/run | yes | Starts one image generation and answers at once with a run_id; the run's generation block holds the image once delivered. A failed generation is charged nothing. While generations are closed it answers generations_disabled and charges nothing. |
| POST /v1/skills/generate_video/quote | no | Prices one silent clip with this request: credits per second times its duration, width, height and warnings. Charges nothing. |
| POST /v1/skills/generate_video/run | yes | Starts one clip generation and answers at once with a run_id; the run's generation block holds the clip once delivered. A failed generation is charged nothing. While generations are closed it answers generations_disabled and charges nothing. |
| POST /v1/reference-analyses/quote | no | Prices an analysis of this asset or link: 70 credits, 10 with mode data, or 0 when its live analysis in that mode would answer. Charges nothing. |
| POST /v1/reference-analyses | yes | Starts reading an MP4 asset, up to 120 s, into a reference profile (or, with mode data, its transcript, hook and a frame sheet) and answers at once; the same asset in the same mode answers the same analysis for free. Holds 70 credits (10 for data), charged once it succeeds; a failed one costs nothing. 10 new full and 60 new data analyses per hour. |
| GET /v1/reference-analyses/{id} | no | One reference analysis: queued, analyzing, succeeded with reference_profile or data and warnings, or failed with its error. Free. |
| POST /v1/creator-profiles/quote | no | Prices a creator profile for this link: 20 credits, or 0 when a profile of the same creator from the last hour would answer. Charges nothing. |
| POST /v1/creator-profiles | yes | Starts profiling the creator behind an Instagram or TikTok profile or video link: their latest 12 videos with views, likes, comments and shares, the median views and each video's x to it. Answers at once; the same creator within an hour answers the same profile for free. Holds 20 credits, charged once it succeeds; a private or empty account, or any failure, costs nothing. 30 new profiles per hour. |
| GET /v1/creator-profiles/{id} | no | One creator profile: queued, analyzing, succeeded with the profile and warnings, or failed with its error. Free. |
| DELETE /v1/creator-profiles/{id} | no | Deletes a finished creator profile at once; the run that made it and its charge stay. Refused with run_not_finished while it is being made. Free. |
| POST /v1/trend-searches/quote | no | Prices a topic search: 20 credits, or 0 when a search of the same query from the last hour would answer. Charges nothing. |
| POST /v1/trend-searches | yes | Starts a search for reels on a topic, TikTok and Instagram ranked together unless platform names one: up to 10 reels per platform, one per creator, each with its views against its creator's median. Answers at once; the same query within an hour answers the same search for free. Holds 20 credits, charged once it succeeds, a mix that reaches only one platform included (warnings[] names the other); no reel found, or any failure, costs nothing. 10 new searches per hour. |
| GET /v1/trend-searches/{id} | no | One topic search: queued, searching, succeeded with the reels and warnings, or failed with its error. sort reads the same reels in another order. Free. |
| POST /v1/feedback | no | Tells us what is missing, confusing, wrong or slow, optionally about a run of this account; we read every one. Free. The same kind, text and run within 24 hours answer the same feedback_id; 20 per hour. |
A run of any skill is read back from the same place, GET /v1/runs/{id}, and moves through these states: queued, generating, scripting, tts, avatar, compositing, uploading, succeeded, failed.
GET /v1/runs/{id}?wait_seconds=N (at most 50) holds the answer until the run succeeds or fails, the wait ends or you disconnect, so no sleep is needed between reads. An unfinished run answers in its current state.
Skills and their input
make_ugc. Start generation of a lip-synced UGC video. The actor comes from actor_id (a saved actor from list_actors) or image, otherwise the default actor is used. Format and resolution follow the request and the source, defaulting to 1080x1920. Captions are OPT-IN: ask the user first. A field the renderer does not honor yet says NOT HONORED YET in its description.
Returns a run_id at once; poll get_run with its poll.arguments until 'succeeded' (video_url) or 'failed'. A 'failed' run whose video is still being made can go back to 'queued' and succeed later, named in warnings[]. attempt=2,3,… starts a NEW run for the same input. For a video like a reference reel, use analyze_reference, then plan_reference_remake with its analysis_id.
| Field | Required | What it means |
|---|---|---|
| script | optional | The words the actor says; required unless segments supplies the spoken text. Segments and text-anchored inserts require long-form qualification on the server. Script limits by speech model: eleven_v3: 5000 characters; eleven_flash_v2_5: 10000 characters; eleven_turbo_v2_5: 10000 characters; eleven_v4: 5000 characters; gemini-3.8-flash-tts: 5000 characters. Count includes spaces, audio tags and stress marks; emoji may count as two characters. Break tags are cut from the text sent to eleven_v4 and gemini-3.8-flash-tts and are not counted there. There is no word-count limit. Duration and price are estimates until measured. Russian stress: write the stressed vowel as a capital inside a lowercase word ("потОм", "зАмок") and eleven_v4 and gemini-3.8-flash-tts receive it as the stress mark U+0301 ("пото́м"); a mark typed directly is kept. A capital at the start of a word stays a capital, and a word with a second capital or a capital consonant inside (all caps, "ВУЗы") is left as it is. A single capital vowel inside a word is always read as stress, so write "Яндекс Еда", not "ЯндексЕда". Tell users writing in Russian that they can mark stress this way. eleven_flash_v2_5 and eleven_turbo_v2_5 cost less but misread stress marks: capitals reach them unchanged. |
| segments | optional | Ordered actor and image segments; requires long-form qualification on the server, captions=false and 1080p. Image media requires explicit broll_policy=anyone. |
| inserts | optional | Text-anchored image or video inserts over full narration, each covering cover_words spoken words from its anchor; requires long-form qualification on the server, 1080p and explicit broll_policy=anyone. Captions draw over inserts. |
| overlays | optional | Text-anchored images or videos over the visible presenter, each at one position or place. At most 2 on screen at once, on boxes that do not overlap. Captions move to the middle or bottom third off an overlay, never over the face, and hide while neither is free. Hidden under full-frame inserts. Same requirements as inserts: script, long-form qualification on the server, 1080p and explicit broll_policy=anyone. The presenter stays on screen, so the price does not change. |
| layout_windows | optional | Text-anchored reframings of the presenter: split (9:16 only) or card. Windows may not overlap one another or an insert. Same requirements as inserts. A covered or blurred presenter is still delivered vendor work and is billed as face time. |
| framing | optional | Text-anchored presenter sizes: each cuts to close or medium on its words and back to the take where they end. Items may not overlap one another, an insert or a layout window. A framing drawn wholly under full-frame templates is named in warnings[]. Where a punch_in emphasis meets a framing, the framing is drawn. Requires script and long-form qualification on the server; the price does not change. |
| templates | optional | Graphic templates, each { id, kind, anchor, cover_words?, placement?, transition?, data }. On any script run. Templates never change the price: every run is billed by its parts as list_prices states them, and a presenter a template covers is still presenter time. Ids are unique across inserts, overlays, layout_windows and templates. Text is one line of Latin or the script's own letters, digits and common punctuation, with no emoji; a wide CJK glyph counts 2 toward a limit, and long text shrinks to fit the kind's width. size small|medium|large scales a kind; absent is medium. A media field takes { asset_id } of a ready asset of the kind it names. An inner quote names words inside the template's own words, after the one before it: its beat lands on their first word, and must not fall under an insert or full-frame template; a click or step without one is spread evenly. A beat spoken before its template has entered moves just after the enter, with a warning. A template that meets the caption band hides captions while it shows; one elsewhere, such as a small one at the top, leaves them. A full-frame template also hides the presenter, overlays and over-presenter templates while it shows, never meets a wipe or zoom insert, and the covered presenter is billed. At most 3 overlays and over-presenter templates show at once, on footprints that do not overlap. template_theme sets the look of them all. Kinds: tag (over): a short gradient pill over the presenter or an insert; without position it sits in the caption spot and hides captions while it shows; data text ≤ 32, position? top|center|bottom, size? small|medium|large. lower_third (over): a name and role bar low in the frame; captions are hidden while it shows; style absent is clean_bar; data name ≤ 28, role? ≤ 36, side left|right, style? clean_bar|side_rule|soft_pill|mask_reveal, size? small|medium|large. end_card (over): a site pill with an optional call to action in the caption spot; captions are hidden while it shows; data site ≤ 32, cta? ≤ 48, size? small|medium|large. title_card (full): a full-frame hook card: the title (up to two lines) on a white card beside an optional logo image, and an optional chip under it; data title ≤ 48, chip? ≤ 32, logo? {asset_id} image, size? small|medium|large. quote (full): a full-frame quote card: marks, the text in up to four lines, then the author with an optional round photo, cut to a circle; data text ≤ 120, author? ≤ 32, photo? {asset_id} image (cut to fill), size? small|medium|large. stat (full|over): a number counting up from 0 to value (up to 2 decimals), with prefix and suffix around it and a label under it; full fills the frame, over sits at chest level on a card; data value number, prefix? ≤ 6, suffix? ≤ 6, label? ≤ 32, size? small|medium|large. callout (over): an arrow or ring that lands on a point of the frame and pulses, target {x,y} in % of the frame from its top left, with an optional label pill below or above it; it may point at the face, its label keeps off the face where it can; also over an insert; data target {x,y}, style arrow|ring, text? ≤ 24, size? small|medium|large. checklist (over): 2–4 rows, each an optional icon, a label and a switch or check box set on or off; a cursor clicks rows in turn (item is the row's index from 0), each click flipping it; below the face; data items 2–4 of {label ≤ 24, on true|false, icon? phone|briefcase|user|clock|chat|star}, clicks? 0–6 of {item number, quote? inner quote}, style? switch|check, size? small|medium|large. phone_call (over): an incoming call on a neutral phone at the side (right unless side left): the caller, a subtitle and a round avatar ring, then the call is answered on answer_at and its timer runs (or a hold banner shows); beside the face in wide frames, under it in tall ones; data caller ≤ 24, subtitle? ≤ 32, avatar? {asset_id} image (cut to fill), answer_at inner quote, side? left|right, size? small|medium|large, banner? ≤ 32, banner_note? ≤ 48, mask_number? true|false. compare (full): a full-frame 3:5 card showing the before image; on reveal_at the after image sweeps in on a diagonal (reveal absent) or wipes in from the left to a middle divider; both stay, with optional label pills; data before {asset_id} image (cut to fill), after {asset_id} image (cut to fill), labels? {before? ≤ 20, after? ≤ 20}, reveal_at inner quote, reveal? sweep|wipe, size? small|medium|large. notification (over): a phone notification at the top, above the face: an icon (else the app's initial), the app, a title and a body line; it drops in, holds at least 1.2 s and leaves before its span ends; each stack card drops in on top on its at, pushing earlier ones back; data app ≤ 20, title ≤ 32, body ≤ 80, icon? phone|briefcase|user|clock|chat|star, stack? 1–2 of {app? ≤ 20, title ≤ 32, body ≤ 80, icon? phone|briefcase|user|clock|chat|star, at? inner quote}, size? small|medium|large. progress (over): 2–5 steps in a column on a card, each completing with a check in turn, on its advance_at quote (one per step, in order) or spread evenly; below the face; data steps 2–5 of ≤ 20, advance_at? 2–5 of inner quote, size? small|medium|large. rating (over): five stars on a card filling in turn up to stars (1–5 in halves, a half star half filled), an optional label under them; at chest level; data stars number, label? ≤ 32, size? small|medium|large. chat_window (full): a full-frame chat window: the prompt types in, the answer streams in word by word, its table opens row by row, then reply cards pop in, each on its at or spread evenly; zoom true cuts in and out on the beats; data app? ≤ 24, prompt ≤ 120, replies? 1–3 of {title ≤ 32, body? ≤ 64, at? inner quote}, answer? {text ≤ 150, at? inner quote}, table? {columns 2–3 of ≤ 16, rows 1–4 of {cells 2–3 of ≤ 20, at? inner quote}}, zoom? true|false, size? small|medium|large. video_grid (full): a full-frame grid of the account's video assets as 9:16 tiles, 2x2 (cells 1–4, the left column raised) or 3x1 (cells 1–3); without layout, the cells given decide, each tile popping in after the one before and playing muted, with an optional title above; data layout? 2x2|3x1, cell_1 {asset_id} video (cut to fill), cell_2 {asset_id} video (cut to fill), cell_3 {asset_id} video (cut to fill), cell_4? {asset_id} video (cut to fill), title? ≤ 40, size? small|medium|large. word_title (full): a full-frame kinetic title in the accent colour: the first entry shows from the template's first frame, each later entry on its at quote (usually the same words, as spoken), or spread evenly; reveal replace (default) shows one entry at a time, build adds each to the title; data words 1–8 of {text ≤ 16, at? inner quote}, reveal? replace|build, size? small|medium|large. phone_screen (full): a full-frame phone playing the screen video (muted; a gradient without one) whose states change in turn, each on its at quote or spread evenly: lock (dimmed, a lock and the text), play (the video clear, the text at its top), liked (a heart and the text), counter (a pill at the screen's foot), timer (a badge on its top edge), tag (a chip with a pointer line at the screen's side); lock, play and liked replace one another, tags add up, a later counter or timer replaces the earlier; data screen? {asset_id} video (cut to fill), states 1–6 of {state lock|play|liked|counter|timer|tag, text? ≤ 24, at? inner quote}, size? small|medium|large. terminal (full): a full-frame dark terminal card: the command types in; a diff under it collapses its del lines (red) and opens its add lines (green) on diff.at; then a checked status line shows on at; title names the card; data title? ≤ 24, command ≤ 100, diff? {lines 2–7 of {text ≤ 36, change? add|del}, at? inner quote}, status ≤ 40, at? inner quote, size? small|medium|large. pointer_chips (over): 1–4 chips in a column below the face (right unless side left), each joined by a curved line to a dot on its target {x,y} in % of the frame from its top left, appearing in turn on its at quote or spread evenly; the lines may cross the face, the chips keep off it; also over an insert; data items 1–4 of {target {x,y}, text ≤ 20, at? inner quote}, side? left|right, size? small|medium|large. chart (full): a full-frame bar or line chart of 2–6 labelled points on a card: on each point's at, or spread evenly, its bar grows or the line draws on to it and its value counts up, suffix after it; title above; data type bar|line, title? ≤ 40, suffix? ≤ 6, points 2–6 of {label ≤ 12, value number, at? inner quote}, size? small|medium|large. race (full): a full-frame ranked bar race: one bar per name, longest on top; the first round's values show from the start, then on each later round's at, or spread evenly, the bars move to its values and swap places by rank, the round's label showing under them; data title? ≤ 40, suffix? ≤ 6, names 3–6 of ≤ 14, rounds 2–5 of {label? ≤ 10, values 3–6 of number, at? inner quote}, size? small|medium|large. hand_title (full|over): a title written out stroke by stroke in a handwriting face (up to two lines), then a marker drawn on the title's words from mark.word (index from 0) for mark.words (absent 1), on mark.at or late in the template; over sits in the caption spot and hides captions; data text ≤ 40, mark? {word number, words? number, style highlight|underline|circle, at? inner quote}, size? small|medium|large. word_fill (full): a full-frame single huge word whose letters show your video (clip, playing muted), the rest of the frame the theme's ground; it grows in over its first 10 frames; data word ≤ 10, clip {asset_id} video, size? small|medium|large. |
| template_theme | optional | The look of every template in the run: a preset and an optional accent; only with templates. |
| design | optional | The run's design DNA: templates, the end card and the caption accent draw in it. Ask for the brand or use extract_brand; absent uses the saved brand kit. Not with template_theme. |
| template_sounds | optional | Accepted; the schema carries no description yet. |
| scenes | optional | Graphics made for this video from your words, for what no template draws: each is drawn over the presenter (over) or on the full frame (full) while its words are spoken; one drawn on the captions hides them while it shows. One that does not pass our check is left out and named in warnings[]. 25 credits per scene, its own quote line, charged only for scenes drawn in the delivered video. Needs script and long-form qualification on the server. |
| motion_style | optional | Accepted type: object. |
| moves | optional | Moves from list_moves on your words: each { name, anchor: { quote }, cover_words?, placement?, data? }. Each is drawn as its line in list_moves says; data takes the fields it names. A move that cannot be drawn on its words or in this video is named in warnings[] and left out, never dropped silently. Its credits are on the quote. |
| edit_sounds | optional | Accepted values: true | false. |
| music | optional | Background music under the voice, made for this video; off unless you ask. "auto" takes the mood from the script; { mood } says it in your words, such as "calm lo-fi". 60 credits per video, its own line on the quote, charged only when the video is delivered with it. |
| call | optional | A phone call after a quote: the presenter listens, then freezes while the call plays as a phone line, with a ring first, and an in-call phone beside them shows it (a phone_call template covering the call is used instead). Needs script, a 9:16 frame and words after it. The frozen presenter and the call add production seconds, not presenter seconds, and a live-avatar still shown on the call has its own price in list_prices. Long-form qualification on the server. |
| interview | optional | An interview: an off-screen speaker says the marked words of script in their own voice, the presenter answers the rest on screen and holds still while the off-screen speaker talks. Needs script and long-form qualification on the server; the presenter's held frames add production seconds, not presenter seconds. |
| presenter_gaze | optional | Where the presenter looks on these words, as the viewer sees it: camera, left or right. Needs actor_id with that view (list_actors); a change of gaze is a cut. |
| shots | optional | Live shots of the actor on some words of script: talking, talking_action (speaks while acting) or action (acts while the words play as voice-over). Shots may not overlap; words outside them are the presenter's usual take. Needs a 9:16 frame and long-form qualification on the server; each kind has its own presenter-second price in list_prices. |
| product_asset_id | optional | The product image (ast_…) that shots with product show. |
| person | optional | NOT HONORED YET: person is not honored yet: this request uses the default actor; choose actor_id from list_actors or provide image to select a different face |
| actor_id | optional | Saved Clipwright actor ID from list_actors. Choose actor_id, image, or person; do not combine them. Without voice or voice_id the voice follows the actor's gender. Do not combine with actor_gender. |
| presenter_framing | optional | close (default), wide (presenter lower, room above) or full (head to feet). Needs the actor's view of that name (list_actors), 9:16 only; full falls to wide, then close, plus warnings[]. |
| presenter_mic | optional | true: presenter holds a tiny mic. Needs the actor's mic view in that framing (list_actors; library ones are wide); else no mic plus warnings[]. |
| image | optional | Public https url of the actor's photo (PNG, JPEG or WebP, up to 10 MB). A file on disk goes through upload_image (POST /v1/uploads) first — pass the url it returns. A source we cannot use — private or loopback host, http, unreachable, redirecting, over 10 MB, or not one of those image types — is refused (unusable_source) before any charge. We do not detect the face's gender: pass actor_gender or voice, or the default male voice is used with a warning. |
| actor_gender | optional | Gender of the face in image: female | male. Only with image: picks the default voice of that gender (female: sarah, male: george; on gemini-3.8-flash-tts: kore and charon). Refused with actor_id (its gender is known) and without image. An explicit voice or voice_id wins and the response warns that actor_gender changed nothing. |
| name | optional | NOT HONORED YET: name is not honored yet: it does not reach the renderer |
| broll_policy | optional | Policy for B-roll: anyone allows people including the actor; no_actor excludes the actor; no_people excludes all people, including hands. Templates with your own picture or video need "anyone"; otherwise stored only. Run override wins over the account actor default; otherwise no_people. |
| captions | optional | Burn word-timed captions into the video, over the presenter and over inserts. Not available with segments. |
| caption_style | optional | Caption look when captions=true. hormozi: the default: heavy white uppercase with a thick black outline, up to three words per cue, one keyword per cue in yellow (emphasised, else the longest non-stop-word). accent: condensed bold white uppercase, no outline, up to three words per cue, one keyword per cue in orange (emphasised, else the longest non-stop-word). tiktok: bold rounded uppercase, one or two words at a time; the spoken word turns yellow and grows. youtube: clean bold sans-serif, four words (five in 16:9) on a dark rounded box near the bottom; words fade in, the spoken word in red. instagram: comic uppercase with a pink outline, two words at a time; the spoken word pops in orange. minimalist: plain sans-serif, four words on a half-transparent dark band, gentle fade. neon: cyan uppercase with a magenta outline, two words at a time; the spoken word pops in white. genz: large comic uppercase, one word at a time, slamming in with a pink highlight. campfire: rounded comic uppercase, three words; spoken words light up gold and stay lit. midnight: spooky uppercase, two words, slow fade; the spoken word in red. plottwist: heavy sans-serif, three words; the spoken word turns sky blue and grows. headline: tall condensed uppercase on a solid black band, three words; the spoken word scales up in yellow. promo: heavy sans-serif on a blue pill, three words; the spoken word pops in yellow. lecture: readable sans-serif, four words on a dark rounded box; the spoken word in green. highlighter: heavy sans-serif, four words; spoken words turn lime and stay lit. studio: sans-serif, four words typed in on a blurred dark box; the spoken word in amber. broadcast: outlined sans-serif, four words low in the frame; the spoken word in pale yellow. arcade: blocky yellow uppercase, two words; the spoken word bounces in white. clutch: brush uppercase, one word at a time, slamming in green. bubblegum: rounded display letters with a pink outline, two words; the spoken word bounces in yellow. crayon: marker handwriting with a navy outline, two words; the spoken word pops in orange. lyrics: handwritten script, four words; spoken words light up pink and stay lit. bassdrop: outlined display uppercase, one word at a time, scaling in orange. punch: mid-frame setup and punch: a small white lead-in line over one or two large blue glowing keywords; other words small and white. Alias: minimal draws minimalist. A font that cannot draw the script's letters is replaced by a plain sans-serif. Previews: https://clipwright.io/captions |
| caption_emphasis | optional | Key words or short phrases to emphasise, each quoted from script and matched as an insert quote is (case and punctuation ignored). An item is the quote, or { quote, look }: look punch shows a small lead-in line from the same sentence over the quote as a large glowing keyword, look pill shows the quote alone in a rounded pill, look color shows it in the accent colour at the same size, look box shows it on a soft box of the accent colour at the same size. Without a look, punch draws punch, hormozi and accent draw color, and other presets draw the quote larger in their highlight colour. Every occurrence is emphasised. Requires captions=true and script; a quote absent from script is refused before any charge. |
| caption_mode | optional | full (default when omitted): every spoken word is captioned. highlights: captions appear only around caption_emphasis quotes (lead-in, quote, short tail) and nowhere else; requires caption_emphasis. In both modes captions are hidden while a full-frame insert is on screen or a template meets their band. |
| caption_position | optional | Where captions sit: top, middle, lower or bottom (80% of the height). Omitted means the caption_style's own place. Requires captions=true. |
| caption_place | optional | Instead of caption_position, the captions at their own height; overlays never move them, and an overlay that meets them is refused unless it is placed relative_to captions. Requires captions=true. |
| presenter_place | optional | The presenter's face height and centre in the frame. The portrait is framed tighter to reach it, from the actor's wide view when the close one is too close; when even that is too close, warnings[] names the face height drawn. Requires actor_id and script with overlays, inserts, layout_windows, framing or call. The price does not change. |
| look | optional | NOT HONORED YET: look is not honored yet: it does not reach the renderer |
| aspect_ratio | optional | Output format: 9:16 | 1:1 | 16:9. Omitted means 9:16 without an image; with an image, the supported format nearest the image's shape, even a landscape one — pass it explicitly whenever you pass image. A mismatch above 15% between the request and the source is refused (aspect_conflict) before any charge. |
| resolution | optional | Output resolution: 720p | 1080p (short side 720 / 1080 px). Omitted means 1080p. |
| voice | optional | Voice name from list_voices. Curated presets: sarah | george | eric | daria_ru_female. The API refuses a name list_voices does not return, before any charge. Omitted means the default voice for the actor's gender: the gender of actor_id, actor_gender with image, or george for the default actor and for image without actor_gender. gemini-3.8-flash-tts speaks only its own voices (kind model_voice in list_voices, such as kore): naming one selects that model, and a voice is refused with a tts_model that does not speak it, before any charge. Mutually exclusive with voice_id. |
| voice_id | optional | Raw vendor voice id (16–32 letters and digits) for a voice outside the catalog. Checked lazily: an unknown id fails the run, not the request. Refused with tts_model=gemini-3.8-flash-tts. Mutually exclusive with voice. |
| tts_model | optional | Speech model: eleven_v3 | eleven_flash_v2_5 | eleven_turbo_v2_5 | eleven_v4 | gemini-3.8-flash-tts. Omitted means the model of the chosen voice (list_voices shows it): gemini-3.8-flash-tts for its own voices, eleven_v4 for every preset, catalog voice and raw voice_id. An explicit tts_model wins, except eleven_v3: it is no longer spoken, and a run naming it speaks eleven_v4 with a warning. eleven_v4 and gemini-3.8-flash-tts read stress marks (a capital vowel inside a Russian word, "потОм", becomes one; see script); eleven_flash_v2_5 and eleven_turbo_v2_5 are cheaper alternatives for languages other than Russian. gemini-3.8-flash-tts speaks only its own voices (list_voices with model=gemini-3.8-flash-tts; without voice, kore for a female actor and charon otherwise) and returns no word timings: a preset or catalog voice, voice_id, captions=true, segments and inserts are refused with it before any charge. Audio tags in square brackets are a convention of the other speech models and are not measured on it. Script limits by speech model: eleven_v3: 5000 characters; eleven_flash_v2_5: 10000 characters; eleven_turbo_v2_5: 10000 characters; eleven_v4: 5000 characters; gemini-3.8-flash-tts: 5000 characters. Count includes spaces, audio tags and stress marks; emoji may count as two characters. Break tags are cut from the text sent to eleven_v4 and gemini-3.8-flash-tts and are not counted there. There is no word-count limit. Duration and price are estimates until measured. |
| voice_pace | optional | Narration speed; 1 is the voice's own. gemini-3.8-flash-tts gets a style hint instead; eleven_v4 has no speed, so pick a voice by list_voices words_per_second; both named in warnings[]. |
| pause_trim | optional | Cut pauses past these s; false keeps all. Default with templates, scenes, shots: 0.15 sentence, 0.08 comma. |
| end_hold_s | optional | Seconds the last frame stays after the last word: the presenter freezes and every layer on screen then stays. Production seconds, not presenter seconds. Needs script and long-form qualification. |
| visible_ai_label | optional | Visible AI label for newly marked presenter videos, including long form; defaults to true in the server admission policy. False hides only the visible label and does not disable invisible marking or signed provenance. Historical outputs are unchanged. |
| background | optional | Accepted values: white | blur | contain. |
create_actor. Create a personal actor for this account from words describing a fictional adult: a 9:16 portrait with exactly one face, plus the other requested formats edited from it. Returns a run_id immediately; poll get_run until 'succeeded' (created_actor.actor_id, then pass it as actor_id to make_ugc) or 'failed'. Each published image is charged at the price quote shows; refused descriptions and unusable portraits cost nothing. When generation is switched off the call fails with actor_generation_disabled. from_actor_id keeps the face (checked; a mismatch costs nothing). With style_reference a portrait that looks like a person in the photo is refused at no charge, and the price is the same. portrait (with gender and approximate_age) becomes the 9:16 portrait at no charge, only the other formats are charged; an image without exactly one face, or with the face cut off, too small or too large, is refused at no charge. A brand mascot drawn with generate_image can be that portrait when its face reads as a human face (a cartoon person); an animal or object mascot is refused, so show it in inserts instead.
| Field | Required | What it means |
|---|---|---|
| description | optional | Words describing a fictional adult: appearance, clothing, setting. Naming a real person or a likeness to one is refused before any charge (actor_prompt_refused). Required unless from_actor_id or portrait is given; with from_actor_id, the look, clothing and pose of the view, while gender and age stay the source's. |
| gender | optional | female | male. Fixes the actor's gender and the default voice of videos with this actor. Required unless from_actor_id is given. |
| approximate_age | optional | Approximate age in years, 18 to 90: actors are adults. Required unless from_actor_id is given. |
| name | required | Name shown in list_actors. |
| aspect_ratios | optional | Formats to create: 9:16 | 1:1 | 16:9, always including 9:16. Omitted means all three. Formats that fail the identity check are not charged and are named in warnings. |
| from_actor_id | optional | Derive a new personal actor from this one (a library actor or your own): the same face, gender and age, with the view changed by framing, holding, setting, description or style_reference. Omit gender and approximate_age. |
| framing | optional | The shot size: close (default, chest-up), wide (waist-up with hands, room above) or full (standing, head to feet). From words it frames the new portrait; with from_actor_id, wide or full moves the camera back. The view is filed by the face size drawn, and make_ugc renders it with presenter_framing of that name. Not with style_reference or portrait, whose image sets the shot. |
| holding | optional | Up to 2 objects the presenter holds, one per hand. |
| setting | optional | The room or background to put the presenter in, in words. |
| style_reference | optional | A PNG or JPEG photo whose set, light, camera angle, shot size and clothing the new portrait follows: chest-up, waist-up or full height as in the photo; a portrait wider than chest-up becomes a wide view. An image asset of this account (upload, URL import or a found picture, rights_confirmed). Without from_actor_id the face is new, from description; with it, the source actor's face. A portrait that looks like a person in the photo is refused at no charge. |
| portrait | optional | Make the actor from your own portrait image instead of generating one: a PNG or JPEG image asset of this account (rights_confirmed), within 2% of 9:16, showing exactly one face that is neither cut off nor too small or too large. It becomes the actor's 9:16 portrait at no charge; the other formats are edited from it and charged as usual. With gender and approximate_age; not with description, style_reference or from_actor_id. |
| quality | optional | Image quality: medium | high. Omitted means medium. high draws the most natural portrait. The price per image depends on it; quote shows it before any charge. |
make_faceless. Start a paid finished faceless video from a script or brief. You select 30–90 seconds; the video ends with its narration, runs at least 25 seconds (an explainer, as long as its narration) and at most a quarter past the selection, never beyond 90. The video has an opening animated clip and image scenes. Captions are on by default and can be turned off.
It answers a run_id at once; poll get_run with its poll.arguments until 'succeeded' (video_url) or 'failed'. attempt=2,3,… starts a NEW run for the same input.
| Field | Required | What it means |
|---|---|---|
| input_mode | required | 'script' narrates your exact text; 'brief' writes the narration from a short description. Send exactly one of script or brief, matching this mode. |
| script | optional | The narration, read as written. Required when input_mode is 'script'; must be absent when it is 'brief'. |
| brief | optional | What the video is about; the narration is written from it. Required when input_mode is 'brief'; must be absent when it is 'script'. |
| duration_seconds | required | Selected length, 30 to 90 seconds, on a 1/25 s frame boundary. The video ends with its narration and runs at least 25 seconds (an explainer, as long as its narration), usually at most a quarter past the selection and 90 seconds; when the narration takes longer, the video runs to its end. A video more than 5 seconds longer than the selection says so in warnings[]. The charge never exceeds the quote. A script is accepted when its estimated length is 21 seconds to the selection plus 5, at most 90. |
| style_reference | optional | Public https image whose visual style the scenes follow. |
| character_reference | optional | Public https image of a person or figure to keep consistent across scenes. |
| character_details | optional | Fixed visual details of that person or figure, such as hair, clothing and absent accessories; kept in every generated scene. |
| scene_images | optional | Your images, each placed at a word range or quote of the narration. |
| captions | optional | Burned-in captions; on by default. |
| transitions | optional | Flash or light leak on the cuts, with its sound: every scene change, or the one nearest each anchor (script only). Omitted on a brief: a flash on every scene change; false for plain cuts. No extra charge. |
| edit_sounds | optional | A low sound on every scene change, its peak on the cut; template_sounds.edit picks it. Price unchanged. |
| template_sounds | optional | false for no sounds, or { flash, whoosh (light leak), edit: name } from list_template_sounds. |
| music | optional | Background music under the voice, made for this video; off unless you ask. "auto" takes the mood from the script; { mood } says it in your words, such as "calm lo-fi". 60 credits per video, its own line on the quote, charged only when the video is delivered with it. |
| templates | optional | Script only: as make_ugc, a slot may show your image or video asset (asset_id, rights_confirmed). Free. |
| shots | optional | Script only: 1-10 {id, anchor, action, asset_id?}, 5 s max each. |
| scenes | optional | Script only: as make_ugc scenes. |
| motion_style | optional | As make_ugc motion_style; spans script only. Free. |
| moves | optional | Script only: Moves from list_moves on your words: each { name, anchor: { quote }, cover_words?, placement?, data? }. Each is drawn as its line in list_moves says; data takes the fields it names. A move that cannot be drawn on its words or in this video is named in warnings[] and left out, never dropped silently. Its credits are on the quote. |
| format | optional | 'explainer': a drawn explainer cartoon, an event on every word, a hero in the corner; script only. It fits a chain of concrete actions told by one narrator: a how-to by steps, how something works, a workflow, a tool walkthrough, N mistakes or N ways, with something to show on every phrase. Reasoning without actions, a dialogue or a story about people fits poorly. Every script quote answers explainer_fit: offer the format where it fits; where it does not, show its reason and its rewrite into actions before asking. The hero is drawn from character_reference or character_details (its pose pictures on the quote), else ours, free. A list of named things gets a picture of each in its scene, one for each thing on the quote, charged only when shown. Without voice, an English script is read by our fastest voice. |
| pause_trim | optional | Script only: as make_ugc pause_trim, same default with templates, scenes, shots or format explainer. |
| voice | optional | Narration voice: a name from list_voices with skill=make_faceless (kind faceless_voice). Omitted means narrator_en_wise_lady; for an English explainer, the fastest voice. Choose a voice in the language of the narration: a voice reads any language, with the accent of its own. The API refuses an unknown name and a make_ugc voice before any charge. A raw voice_id is not accepted here. |
- A faceless video has no on-camera presenter: narration plays over an opening animated clip and image scenes. Send exactly one of script and brief, the one input_mode names.
- duration_seconds is a target, not a length the video is stretched to: the video ends with its narration, which runs at least 25 seconds (an explainer, just its narration) and usually at most a quarter past duration_seconds and 90 seconds; a longer narration runs to its end. A video more than 5 seconds longer says so in warnings[]. The charge never exceeds the quote.
- Captions drop punctuation, keep accents and other diacritics, and show numbers as written. With captions on, the script is checked against the caption font before speech is made; a character the font cannot draw fails the run without a charge.
- The narration language is detected automatically.
- voice names the narration voice: take a name from GET /v1/voices?skill=make_faceless. Without it the narration is read by narrator_en_wise_lady, and an English explainer by the fastest voice. Choose a voice in the language of the narration. An unknown name, a make_ugc voice and voice_id are refused before any charge: make_faceless speaks only the voices listed for it.
- make_faceless always delivers 1080p in 9:16. When faceless generation is switched off on the server, both of its calls answer paid_render_disabled and nothing is charged.
- A failed run whose error begins with story_not_planned stopped at its first step: the narration could not be planned on that attempt. Nothing was charged, and the same input can be run again under a new Idempotency-Key.
For make_ugc and create_actor, output format follows the request and the source. Supported formats are 9:16, 1:1, 16:9 and resolutions 720p, 1080p; silence means 1080p in 9:16.
Workflows
GET /v1/public/workflows lists which methods can run. The native methods are ugc-product-intro, ugc-faq-answer, faceless-explainer and product-image-story. website-proof-demo and kinetic-launch cannot run yet and remain execution_blocked with reasons.
The shortest path with the CLI starts from a method's own example. Only plan-run spends credits; every command before it is free. Edit the brief before creating the plan, and keep the method pins.
clipwright workflow-show faceless-explainer > method.json
jq '{document: {v: 1, method: {method_id: .descriptor.id, version: .descriptor.version, content_digest: .descriptor.content_digest, capability_digest: .capabilities.content_digest}, brief: .descriptor.example.brief, beats: .descriptor.example.beats, claims: [], assets: []}}' method.json > create.json
clipwright plan-create --file create.json > plan.json
jq '{revision, content_digest}' plan.json > approval.json
clipwright plan-approve wfp_example --file approval.json
clipwright plan-quote wfp_example > quote.json
jq '{revision, content_digest, semantic_input_digest, quote_id, idempotency_key: "workflow-example:1"}' quote.json > launch.json
clipwright plan-run wfp_example --file launch.json
clipwright runs run_exampleReplace wfp_example with the plan_id from plan.json and run_example with the run id plan-run prints. A quote expires five minutes after it is issued, so run plan-run soon after plan-quote. For product-image-story, upload each photo with clipwright upload --json. The response gives its upload_id and content_digest; copy them directly into the asset. Add one asset per photo. The uploads command returns the same identity for an earlier upload.
clipwright upload mug-front.jpg --json > upload.json
jq '{instance_id: "mug-front", asset_id: .upload_id, content_digest, anchor: {quote: "overall shape"}, provenance: "owner_supplied", rights: "owner_authorized", purpose: "product_proof"}' upload.json > asset.json{
"instance_id": "mug-front",
"asset_id": "upl_00000000000000000000000000000000",
"content_digest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"anchor": {
"quote": "overall shape"
},
"provenance": "owner_supplied",
"rights": "owner_authorized",
"purpose": "product_proof"
}- Create an account-owned plan, save revisions and approve the exact revision and content_digest. Planning and approval charge nothing and start no render. An edited revision needs new approval.
- For an approved plan, POST an empty body to /v1/workflow-plans/{id}/quote. Use its quote_id, revision, content_digest and semantic_input_digest in the run request. Check expires_at before starting a new purchase; it is quote metadata. Idempotency-Key must equal the body's idempotency_key.
- UGC workflow quotes disclose in native_quote.quote.warnings that duration_estimate_sec and credits_estimate are estimates, not a fixed price. Delivered short UGC is charged by its price_lines on the measured video, each rounded up to whole seconds, so a video longer than estimated is charged more than the estimate. Read credits_charged on the completed run. A native quote with long_form_pricing instead uses verified delivered face intervals and its tariff frozen at admission, not total video seconds. Native failure terms still apply.
- The native tariff and failure rules still apply. Faceless duration_seconds selects the duration whose quote caps the charge; measured narration determines delivered duration. Voice, captions and the exact accepted script keep their native settings.
- Bind each supplied image with a distinct instance_id, stable owned upload asset_id, content_digest, rights, provenance, purpose and a nonoverlapping word range or unambiguous quote. Reuse one upload through several instances; temporary signed URLs are resolved at quote and launch.
- Original product proof occupies supported IMAGE shots. The generated opener is illustrative. An exact still-only slideshow, a customer-only opener and arbitrary scene layouts are unavailable when required.
- A protected_area is a rectangle in source fractions. Preflight must preserve it through crop, PAN motion and captions. Unproven image orientation or placement is refused; a generated image never replaces required original proof.
- Protected PNGs with animation or EXIF chunks and JPEGs with any APP1 (EXIF/XMP) segment are refused. Export an upright static copy without that metadata. Coverage uses the native measured 25 fps frame clock.
- Upload retention and the 24-hour signed URL are independent limits; a quote extends neither. Sources and signatures must remain available for rendering and recovery. Accepted replay retains its original sources and signatures.
- A successful launch returns the receipt and its existing native Run. Retry the same accepted request to retrieve that purchase, including after quote expiry or source signing becomes unavailable. Read progress and the delivered video through GET /v1/runs/{id}.
- Private plans belong to the token's account.
- For product-image-story, generated scenes and the opener image use the first two distinct eligible product_proof originals in accepted asset order. The first anchors the product's shape; the second supplies another view. Plan readiness and the native quote disclose the selected count and that generated illustrations may differ. Additional originals keep their exact bound image placements. Recovery of an older paid checkpoint reuses its existing scenes with an explicit warning when original-product conditioning cannot be verified. Revisions and candidates, including kept candidates and later revisions, retain that known conditioning limitation without promising that the old scenes or placements survive the edit.
- Writes reject unknown fields and documents over 256000 JSON bytes. Reads accept future fields and enum values; unknown fields inside a read document are preserved, so an older client refuses an unsupported save instead of erasing intent. Keep accepted raw narration unchanged.
- After a lost save answer, repeat the same explicit base_version/document request. If it already became the current document, the backend returns that revision without resetting approval; a different current document produces workflow_conflict. SDK, MCP and CLI never approve, quote or buy as a side effect of saving.
- GET /v1/runs/{id} may include workflow: its immutable receipt, approved_plan, native state/warnings, actual_output, asset_placements, advisory beat_dispositions, validation_issues and creative_acceptance=not_reviewed. approved_plan leaves out brief.reference_profile, so a poll does not repeat it; what the plan does not reproduce from the reference is in warnings, and GET /v1/workflow-plans/{id} returns the full current revision. The run keeps the purchased revision even after the draft changes. A revision or shot candidate never inherits its ancestor's receipt.
- Verified faceless settlement supplies the artifact digest, format and measured duration even if later optional geometry is missing. Placements require an intact account/run/request-bound checkpoint; missing proof is null with a validation issue. A verified empty placement list is []. Verified UGC delivery supplies the measured final-file format, duration and digest; format comes from measured dimensions, not the request. UGC without byte/format proof, including older runs, has actual_output=null with output_evidence_unavailable; its native video_url remains the result. Historical evidence does not renew an expired download URL or prove human creative acceptance.
- SDK methods are listWorkflows, getWorkflow, createWorkflowPlan, planReferenceRemake, getWorkflowPlan, saveWorkflowPlan, approveWorkflowPlan, quoteWorkflowPlan and runWorkflowPlan; getRun preserves workflow evidence. CLI commands are workflows, workflow-show, plan-create --file, plan-show, plan-save --file, plan-approve --file, plan-quote and plan-run --file. These workflow commands print JSON; files are bounded to 1 MiB before parsing. A plan-show file can be edited and passed to plan-save for the same plan.
Example body for POST /v1/workflow-plans/{id}/run: replace the quote fields with your approved plan's quote and send Idempotency-Key: workflow-example:1.
{
"quote_id": "wq_example",
"revision": 1,
"content_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"semantic_input_digest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"idempotency_key": "workflow-example:1"
}Starting a run
A paid call carries one header besides the key: Idempotency-Key. POST /v1/skills/make_faceless/run and POST /v1/runs/{id}/revisions and POST /v1/runs/{id}/candidates and POST /v1/skills/make_ugc/run and POST /v1/skills/create_actor/run and POST /v1/workflow-plans/{id}/run and POST /v1/skills/generate_image/run and POST /v1/skills/generate_video/run and POST /v1/reference-analyses and POST /v1/creator-profiles and POST /v1/trend-searches require it, and a call without it is refused with 400 idempotency_key_required before anything is charged.
- You choose the key, and it is the only thing that tells a retry from a second order. Any unique string will do; keep it for as long as you might resend the call.
- The same key with the same body returns the run it already started and charges nothing a second time. That is what makes an ordinary retry safe.
- The same key with a different body is refused with 409 idempotency_key_reused. Take a new key for a new request instead of editing a request under a key already spent.
- To start a deliberately new run on the same input — a retry after a failure — send a new key. The run you already paid for stays where it is.
- @clipwright/sdk and @clipwright/mcp-server build the key for you out of the client and the input, and turn attempt=2, 3 … into a new one. Over plain HTTP the key is yours to pick.
When a call fails
Every refusal carries an error object with a code and a message. What to do with it follows from the kind of refusal, not from the text:
| Refusal | HTTP | Repeat the same call? | What to do | Charged |
|---|---|---|---|---|
| rate_limited | 429 | yes, after the wait | Back pressure, not an error: the response names the seconds to wait, in Retry-After and in the body. | Nothing. The call was not taken. |
| server_error | 500, 502, 503 | yes, after the wait | The failure is on the server side. Do not start a second run with a new idempotency key: the same call is the retry. | Not by this answer. A run the server could not start gets its credit hold back; a run that did start is billed once, by its own outcome. |
| insufficient_credits | 402 | no, it gives the same answer | Stop and tell the person the balance and the price; both are in the body. Repeating cannot change either. | Nothing. The call was refused before it created a run or held any credits. |
| debt_outstanding | 402 | no, it gives the same answer | Stop. Buying credits clears the debt before anything reaches the balance, and that lifts the block. | Nothing. The call was refused before it created a run or held any credits. |
| not_admitted | 403 | no, it gives the same answer | Stop. The account has no beta access; neither a retry nor a purchase changes that. Contact support. | Nothing. The call was refused before it created a run or held any credits. |
| edit_conflict | 409 | no, it gives the same answer | Stop. The draft of the edit is at another version than the save was written against; the body names it. Read the draft: if it already holds your document, an earlier save went through. Otherwise apply the change to what you read and save with its version. | Nothing. The call was refused before it created a run or held any credits. |
| run_not_editable | 409 | no, it gives the same answer | Stop. The run has no draft of an edit to read or to save. The body names the state and the reason, the same ones the shots of the run state. | Nothing. The call was refused before it created a run or held any credits. |
| editing_closed | 503 | no, it gives the same answer | Stop. Paid edits are not open; the body names why. Nothing was charged and no run was created, and paid_edits on the run's shots says when they open. | Nothing. The call was refused before it created a run or held any credits. |
| edit_ceiling_reached | 409 | no, it gives the same answer | Stop. The original has as many revisions, or as many shot candidates (each counted apart), running, or failed and unfinished, as it may; the body names the limit, the count and the runs. Follow a running one with GET /v1/runs/{id} and start the next after it finishes; support raises the failure limit of revisions after a failure that was ours. | Nothing. The call was refused before it created a run or held any credits. |
| edit_invalid | 422 | no, it gives the same answer | Stop. The document, or the operation a shot candidate asks for, breaks rules of the run it edits; issues lists each in order. Fix the request: the rules are in the run's shots. | Nothing. The call was refused before it created a run or held any credits. |
| workflow_invalid | 400 | no, it gives the same answer | Stop. Read every issue and warning, then correct the workflow document. Approval, quote and paid launch are separate explicit calls. | Nothing. The call was refused before it created a run or held any credits. |
| workflow_conflict | 409 | no, it gives the same answer | Stop. Read the saved plan and compare its revision and document with your original request. Recover an uncertain launch with the original request and idempotency key; a new key can pay twice. | Nothing for this call. A launch that went through earlier is billed once, by its own outcome; the original request and key return it. |
| workflow_not_ready | 409 | no, it gives the same answer | Stop. Read every issue and warning. Resolve readiness, then explicitly approve and quote the chosen revision before a keyed paid launch. | Nothing. The call was refused before it created a run or held any credits. |
| client_error | 400, 401, 403, 404, 409, 410, 413, 415, 422, 503 | no, it gives the same answer | Stop. The request itself was refused: read the message, fix the call, then send it again. | Nothing for this call: it created no run and held no credits. A run started earlier under the same Idempotency-Key (409 idempotency_key_reused) is billed once, by its own outcome. |
These are all the codes the API puts in error.code. A code you have not seen before still follows its row above, because the row is chosen by the status:
- account_not_admitted
- actor_creation_limited
- actor_format_unavailable
- actor_generation_disabled
- actor_in_use
- actor_storage_unavailable
- actor_unavailable
- analysis_enqueue_failed
- aspect_conflict
- asset_cap_exceeded
- asset_ingest_cap_exceeded
- asset_not_found
- asset_not_ready
- assets_disabled
- creator_profile_limited
- creator_profile_not_found
- creator_url_unsupported
- debt_outstanding
- deletion_held
- edit_ceiling_reached
- edit_conflict
- edit_invalid
- editing_disabled
- editing_not_priced
- editing_window_not_set
- feedback_limited
- generations_disabled
- generations_marking_pending
- idempotency_key_required
- idempotency_key_reused
- ingest_enqueue_failed
- insufficient_credits
- insufficient_scope
- internal_error
- invalid_image
- invalid_request
- malformed_body
- no_changes
- not_found
- paid_render_disabled
- payload_too_large
- rate_limited
- reference_analysis_disabled
- reference_analysis_limited
- reference_analysis_not_found
- reference_not_analyzable
- reference_not_usable
- reference_url_unsupported
- rejected_field
- revision_needs_generated_unit
- rights_not_confirmed
- run_media_in_use
- run_not_editable
- run_not_finished
- run_not_found
- script_encoding_lost
- source_aspect_mismatch
- source_not_usable
- trend_search_limited
- trend_search_not_found
- trends_unavailable
- unauthorized
- unknown_field
- unsupported_combination
- unsupported_media_type
- unusable_source
- upload_cap_exceeded
- upload_digest_mismatch
- upload_expired
- upload_in_use
- upload_not_finished
- upload_size_mismatch
- upstream_error
- url_fetch_busy
- url_fetch_failed
- url_fetch_rate_limited
- url_fetch_refused
- url_import_disabled
- workflow_conflict
- workflow_invalid
- workflow_not_ready
Limits
- 60 paid requests and 300 free ones per 60 seconds. The window is counted per account, not per key, so extra keys buy no extra throughput.
- 3 videos with a presenter and 2 faceless videos are made at once per account; the rest queue and are not refused. When many accounts render at once, a run may also wait its turn behind theirs.
- A refusal by rate limit names the seconds to wait in Retry-After and in the body. Honour the larger of the two.
- 99 inserts per clip, and at most 16 appearances of the actor between them. Both are counted from the word indexes you send, so an input that asks for more is refused before anything is paid for.
- cover_words says how many spoken words an insert covers, counted from the first word of its anchor. The insert ends where the first uncovered word begins, so two inserts whose coverage meets are adjacent and leave no actor shot between them.
- The share of words you leave uncovered decides the share of the clip that shows a face, and it does not move with the voice's speed. Word length does vary: at a 560-word script, asking for 19% delivers about 16 to 22; a shorter script scatters wider, and a different voice moves it.
- Two single-word choices change the price, not just the look. An insert anchored at word 0 owns the silence before the first word; anchored at word 1 it leaves an extra appearance of the actor, and every appearance adds to the price. Coverage that reaches the last word takes the clip to its end and removes the closing appearance the same way.
- A quote reports the share as estimatedFaceWordShare. Read that field; do not divide estimatedFaceSeconds by estimatedTotalDurationSec. Those two answer different questions — the first is the reserve we hold at the slow end of the speaking range, the second is how long the clip is expected to run — and their ratio is not the share of anything.
What this API will never do
- Publish anything. We return a file and a signed link; where it goes is yours to decide.
- Cancel a started run. There is no endpoint for it.
- Accept these make_ugc fields: character, broll_url, disclosure_overlay, webhook_url. They are refused by name before any charge, not accepted and quietly ignored.
- Change the format or the resolution you asked for without saying so. A mismatch is either snapped with a warning or refused before the paid call.
- Call you back. There are no webhooks: read the run with GET /v1/runs/{id}.
- Show a key a second time, or recover one from a backup.
Also worth knowing
- Warnings, not silence. Anything we could not honour comes back in warnings[] on the same run, named. A parameter never disappears without a line about it.
- An MCP server. @clipwright/mcp-server exposes the same contract as tools, and its tools/list is the machine-readable form of this page.