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.
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 in the dashboard at https://app.clipwright.io/api-keys. 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: 30 credits for every second of finished video, rounded up to the whole second.
- make_ugc with segments or inserts: 10 credits for every second a face is on screen, and at least 400 credits on a video we delivered. Seconds without a face cost nothing, and a run that delivers no file costs nothing at all, even when the vendor was already paid. Face time is added up across the whole video and rounded up once, not per segment. These fields need long-form qualification on the deployment; where it is off they are refused by name before any charge.
- create_actor at quality medium: 10 credits for the portrait and 10 for each additional format.
- create_actor at quality high: 20 credits for the portrait and 20 for each additional format.
- make_faceless: 350 credits for a delivered video of up to 66 seconds, rising with length to 420 at 90 seconds. You pay for the length delivered, never more than the quote for your duration_seconds, and captions do not change the price.
- Credits are bought in packs: 1000 credits for $10.00, one 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, from 25 seconds up to a quarter past that length (never past 90), and 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 delivering work reached the vendor is charged. One that failed before it costs nothing, and so does one we stopped, lost or refused ourselves, even when the vendor was already paid. On the face meter no failure is charged at all.
- create_actor: a failed run costs nothing at all, even when the vendor was already paid, 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.
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 /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/account | no | Balance, debt and holds of the account behind the key. |
| 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. Paid edits are not open yet: 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. Paid edits are not open yet: it answers editing_disabled. |
| 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/actors | no | Actors saved on the account, with the id make_ugc takes. |
| 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. |
| 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 the https url that make_ugc, create_actor and make_faceless accept. |
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.
Skills and their input
make_ugc. Start generation of a lip-synced UGC video. Give a script within the selected speech model's text limit; 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. Fields the renderer does not honor yet carry a NOT HONORED YET note in their own description — read it instead of guessing.
Call quote_ugc before generating and show the cost. This does NOT wait for the video: it starts the run and returns a run_id IMMEDIATELY. You MUST then poll get_run with that run_id until the state is 'succeeded' (video_url) or 'failed'. A 'failed' run whose paid vendor job we still hold can go back to 'queued' and reach 'succeeded' later; whenever that happens it is named in warnings[]. Pass attempt=2,3,… to deliberately start a NEW run for the same input (retry after a failure).
| 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_v3, 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 inserts over full narration, each covering cover_words spoken words from its anchor; requires long-form qualification on the server, captions=false, 1080p and explicit broll_policy=anyone. |
| 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. |
| 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 | STORED ONLY: Saved policy for B-roll: anyone allows people including the actor; no_actor excludes the actor; no_people excludes all people, including hands. Segmented media generation is closed. This setting is stored only and has no effect on actor-only videos. Run override wins over the account actor default; otherwise no_people. |
| captions | optional | NOT HONORED YET: captions requested but not rendered in this prototype (stage-B) |
| caption_style | optional | NOT HONORED YET: caption_style is not honored: captions are not rendered in this prototype (stage-B) |
| 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, and a source of another shape is snapped to 9:16 with a warning — 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 | 4k (short side 720 / 1080 / 2160 px). Omitted means 1080p. |
| voice | optional | Voice name from list_voices. Curated presets: owner_ru_clone | sarah | george | eric | daria_ru_female (owner_ru_clone is the Russian cloned voice). 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): eleven_v4 for the Russian presets and for catalog voices named ru_*, gemini-3.8-flash-tts for its own voices, eleven_v3 for every other preset and catalog voice. A raw voice_id speaks eleven_v4 when the script is mostly Cyrillic and eleven_v3 otherwise. An explicit tts_model always wins. eleven_v4, eleven_v3 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 an eleven_v3 and eleven_v4 convention 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. |
| 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.
| Field | Required | What it means |
|---|---|---|
| description | required | 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). |
| gender | required | female | male. Fixes the actor's gender and the default voice of videos with this actor. |
| approximate_age | required | Approximate age in years, 18 to 90: actors are adults. |
| 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. |
| quality | optional | Image quality: medium | high. Omitted means medium. 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 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.
Call quote_faceless first and show the user the price. This does NOT wait for the video: it starts the run and returns a run_id IMMEDIATELY. Then poll get_run with that run_id until the state is 'succeeded' (video_url) or 'failed'. Pass attempt=2,3,… to deliberately start 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, runs at least 25 seconds and may exceed the selection by up to a quarter, never beyond 90 seconds; a video more than 5 seconds longer 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 | Optional public https image whose visual style the scenes follow. |
| character_reference | optional | Optional public https image of a person or figure to keep consistent across scenes. |
| character_details | optional | Optional fixed visual details for the referenced person or figure, such as hairstyle, clothing and absent accessories; applied to every generated scene. |
| scene_images | optional | Optional images of your own, each placed at a word range or quote of the narration. |
| captions | optional | Burned-in captions; on by default. |
| voice | optional | Narration voice: a name from list_voices with skill=make_faceless (kind faceless_voice). Omitted means narrator_en_wise_lady. 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 may run from 25 seconds to a quarter past duration_seconds (never past 90); 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. 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, 4k; silence means 1080p in 9:16.
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/skills/make_ugc/run and POST /v1/skills/create_actor/run 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 |
|---|---|---|---|
| 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. |
| 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. |
| 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. |
| 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. |
| not_admitted | 403 | no, it gives the same answer | Stop. The account has no beta access; neither a retry nor a purchase changes that. Ask the operator. |
| 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. |
| 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. |
| client_error | 400, 401, 404, 409, 413, 415, 422 | no, it gives the same answer | Stop. The request itself was refused: read the message, fix the call, then send it again. |
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
- aspect_conflict
- debt_outstanding
- edit_ceiling_reached
- edit_conflict
- edit_invalid
- editing_disabled
- editing_not_priced
- editing_window_not_set
- idempotency_key_required
- idempotency_key_reused
- insufficient_credits
- internal_error
- invalid_image
- invalid_request
- malformed_body
- no_changes
- not_found
- paid_render_disabled
- payload_too_large
- rate_limited
- rejected_field
- revision_needs_generated_unit
- run_not_editable
- script_encoding_lost
- unauthorized
- unknown_field
- unsupported_media_type
- unusable_source
- upload_cap_exceeded
- upstream_error
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.
- 49 inserts per clip, and at most 6 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% delivered 16 to 22 in nine hundred and ninety-seven simulated runs out of a thousand, and stayed inside 15 to 24 in fifty thousand. Those numbers were measured on this profile's voice and at that length; a shorter script scatters wider, and a different voice moves them.
- 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 is a separate paid job. 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: once the vendor has the work, stopping it on our side would not unspend 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.