Clipwright API
One HTTP API that turns a script into a lip-synced UGC video. 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.
- Credits are bought in packs: 1000 credits for $10.00, one payment, no subscription.
Ask before you spend: the quote endpoint of either 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.
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.
Endpoints
| Endpoint | Costs credits | What it does |
|---|---|---|
| GET /health | no | Liveness of the API itself. Answers without a key. |
| GET /v1/voices | no | Voices you can name in voice or voice_id. |
| 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. |
| 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 and create_actor accept. |
A run of either 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. Count includes spaces, audio tags and stress marks; emoji may count as two characters. 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 receives 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). 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. 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. Mutually exclusive with voice. |
| tts_model | optional | Speech model: eleven_v3 | eleven_flash_v2_5 | eleven_turbo_v2_5. Omitted means the model of the chosen preset (list_voices shows it; every preset speaks eleven_v3) or eleven_v3 for a raw voice_id. eleven_v3 is the most expressive and the only one that reads 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. Script limits by speech model: eleven_v3: 5000 characters; eleven_flash_v2_5: 10000 characters; eleven_turbo_v2_5: 10000 characters. Count includes spaces, audio tags and stress marks; emoji may count as two characters. There is no word-count limit. Duration and price are estimates until measured. |
| disclosure_overlay | optional | Accepted values: true | false. |
| 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. |
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_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. |
| client_error | 400, 401, 404, 409, 413, 415 | 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
- idempotency_key_required
- idempotency_key_reused
- insufficient_credits
- internal_error
- invalid_image
- invalid_request
- malformed_body
- not_found
- paid_render_disabled
- payload_too_large
- rate_limited
- rejected_field
- 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 renders run at once per account; the rest queue and are not refused.
- 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 fields: character, broll_url, 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.