Everything the Channel Farm dashboard can do, over plain HTTPS: scripts, brand profiles, voices, visual styles, characters, and rendered videos. Base URL https://api.channel.farm/v1. An OpenAPI spec is served at /v1/openapi.json. Prefer driving Channel Farm from an AI agent? Use the MCP server instead. Same account, same tokens.
Every request needs a personal access token in the Authorization header. Create one in your dashboard under Settings → Connect & API. The token starts with cf_live_ and is shown once, at creation.
Authorization: Bearer cf_live_... A token has full access to your account. Keep it server-side, never in a browser or a public repo. Revoke it from the same page if it leaks. The same token also works for the MCP server.
From idea to rendered video in four calls. First, check what your account can do:
curl https://api.channel.farm/v1/account \
-H "Authorization: Bearer $CF_TOKEN" curl -X POST https://api.channel.farm/v1/scripts \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"idea": "The hidden history of coffee", "target_length_minutes": 5}' curl https://api.channel.farm/v1/scripts/<script_id> \
-H "Authorization: Bearer $CF_TOKEN" curl -X POST https://api.channel.farm/v1/videos \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"brand_profile_id": "<id>", "script_id": "<script_id>", "pacing": "dynamic"}' You need at least one brand profile with a voice set before rendering. Create one with POST /brand-profiles or in the dashboard.
Errors are JSON with a stable code and a human-readable message. When subscribing or upgrading clears the error, the body also carries pricing_url.
{
"error": {
"code": "credit_quota_exhausted",
"message": "You've used all 750 credits in your Starter plan for this billing period. ...",
"pricing_url": "https://channel.farm/pricing",
"action_required": "subscribe_or_upgrade"
}
} 400 | invalid_argument | A parameter is missing or malformed. Also content_blocked when an image safety check rejects a style prompt. |
401 | unauthorized | Missing, invalid, revoked, or expired token. |
402 | payment | Subscription or credits required: subscription_required, credit_quota_exhausted, max_length_exceeded, brand_profile_limit_reached, and similar. Includes pricing_url. |
403 | forbidden | Not available on this account: feature_not_enabled, publishing_unavailable, account_blocked, and similar. Retrying does not help. |
404 | not_found | The resource does not exist or belongs to another account: script_not_found, video_not_found, route_not_found, and similar. |
409 | conflict | Valid request, wrong state: script_not_ready, brand_profile_needs_elevenlabs_voice (the profile has no voice that can narrate), character_not_approved, voice_in_use, and similar. |
422 | clone_rejected | Google refused a cloned voice recording. Record it again. |
429 | rate_limited | Too many requests, or a daily limit reached. Wait and retry. |
502 | upstream_failed | A service we call failed: image_fetch_failed, preview_failed, create_failed, and similar. Safe to retry. |
503 | unavailable | Switched off or busy for a moment: rendering_paused, voice_tags_unavailable, and similar. Retry later. |
5xx | server_error | Something failed on our side. Safe to retry with backoff. |
Rendering spends credits, the same currency as the dashboard. A render costs about minutes × rate for its pacing, never less than the minimum: 25 credits for a 16:9 video and 8 for a 9:16 one (GET /account has the live figures):
relaxed | 5 / min | Calm pacing. A new image roughly every 12 to 22 seconds. Stretches credits the furthest. |
dynamic | 7 / min | A new image roughly every 8 to 12 seconds. |
rapid | 9 / min | High energy. A new image roughly every 3 to 6 seconds. |
hyper | 12 / min | The fastest cut speed. The most images per video. |
Animated scenes cost extra on top of that. Each animated scene is charged by its length, so turning animation on can multiply the price of a long video. Send your settings to POST /scripts/validate to see the real figure before you spend anything.
Check your balance with GET /credits and preview a script's cost with POST /scripts/validate. Script generation is capped at 30 per hour and renders at 10 per hour. Reads are not metered. Rendering requires an active paid plan. Without one, reads and profile management still work.
Script generation and rendering are asynchronous. The create call returns immediately with an id. For videos, POST /videos also returns estimated_build_seconds and recommended_first_poll_seconds. Wait that long before the first poll, then follow next_poll_after_seconds from each GET /videos/:id response. Renders typically take 5 to 40 minutes depending on length and load.
Estimates use recent comparable generations and exclude time spent editing. With advanced editing enabled, the estimate ends at ready_to_edit; otherwise it ends when the video is rendered. Estimate fields may be null when timing data is unavailable; the polling recommendation still applies.
/account Plan, credits, quotas, and limits.
{
"subscription_tier": "starter",
"subscription_status": "active",
"can_render_video": true,
"plan": { "max_video_length_min": 15, "videos_per_month": 6 },
"credits": {
"allowance": 750,
"used": 210,
"remaining": 540,
"rollover": 0,
"period_end": "2026-09-01T00:00:00+00:00",
"bonus": { "balance": 0, "soonest_expiry": null, "purchased": 0, "gifted": 0 },
"rates": {
"per_minute": { "relaxed": 5, "dynamic": 7, "rapid": 9, "hyper": 12 },
"minimum_per_video": 25,
"minimum_per_short_video": 8,
"note": "A render costs about minutes × rate for its pacing, never less than the minimum..."
}
},
"script_quota": { "used": 3, "limit": 60, "remaining": 57, "unlimited": false },
"input_limits": { "idea_min_chars": 5, "idea_max_chars": 2000, "script_max_video_length_min": 15 }
} /credits Credit balance for this billing period.
The credits block on its own, for cheap balance checks between renders. allowance includes any rollover. bonus is gifted or topped-up credits, spent after the plan allowance. Null allowance means no active billing period.
{
"subscription_tier": "starter",
"can_render_video": true,
"allowance": 750,
"used": 210,
"remaining": 540,
"rollover": 0,
"period_end": "2026-09-01T00:00:00+00:00",
"bonus": { "balance": 100, "soonest_expiry": "2026-08-20T00:00:00+00:00", "purchased": 0, "gifted": 100 },
"rates": {
"per_minute": { "relaxed": 5, "dynamic": 7, "rapid": 9, "hyper": 12 },
"minimum_per_video": 25,
"minimum_per_short_video": 8,
"note": "A render costs about minutes × rate for its pacing, never less than the minimum..."
}
} /brand-profiles List brand profiles.
A brand profile is the reusable identity videos are rendered with: voice, visual style, captions, and optionally a cast of recurring characters. render_ready is true only when the profile has a voice that can narrate: an ElevenLabs voice, a Google voice, or one of your own. voice_tags says which delivery marks that voice performs (see GET /brand-profiles/:id/voice-tags).
{
"brand_profiles": [
{
"id": "9a1e...",
"name": "Sleepless Histories",
"is_favorite": true,
"language": "en",
"render_ready": true,
"voice": { "id": "41b0...", "name": "Archer", "provider": "elevenlabs", "custom": false },
"voice_tags": "eleven-v4",
"visual_style": { "id": "77aa...", "name": "Storybook Ink" },
"character": {
"id": "c3f2...",
"name": "Professor Owl",
"status": "approved",
"style_matches_profile": true
}
}
]
} /brand-profiles Create a brand profile.
name required | string | Profile name, 1 to 80 characters. |
description | string | Channel or niche description, up to 500 characters. |
voice_id | uuid | A voice from GET /voices. Required before the profile can render. |
visual_style_id | uuid | A style from GET /visual-styles or POST /visual-styles. |
language | string | The language its videos are written and narrated in, as a code like en or es. Default en. |
text_enabled | boolean | Word-by-word captions. Default true. |
caption_* | mixed | caption_font, caption_size, caption_color, caption_highlight_color, caption_shadow, caption_words_per_line, caption_uppercase, caption_position, caption_x_pct and caption_y_pct. See PATCH below. |
short_captions | object | Captions for 9:16 videos only. See PATCH below. |
voice_tuning | object | ElevenLabs voice dials. See PATCH below. |
voice_style | string | Google voices only: how the narrator delivers every line. See PATCH below. |
Counts against your plan's brand-profile limit. A new account gets its first profile free. Returns 201 with the new brand_profile_id.
/brand-profiles/:id Update a brand profile.
name | string | New name, 1 to 80 characters. |
description | string | New description, up to 500 characters. |
voice_id | uuid | A voice from GET /voices. A new voice starts from the default dials; a Google delivery style is kept. |
visual_style_id | uuid | A style from GET /visual-styles. |
language | string | The language its videos are written and narrated in. Videos already being made keep theirs. |
character_id | uuid | "none" | Make an approved character from GET /characters the lead, or "none" to take the lead out. Attaching also switches the profile to the character's own visual style. POST /brand-profiles/:id/characters sets the whole cast. |
text_enabled | boolean | Word-by-word captions on or off. |
caption_font | string | One of the installed caption fonts, for example Inter, Montserrat, Bebas Neue, Anton or Poppins. |
caption_size | number | 25 to 120. Captions that are off need text_enabled: true with it. |
caption_color | string | A hex code like "#ffffff", or white, lime, yellow, orange, red or blue. |
caption_highlight_color | string | The colour of the word being spoken. Same formats. |
caption_shadow | string | The outline behind the captions: none, soft, medium, hard or glow. |
caption_words_per_line | number | 1 to 10 words on screen at once. |
caption_uppercase | boolean | Captions in capitals. |
caption_position | string | Where captions sit: top, middle or bottom, then left, center or right (e.g. top-center). bottom-center is the normal spot and resets the position. Kept on screen in 16:9 and 9:16 videos. |
caption_x_pct | number | An exact spot instead: the caption box’s centre, 0 (left) to 100 (right). Pulled in as far as keeps the text on screen. |
caption_y_pct | number | The caption box’s bottom edge, 0 (top) to 100 (bottom). The normal spot is 92. |
short_captions | object | "inherit" | Captions for 9:16 videos only: any of enabled, font, size (44 to 213), color, highlight_color, shadow, words_per_line (1 to 6) and uppercase. Send only what should differ. "inherit" drops the override. |
voice_tuning | object | "reset" | ElevenLabs dials: stability, similarity_boost and style (0 to 1), and speed (0.7 to 1.2). Only the dials you name change. "reset" goes back to the defaults. |
voice_style | string | Google voices only: how the narrator delivers every line, up to 300 characters. An empty string removes it. |
An ElevenLabs voice at speed 1 renders on Eleven v4 Turbo, which uses stability and similarity_boost and performs voice tags. Any other speed moves it to multilingual-v2, which uses all four dials but would read a tag out loud, so it takes pauses only. Hear a setting before saving it with POST /voices/preview.
curl -X PATCH https://api.channel.farm/v1/brand-profiles/<id> \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"caption_size": 48, "voice_tuning": {"stability": 0.4}}' /brand-profiles/:id Every setting of one brand profile.
Every setting of one profile, in the same terms PATCH takes: the voice and the model it renders on, its tuning or delivery style, which voice tags it performs, the visual style, the captions for 16:9 and 9:16, and the cast. Read it before you change anything.
{
"brand_profile_id": "9a1e...",
"name": "Sleepless Histories",
"language": "en",
"render_ready": true,
"voice": { "id": "41b0...", "name": "Archer", "provider": "elevenlabs", "custom": false },
"voice_tuning": { "stability": 0.5, "similarity_boost": 0.75 },
"voice_style": null,
"voice_model": "Eleven v4 Turbo (stability and similarity_boost apply; voice tags work)",
"voice_tags": "eleven-v4",
"visual_style": { "id": "77aa...", "name": "Storybook Ink", "is_yours": false },
"captions": { "enabled": true, "font": "Inter", "size": 35, "color": "#ffffff", "highlight_color": "#00ff00", "shadow": "soft", "words_per_line": 4, "uppercase": false,
"position": { "x_pct": 50, "y_pct": 23.9, "max_width_pct": 96, "quick_position": "top-center" } },
"short_captions": null,
"cast": [
{ "id": "c3f2...", "name": "Professor Owl", "enabled": true, "lead": true, "mention": "@professor-owl", "style_matches_profile": true }
]
} /brand-profiles/:id Delete a brand profile for good.
Permanent. It also deletes the characters that were created on this profile and the scripts written for it, and the response counts them. Videos already made are kept. Refused with a 409 while one of its videos is still being made.
/brand-profiles/:id/characters Set a profile’s cast of characters.
characters required | array | The whole list, in order: [{ "id": "<character id>", "enabled": true }]. enabled defaults to true, and at most 4 can be on. [] means no cast. |
The whole cast of recurring characters, replacing what was there. The first one switched on is the lead. Every new video from the profile uses the switched-on cast, and character_ids on POST /videos can pick a different cast for one video. Each must be your own approved character. One that is switched on must be drawn in the profile’s visual style; one switched off can stay on the list.
/brand-profiles/:id/voice-tags The voice tags a profile’s voice performs.
Which delivery marks this profile’s voice performs, with every one of them and a hint. Write them into the script you render. They are performed, never spoken, and never shown in the captions.
eleven-v4 | [tags] | ElevenLabs voices: tags in square brackets, like [whispers] or [pause]. |
google | <cues> | Google voices: cues in angle brackets, like <breath> or <short pause>. |
pauses | <break /> | An ElevenLabs voice with a custom speed: pauses only, like <break time="1s" />. |
A set of null means this voice is offered no marks right now: write plain narration. POST /scripts/voice-tags writes them in for you.
/characters List recurring characters.
A recurring character looks the same in every video it appears in. Draw one with POST /characters, save it with POST /characters/:id/approve, and set which characters a profile uses with POST /brand-profiles/:id/characters. This list shows each character and the profile casts it is in.
{
"characters": [
{
"id": "c3f2...",
"name": "Professor Owl",
"status": "approved",
"attachable": true,
"mention": "@professor-owl",
"preview_image_url": "https://...",
"visual_style_id": "77aa...",
"visual_style_name": "Storybook Ink",
"created_for_brand_profile_id": "b1d0...",
"in_casts": [{ "brand_profile_id": "b1d0...", "brand_profile_name": "History Owl", "enabled": true, "lead": true }],
"created_at": "2026-07-25T18:12:00+00:00"
}
]
} /voices Browse narration voices.
provider | string | elevenlabs or google. Both if omitted. |
language | string | Only voices for this language, as a code like en or de. |
gender | string | male, female or neutral. |
accent | string | Substring match, for example american or british. |
niche | string | Voices suited to this kind of channel. |
search | string | Name or description search. |
yours_only | boolean | Only the voices you designed or cloned. |
limit | integer | 1 to 50. Default 12. |
offset | integer | Skip this many. Take next_offset from the previous page. |
About 1,200 ElevenLabs voices, over 2,000 Google voices in many languages, and your own voices (is_yours). Featured voices come first. A voice with a null preview_url gets one from POST /voices/preview. Use the id as voice_id.
/voices/preview Hear a voice.
voice_id | uuid | A voice from GET /voices. Or send brand_profile_id. |
brand_profile_id | uuid | Hear this profile’s voice with its saved settings. |
voice_tuning | object | ElevenLabs only: hear these dials before saving them. stability, similarity_boost and style 0 to 1, speed 0.7 to 1.2. |
voice_style | string | Google only: hear this delivery style before saving it. Up to 300 characters. |
Returns an audio_url: the same sample line, in the voice’s own language, on the model your videos render with. With no dials or style to try, a library voice returns its standard sample. Free. Limited to a few a minute and a daily number.
/voices Design or clone a voice of your own.
mode required | string | design to describe a voice, or clone to use your own recordings. |
name required | string | The voice’s name, 1 to 40 characters. |
description | string | design only: 20 to 600 characters on how it sounds (age, pitch, pace, accent, warmth), not what it says. |
gender | string | design only: male, female or neutral. |
language | string | design only: the language it speaks best, as a code. Default en. |
source_audio_base64 | string | clone only: a WAV of 10 to 30 seconds of you speaking naturally, as base64, up to 4 MB. |
consent_audio_base64 | string | clone only: a WAV of 3 to 20 seconds of you reading the consent sentence below exactly, as base64, up to 4 MB. |
attest_own_voice | boolean | clone only: must be true. Cloning is for the account holder’s own voice only. |
A Google voice only your account can use. Starter plan and up. Each voice costs 15 credits, taken when it starts and refunded if it fails. Each plan includes a number of voices: 1 on Starter, 2 on Developmental, 3 on Professional. The response has a sample_url to listen to.
I am the owner of this voice and I consent to Google using this voice to create a synthetic voice model. /voices/:id Delete one of your own voices.
Deletes one of your own voices for good, with its recordings. Refused with a 409 while a brand profile still uses it: change the voice on those profiles first. The credits it cost are not returned. Videos already made keep their narration.
/visual-styles List visual styles.
The stock style catalog plus your own custom styles (is_yours). Each has a sample image_url showing the look. Filter with ?category=.
/visual-styles Create a custom visual style.
name required | string | Style name, 1 to 80 characters. |
style_prompt required | string | The art direction appended to every image prompt, 20 to 2000 characters. Describe medium, technique, palette, lighting, and mood. Never a subject. |
description | string | One line shown in the picker, up to 300 characters. |
image_rules | string | Hard constraints for every image, up to 2000 characters. Do not name an aspect ratio: one style serves 16:9 and 9:16. |
figure_law | string | How people are drawn in this style, in a sentence or two. Up to 400 characters. |
reference_images | array | 1 to 3 example pictures of the look, each {"url": "https://..."} or {"base64": "..."}. JPG, PNG or WebP, under 5 MB. They steer every scene drawn in this style. |
image_url | string | Optional card art. Omit it and your first example picture is the card; with no pictures either, a card is rendered from style_prompt. Or send image_base64 with image_mime_type. |
Custom styles are private to your account, and a paid feature. Limit 20. The pictures steer the look of every scene: the example pictures when you send them, and otherwise the card art. Three example scenes are drawn in the background after it is saved.
/visual-styles/:id Change one of your own styles.
name | string | New name, 1 to 80 characters. |
description | string | Up to 300 characters. |
style_prompt | string | 20 to 2000 characters. |
image_rules | string | An empty string clears it. |
figure_law | string | An empty string clears it. |
Only your own styles. When the look changes on a style without example pictures, its card is drawn again and new example scenes follow. Videos already made keep their look. Example pictures cannot be changed: make a new style for a new set.
/visual-styles/:id Delete one of your own styles.
Deletes one of your own styles for good. A style that has already made videos cannot be deleted (409). Brand profiles still using it are named in the response and need another style before they can make new videos.
/scripts Generate a script from an idea.
idea required | string | The topic or idea, 5 to 2000 characters. |
brand_profile_id | uuid | The profile the video is for. Its language and voice shape the script. Recommended. |
target_length_minutes | number | 0.5 to 60, never past your plan’s max length. Default 8. |
style | string | first-person, storytelling, educational, motivational, or tutorial. Default storytelling. |
language | string | Write in this language instead of the profile’s. Send the same one to POST /videos. |
Returns 202 with a script_id. Poll GET /scripts/:id until ready is true. The defaults match the create page’s Write button. Counts against your script quota. Capped at 30 per hour.
/scripts/:id Fetch a generated script.
The script comes back with any voice tags the writer added. A script that ran past your plan’s length is cut at a whole sentence, the way the create page cuts it, and rendering this script_id renders exactly that text.
{
"script_id": "5b7c...",
"status": "completed",
"ready": true,
"script": "Every morning, two billion people...",
"title": "The Hidden History of Coffee",
"topic": "The hidden history of coffee",
"language": "en",
"estimated_seconds": 482,
"has_voice_tags": false
} /scripts/validate Estimate length and credit cost.
Estimate a script's spoken length, whether it fits your plan, and what it would cost to render at each pacing. Free to call. It measures the words that are spoken: voice tags and cues are not counted. Send language or the brand_profile_id you will render with, because languages are spoken at different speeds.
You can also send any of the settings from POST /videos. You then get back estimated_credits_for_settings: the price of that exact video, animation included. Do this before you animate anything.
With brand_profile_id, it also checks the script’s tags against that profile’s voice. Tags written for a different voice come back in foreign_voice_tags; POST /videos refuses the script until they are fixed. Send fix_foreign_tags here to see and price the fixed script first.
{
"language": "en",
"estimated_seconds": 292,
"estimated_minutes": 4.9,
"plan_max_minutes": 15,
"fits_plan": true,
"estimated_credits": { "relaxed": 25, "dynamic": 34, "rapid": 44, "hyper": 59 },
"estimated_credits_for_settings": 96,
"delivery_marks": { "tags": 3, "cues": 0 }
} /scripts List your saved scripts.
brand_profile_id | uuid | Only scripts written for this profile. |
limit | integer | 1 to 50. Default 10. |
offset | integer | Skip this many. Take next_offset from the previous page. |
Your saved scripts, newest first. Any one with ready: true renders with script_id on POST /videos.
/scripts/:id Delete a saved script.
Deletes a saved script. Videos already made from it are unaffected, and it still counts toward this month’s script allowance. A script that is still being written cannot be deleted yet (409).
/scripts/voice-tags Have AI add voice tags or cues to a script.
script_text required | string | The script to mark up. |
brand_profile_id required | uuid | The profile the video will be made with. Its voice decides which marks. |
The create page’s “Add with AI”. An AI adds [tags] for an ElevenLabs voice or <cues> for a Google voice, and never changes a word. Free, within a daily number of runs. Show the result to the person, then send it as script_text to POST /videos.
/visual-directions Draft a visual direction.
script_text required | string | The script the video will be made from, at least 40 characters. |
brand_profile_id | uuid | The profile it will be made with. Its style and cast shape the draft. |
character_ids | uuid[] | The cast of this video, if not the profile’s. Needs brand_profile_id. |
redo | boolean | A fresh draft of words drafted before. Otherwise the same words return the same draft for a week. |
The create page’s “Ask AI for a draft”: the world the scenes are drawn in, written from the script. Free, needs a paid plan, and has an hourly limit. It takes 20 to 60 seconds, so poll GET /visual-directions/:id. Edit it with the person, then send it as visual_direction on POST /videos.
/visual-directions/:id Poll a visual direction draft.
{
"visual_direction_id": "0c1d...",
"status": "completed",
"ready": true,
"direction": "A rain-soaked harbour town in the 1920s...",
"error": null
} /videos Render a video.
brand_profile_id required | uuid | A render-ready profile from GET /brand-profiles. |
script_id | uuid | A completed script. Mutually exclusive with script_text. |
script_text | string | Your own script. Mutually exclusive with script_id. It may carry the delivery marks the profile’s voice performs (GET /brand-profiles/:id/voice-tags). They are performed, never spoken or captioned. |
title | string | Working title, up to 120 characters. Defaults to the brand profile name. We write the real title after the render either way. |
fix_foreign_tags | string | convert or remove. Tags the profile’s voice cannot use (written for a different voice) stop the render with 400 voice_tags_wrong_voice. convert swaps each for the same sound in this voice’s marks and removes the rest; remove removes them all. |
pacing | string | relaxed, dynamic, rapid or hyper. Sets how often the image changes and scales the credit cost. Default relaxed. |
pace_mode | string | uniform for one pace throughout, or hook for a faster opening. Default uniform. |
hook_pace | string | How fast the opening cuts. Must be faster than pacing. Only with pace_mode hook. Default hyper. |
hook_seconds | integer | How long the faster opening lasts, 5 to 90. Only with pace_mode hook. Default 30. |
animate | string | off, first, percent or all. Turns stills into moving clips. Needs a Starter plan or above and costs extra credits. Default off. |
animate_first_scenes | integer | How many opening scenes to animate, 1 to 12. Only with animate first. Default 3. |
animate_percent | integer | What share of scenes to animate, 5 to 100. They are spread evenly. Only with animate percent. Default 30. |
transition | string | random, pushRight, fade or none. How one scene becomes the next. Default random. |
motion | string | random, zoomIn, zoomOut, panLeft, panRight or none. The slow move each still gets. Default zoomIn. Animated scenes ignore it. |
motion_speed | string | slow, medium or fast. Default slow. |
language | string | Narrate and caption this one video in another language. Overrides the brand profile. |
format | string | long for 16:9, the default, or short for vertical 9:16. A shape, not a length: both follow the same plan length cap. |
character_ids | uuid[] | The recurring characters in this video, lead first, up to 4. Leave it out to use the profile’s cast. [] means none. Each must be your own approved character drawn in the profile’s style. |
visual_direction | string | The world the scenes are drawn in, up to 1500 characters. Write @name to place a cast member. Leave it out for the normal, more varied scenes. |
visual_direction_id | uuid | A finished draft from POST /visual-directions. Its text is used when visual_direction is not sent. |
edit_before_render | boolean | Stop at ready_to_edit instead of finishing, so you can change scenes and narration first. Default false. |
{
"video_id": "e8d4...",
"status": "processing",
"title": "Coffee history",
"pacing": "dynamic",
"language": "en",
"settings": {
"pacing": "dynamic",
"animate": "first",
"animate_first_scenes": 3,
"transition": "fade",
"motion": "zoomIn"
},
"edit_before_render": false,
"estimated_credit_cost": 95,
"estimated_build_seconds": 1348,
"estimated_build_minutes": 22.5,
"recommended_first_poll_seconds": 1348,
"recommended_poll_interval_seconds": 30
} This is the one paid action: it reserves the credit cost up front. Requires an active paid plan or bonus credits. Capped at 10 renders per hour. A script longer than your plan allows is refused with a 402; a generated script is first cut to fit at a whole sentence, as the create page does.
Everything except brand_profile_id and the script is optional. Send nothing else and you get the same video this endpoint always made. The settings field in the response is what the render actually started with, defaults filled in.
With edit_before_render set, the video stops at ready_to_edit and waits. Nothing finishes it until you export it, so only set it if you plan to edit.
/videos List recent videos.
status | string | draft, processing, ready_to_edit, completed or error. |
archived | string | exclude (the default, your library), only (the Archived tab), or include. |
format | string | long for 16:9 or short for 9:16. |
search | string | Words in the title. |
limit | integer | 1 to 50. Default 10. |
offset | integer | Skip this many. Take next_offset from the previous page. |
Most recent first.
/videos/:id Poll a video render.
{
"video_id": "e8d4...",
"status": "completed",
"ready": true,
"progress_percent": 100,
"next_poll_after_seconds": null,
"video_url": "https://storage.googleapis.com/.../final.mp4",
"youtube_url": null,
"thumbnail_url": "https://...",
"duration_seconds": 294,
"youtube_metadata": {
"title": "The Hidden History of Coffee",
"description": "...",
"tags": ["coffee", "history"]
},
"error": null
} /videos/:id/scenes List a video’s scenes.
Every scene of a finished video, in order: the prompt it was drawn from, a link to the image, the narration spoken over it, how long it lasts, and whether it has been animated.
Scenes are numbered from 0. That number is how you refer to a scene everywhere else, so you do not have to keep track of ids. A motion of null means the scene follows the video setting, which is not the same as "none".
{
"video_id": "e8d4...",
"status": "ready_to_edit",
"editable": true,
"scene_count": 34,
"scenes": [
{
"scene": 0,
"image_id": "9f2a...",
"prompt": "A hand pouring coffee...",
"narration": "Every morning, two billion people...",
"image_url": "https://storage.googleapis.com/.../image_0.png",
"seconds": 14.2,
"motion": null,
"transition_after": null,
"animated": false,
"ready": true
}
]
} /videos/:id/narration List a video’s narration lines.
The narration, line by line. Only videos with segmented narration have separate lines. An older video returns segmented: false and its narration cannot be changed.
Line numbers have gaps where lines were deleted. Read seq from this response rather than counting.
Each line says which voice tags its voice reads in tag_family: elevenlabs ([tags]), google (<cues>) or none. A line that carries tags also has spoken_text: the line as its voice is given it.
/videos/:id/open Open a finished video for editing.
Open a finished video so it can be changed. Nothing can be edited until you do.
The video moves to ready_to_edit and stays there. The version people can watch is untouched until you export, and nothing else will finish it. Safe to call twice. A video created with edit_before_render is already open.
/videos/:id/export Render your edits.
Render your edits and make them the version people see. This is the only thing that finishes a video waiting at ready_to_edit. Poll GET /videos/:id until it is completed.
A video can only be exported a limited number of times, and the response says how many are left. A 409 with voiceover_master_stale means a narration edit is still settling: wait a few seconds and call it again.
Every endpoint below needs the video open for editing, and names a scene the same way: send scene (its number from GET /videos/:id/scenes, counting from 0) or image_id. Nothing you change here is visible until you export.
/videos/:id/scenes/regenerate Draw a scene again.
scene | integer | The scene number. Or send image_id. |
image_id | uuid | The scene’s image id, instead of scene. |
mode | string | rewrite (the default) draws it again from a prompt. edit changes the current picture from a short instruction, where the video’s style allows it. |
prompt | string | rewrite: what the scene should show. Describe the subject and action, not the art style: the video’s style is added on top. Leave out to redraw the same prompt. edit: the change to make, like “make the sky stormy”. |
quote_only | boolean | Return what the next drawing costs and spend nothing. |
On a black screen this fills it with a new picture from prompt. Write @name in a prompt to put one of the video’s characters in.
Each video gets a number of free scene drawings, 10 by default, shared with POST /videos/:id/scenes/add, and does not get more. After those, a drawing costs credits and goes ahead rather than being refused; the response says which happened. Going back to an earlier picture is free, so check the versions first. The new picture takes a minute. Poll GET /videos/:id/scenes until that scene is ready.
/videos/:id/scenes/versions A scene’s earlier pictures.
Every picture drawn for one scene, oldest first, with the prompt each came from and which one is showing. Going back to one is free, so check here before spending another redraw.
/videos/:id/scenes/versions Go back to an earlier picture.
Show an earlier picture instead of the current one. Send the file_path from the versions list. Free, instant, and reversible.
/videos/:id/scenes/split Cut a scene into two.
Cut one scene into two showing the same picture, so a long stretch of narration can have two pictures. The video does not get longer: the scene’s time is divided between the halves. Send at_seconds to choose where, measured from the scene’s own start; it defaults to halfway.
Both halves show the same picture until you regenerate one, which gives it a picture of its own automatically. Scene numbers after the split shift up by one.
/videos/:id/scenes/remove Take a scene out.
Take a scene out. Its time goes to the scene before it, so the video stays exactly as long and the narration is untouched: the previous picture just stays on screen longer. Scene numbers after it shift down by one.
To hide a scene without giving its time away, black it out instead. To make the video shorter, delete the narration.
/videos/:id/scenes/blank Black out a scene.
Replace a scene’s picture with a black screen for exactly as long as that scene lasted. Nothing else moves and the narration keeps playing. Fill it again later with a new drawing or your own image.
/videos/:id/scenes One scene’s motion, transition, fades.
scene | integer | The scene number. Or send image_id. |
motion | string | inherit, zoomIn, zoomOut, panLeft, panRight, panUp, panDown or none. "inherit" follows the video; "none" holds still. Animated scenes ignore it. |
motion_speed | string | slow, medium or fast. Default slow. |
transition | string | How this scene becomes the next one. "inherit" follows the video. Meaningless on the last scene. |
fade_in_seconds | number | Fade from black at the start. 0 turns it off. Capped at 3s and at half the scene. |
fade_out_seconds | number | Fade to black at the end. |
Setting a transition clears any fade at that cut, because a fade would otherwise outrank it and the transition would never play.
/videos/:id/style The whole video’s cuts and motion.
transition | string | How every scene becomes the next: random, alternate, fade, a push or slide direction, or none. |
clear_fades | boolean | With a new transition: also remove the scene fades at cuts, which would otherwise play instead of it. The very first and last fades stay. |
motion | string | random, zoomIn, zoomOut, panLeft, panRight or none. The slow move every still scene gets. |
motion_speed | string | slow, medium or fast. Sent alone, it changes only the speed. |
Works like the editor’s pickers. A new transition applies to every cut and clears the per-cut ones you had pinned. A new motion applies to every still scene, clears the per-scene ones, and keeps the current speed unless you send one.
/videos/:id/scenes/animate Turn a still into a moving clip.
Turn one scene’s still picture into a short moving clip. This spends credits. They are charged when it starts and are not refunded if you change your mind, and the amount scales with how long the scene runs.
Send quote_only: true first to see the price without spending anything. Needs a Starter plan or above. It takes a minute or two; poll the scenes list until that scene shows animated: true.
/videos/:id/scenes/unanimate Back to the still picture.
Put an animated scene back to its still picture. The credits already spent are not refunded, so this undoes the look, not the cost.
/videos/:id/scenes/add Draw a new picture into the video.
prompt | string | What the new scene should show. Describe the subject, the action and the setting, not the art style: the video’s own style is added on top. Do not ask for text or lettering. Required unless quote_only is true. |
after_scene | integer | The scene number to put it after. Defaults to the end of the video. That scene is halved to make room, so it has to run at least 2 seconds. |
quote_only | boolean | Return what the drawing would cost, and spend nothing. |
The scene before it is cut in half, so the video does not get longer and the narration is untouched. Use it when a stretch of narration needs a second picture, not to make the video say more. Scene numbers after the new one shift up by one.
Each video gets 10 free scene drawings, shared with POST /videos/:id/scenes/regenerate. After those, every drawing costs credits. The response says which happened, in credits_charged and free_drawings_remaining. The picture takes a minute; poll GET /videos/:id/scenes until it is ready.
/videos/:id/scenes/upload Put your own image in a scene.
scene | integer | The scene number. Or send image_id. |
image_id | uuid | The scene’s image id, instead of scene. |
image_url | string | A public https URL we can download the image from. Send this or image_base64, not both. |
image_base64 | string | The image bytes as base64. A "data:" prefix is accepted and stripped. Send this or image_url, not both. |
name | string | Label for the upload in your media list. Up to 80 characters. |
Put your own photo or artwork into a scene, or into a black screen, for the times no prompt would produce it. JPG, PNG or WebP, at least 320 by 180, up to 10 MB. Free, and the picture it replaces is kept, so POST /videos/:id/scenes/versions can put it back.
Capped at 30 uploads an hour across your account, and 200 per video.
/videos/:id/scenes/cut Move the cut between two scenes.
scene | integer | The scene number. Or send image_id. |
image_id | uuid | The scene’s image id, instead of scene. |
delta_seconds required | number | Seconds to move the cut by, up to 120. Positive holds this scene longer and shortens the next one. Negative does the reverse. |
The time moves between the two scenes, so the video’s length and its narration never change. Use it when a picture arrives too early or lingers.
If the amount you asked for would push a neighbour under its minimum length, it is trimmed to fit. The response then has clamped: true and moved_seconds is what actually happened.
/videos/:id/scenes/reorder Move a scene somewhere else.
scene | integer | The scene number to move. Or send image_id. |
image_id | uuid | The scene’s image id, instead of scene. |
to_scene required | integer | The position to move it to, counting from 0. |
Works like dragging a scene in the editor. The cuts stay where they are on the narration and black screens stay put, so the picture you moved fills its new slot’s length and plays over different words. Re-read GET /videos/:id/scenes afterwards and check each picture still matches what is being said.
These four are the only endpoints that change how long a video is. Everything under Editing scenes moves time between scenes and leaves the total alone. Like scene editing, they need the video open first: see POST /videos/:id/open. Only videos with segmented narration have separate lines: check GET /videos/:id/narration first, which also reports the edit budget. Rewriting or adding a line uses it: the first 3 of a video are free and each one after that costs a credit. Deleting or muting a line is free and does not use it. Name a line by its seq or its segment_id. Numbers keep their gaps after a deletion, so read them rather than counting. Nothing here reaches the version people watch until you export.
/videos/:id/narration/rewrite Say a line again, or differently.
line | integer | The line’s seq from GET /videos/:id/narration. Or send segment_id. |
segment_id | uuid | The line’s id, instead of line. |
text | string | New words for this line, under 400 characters. It may carry the tags this line’s voice reads ([tags] for an ElevenLabs line, <cues> for a Google one); the captions keep the plain words. Pauses (<break/>) cannot go in a line. Leave it out to say the same words again with a fresh take. |
This is the only way to change what the video says. If the new take is a different length, the video’s length changes with it and the pictures shift to match, so keep new words close to the original length. It takes a few seconds. Poll GET /videos/:id/narration until the line is ready, then re-read the scenes before judging the timing.
/videos/:id/narration/insert Add a spoken line.
text required | string | What the new line says. Under 400 characters. Plain words only: add voice tags afterwards with POST /videos/:id/narration/rewrite. |
after_line | integer | The seq of the line to put this one after. 0 puts it before the first line, which is the default. |
The video gets longer by however long the line takes to say. No scene is added: the picture covering that moment simply stretches over the new words, so follow with POST /videos/:id/scenes/add if they deserve their own picture. Line numbers after it shift up by one.
/videos/:id/narration/delete Cut a spoken line out.
line | integer | The line’s seq from GET /videos/:id/narration. Or send segment_id. |
segment_id | uuid | The line’s id, instead of line. |
The video gets shorter by exactly that line and the pictures close up over the gap. This is how you cut something the video should not say. Removing a scene does not shorten anything.
It is free and does not use up a narration edit. The line numbers left behind keep their gaps.
/videos/:id/narration/mute Silence a line, keep its pause.
line | integer | The line’s seq from GET /videos/:id/narration. Or send segment_id. |
segment_id | uuid | The line’s id, instead of line. |
muted | boolean | true silences the line, which is the default. false makes it speak again. |
The words stop, the pause stays, and nothing else moves. Use it to drop a sentence while leaving every picture exactly where it is. To take the time back as well, delete the line instead.
What a finished video is published as: its title, description, tags and thumbnail. These do not need the video open for editing. Each field can be regenerated 3 times per video and that count never resets, so read GET /videos/:id/versions first. Putting an earlier one back is free and spends no regeneration.
/videos/:id/metadata Title, description, tags, thumbnail.
GET /videos/:id says whether a video is done. This says whether it is any good: the title, description, tags and chapters it will publish as, the thumbnail and the prompt behind it, where it is on YouTube, and how many regenerations of each are left.
{
"video_id": "e8d4...",
"status": "completed",
"title": "The Hidden History of Coffee",
"description": "...",
"tags": ["coffee", "history"],
"thumbnail_url": "https://...",
"thumbnail_prompt": "THE PRESENTER holds up a steaming coffee cup...",
"thumbnail_text": "NOBODY TOLD YOU",
"chapters": [{ "title": "Intro", "start_seconds": 0 }],
"youtube_url": "https://www.youtube.com/watch?v=...",
"archived": false,
"marked_published": true,
"regenerations_left": { "title": 2, "description": 3, "tags": 3, "thumbnail": 1 }
} /videos/:id/metadata Write the title, description or tags again.
field required | string | title, description or tags. For the thumbnail use POST /videos/:id/thumbnail. |
Free, but capped at 3 regenerations of that field per video, for ever. It takes a few seconds. Poll GET /videos/:id/metadata until the field changes. The old value is kept, so a regeneration is always reversible.
/videos/:id/thumbnail Draw the thumbnail again.
prompt | string | The picture, in one sentence: who is in it and the one object that carries the idea. Write THE PRESENTER for the recurring character of the video. No art style and no lettering here: use text for that. Keeps the current prompt if omitted. |
text | string | The label hand-lettered across the thumbnail. Two to seven words works best. An empty string goes back to letting us choose. Omit to keep what is set. |
The thumbnail is drawn in the video’s own style, from its first scenes, with its recurring character as the constant. Anything you leave out keeps what the video already has. Free, but capped at 3 thumbnail regenerations per video, for ever. Vertical 9:16 videos keep their first scene and are refused. It takes about a minute. Poll GET /videos/:id/metadata until thumbnail_url changes.
/videos/:id/versions Earlier titles, tags and thumbnails.
field | string | title, description, tags or thumbnail. All four if omitted. |
Every title, description, tag set and thumbnail this video has had, oldest first, with is_current marking the one showing now. Always check here before spending another regeneration.
/videos/:id/versions Put an earlier one back.
version_id required | uuid | A version_id from GET /videos/:id/versions. |
Free, instant, and it spends no regeneration. The value it replaces is kept too, so this is reversible in both directions.
/videos/:id Rename a video.
name | string | Up to 120 characters. An empty string clears it. |
Your own label for the video, the name the library and GET /videos list it under. It is never sent to YouTube: the YouTube title, description and uploads stay as they are. A title sent to POST /videos is kept as this name.
/videos/:id/archive Hide a video, or bring it back.
archived | boolean | true hides it, which is the default. false brings it back into the library. |
Reversible, and it destroys nothing: the video, its file and its history all survive. This is the one to reach for whenever a video just needs to be out of the way.
/videos/:id/published Mark a video as published.
published | boolean | true marks it published, which is the default. false unmarks it. |
For a video you posted somewhere yourself: it only marks the video in your library. It posts nothing anywhere. To put a video on YouTube, use POST /youtube/posts.
/videos/:id/captions One video’s captions.
enabled | boolean | Captions on or off for this video. |
font | string | One of the installed caption fonts, for example Inter, Montserrat, Bebas Neue, Anton or Poppins. |
size | number | 25 to 120 for a 16:9 video. 44 to 213 for a 9:16 video, which is sized on a 1920-tall frame. |
color | string | A hex code like "#ffffff", or white, lime, yellow, orange, red or blue. |
highlight_color | string | The colour of the word being spoken. Same formats. |
shadow | string | none, soft, medium, hard or glow. |
words_per_line | number | 1 to 10. |
uppercase | boolean | Captions in capitals. |
position | string | Where the captions sit: top, middle or bottom, then left, center or right. bottom-center is the normal spot. Kept on screen in this video’s frame. |
x_pct | number | An exact spot instead: the caption box’s centre, 0 (left) to 100 (right). |
y_pct | number | The caption box’s bottom edge, 0 (top) to 100 (bottom). |
reset | boolean | Copy the brand profile’s current captions, placement included, onto this video. Send it alone. |
The captions of this one video, like the editor’s Captions panel. The brand profile is not changed. The video must be open for editing, and the new captions show once you export it. The response’s captions.position is where they sit ({ x_pct, y_pct, max_width_pct }), or null for the normal spot at the bottom centre. The box width is worked out for you, so the text stays on screen.
/videos/:id Delete a video for good.
Permanent, and it takes the scenes, the narration and the edit history with it. This cannot be undone, and the credits the video cost are not returned. Use POST /videos/:id/archive unless the video is genuinely meant to be destroyed.
A video that is still building cannot be deleted. Wait for it to finish or fail first: the credits it reserved are spent either way.
A recurring character looks the same in every video of the profile it is attached to. Drawing one is a draft: nothing reaches a video until you approve it. Approving makes it the lead of the profile it was drawn on, and POST /brand-profiles/:id/characters sets a profile’s whole cast. List what you already have with GET /characters.
/characters Draw a recurring character.
brand_profile_id required | uuid | The profile this character belongs to. |
name required | string | The character’s name, 1 to 60 characters. |
description required | string | What they look like, 10 to 400 characters. Describe the person or creature only: species, build, face, hair, clothing, colours. Not the art style, a pose, a background, or anything they are holding. A prop written in here follows them into every scene. |
visual_style_id | uuid | Draw them in this style instead of the profile’s own. |
Each drawing comes out of a monthly free allowance, then costs a credit, and the response shows both, so settle the description first. Calling it again adds another take of the same character rather than making a second one, so this is both "make one" and "try again". The response says how many redraws are left. Needs an active paid plan.
The description it returns is not the one you sent: it is what we read back off the picture, and it is the text that travels into every scene. Judge that as much as the image. Nothing renders in a video until you approve a take.
/characters/describe Reword a character, free.
change required | string | The change to make, in plain words. For example "older" or "lose the hat". |
description | string | The description to change. Send this or character_id. |
character_id | uuid | Take the current description from this character instead of passing it. |
Free and instant. Nothing is drawn and nothing is charged. Use it to settle the wording before spending a draw, because the description is what travels into every scene prompt. Pass the result to POST /characters, or to POST /characters/:id/approve to save it against a picture already drawn.
/characters/:id/approve Save a character.
take | integer | Which take to keep, from the takes list POST /characters returned. Defaults to the most recent. |
description | string | Save this description instead of the drawn one. The picture is unchanged. |
Saving fixes the picture and the description for good. Only the name can be changed afterwards, because a saved character has to stay the one the person judged. Be sure here, not after.
Approving also makes it the lead character of the profile it was drawn on, so every new video from that profile shows it. To use it in another profile, add it to that profile’s cast with POST /brand-profiles/:id/characters. A character only renders in the visual style it was drawn in.
/characters/:id Throw a character away.
Throws a character away, saved or not. Any brand profile using it stops using it, so its next videos are drawn without a recurring character. Videos already made keep theirs.
/characters/:id Rename a character.
name required | string | The new name, 1 to 60 characters. |
Works on a saved character too: the name is the one thing that can still change. Its @mention stays the same, so directions and prompts already written still find it.
Put finished videos on your YouTube channel and see how they do. Connecting a channel is Google’s sign-in, so it happens in the dashboard, not over the API.
/youtube/channels Your connected YouTube channels.
The channels connected to your account. can_upload says whether a channel can take an upload, and uploads_per_channel_per_day is the daily limit per channel.
/youtube/posts Publish a video to YouTube.
video_id required | uuid | A finished video. |
privacy required | string | public, unlisted or private. Always ask the person. |
channel_id | uuid | From GET /youtube/channels. Optional when only one channel is connected. |
publish_at required | string | "now", or an ISO 8601 time that names its offset: Z for UTC, or e.g. -04:00. A time without one is refused. Up to a year ahead. Never assumed: say "now" to post at once. |
title | string | Up to 100 characters. Default: the video’s title. |
description | string | Up to 5000 characters. Default: the video’s description with its chapters. |
tags | string[] | Default: the video’s tags. |
category_id | string | A YouTube category id. Default 22 (People & Blogs). |
thumbnail_version_id | uuid | Use an earlier thumbnail from GET /videos/:id/versions. It becomes the video’s thumbnail. |
timezone | string | Your IANA timezone, for display. Default UTC. |
allow_duplicate | boolean | Post again although this video already has a scheduled or published post on this channel. Refused (409) otherwise. |
This posts to your channel, so confirm the channel, time, title and privacy first. A later time uploads it when that time comes, at the privacy you chose. Each channel can take a set number of uploads a day. Poll GET /youtube/posts for the YouTube link.
/youtube/posts Your YouTube posts.
video_id | uuid | Only this video’s posts. |
status | string | scheduled, publishing, published, failed or cancelled. |
limit | integer | 1 to 50. Default 20. |
offset | integer | Skip this many. Take next_offset from the previous page. |
Newest time first. A published post has its youtube_url, and a failed one says why.
/youtube/posts/:id Change, cancel or retry a post.
action | string | edit (the default), cancel, retry (a failed post only) or publish_now. |
publish_at | string | A new ISO 8601 time that names its offset (Z, or e.g. -04:00). |
title | string | Up to 100 characters. |
description | string | Up to 5000 characters. |
tags | string[] | Replaces the tags. |
privacy | string | public, unlisted or private. |
category_id | string | A YouTube category id. |
allow_duplicate | boolean | Retry even though the video has gone out on this channel since. Refused (409, already_posted) otherwise. |
Only a post that has not gone out: edit a scheduled, failed or cancelled one, cancel a scheduled one, retry a failed one (at once, unless you send a new publish_at), or publish a scheduled one now. A cancelled post is scheduled again with POST /youtube/posts. A published video is changed in YouTube Studio instead.
/youtube/analytics Channel and video analytics.
channel_id | uuid | One channel. All your channels if omitted. |
video_id | uuid | One video’s figures instead of the channel’s: a Channel Farm video id, or a catalog_id from top_videos for a video not made here. |
days | integer | 1 to 365. Default 28. |
Views, watch time, subscribers and click-through over the last days, against the period before, plus your top videos. From YouTube’s own reporting, which runs a day or two behind.