Skip to main content

Generate a video end to end

The full pipeline, one step at a time: content → voiceover → render.

If you want it collapsed into a single request, see One-shot generation. The reason to do it stepwise is that you get to read and correct the script before spending credits on a render, which the one-shot path does not offer.

Scopes: read + generate.

1. Create the content item

curl -s -X POST https://api.botlobby.ai/v1/contents \
-H "Authorization: Bearer $BOTLOBBY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"full_text": "The James Webb telescope found water vapour on K2-18b",
"persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Water on a distant world",
"related_description": true
}'

full_text is the topic or source material — the script is generated from it, so it does not need to be the script itself. persona_id comes from GET /personas and decides the voice and models.

related_description controls whether the generated description relates to the video or is generic filler.

The id comes back nested under item — read item.id, not id. It is the creation_id and content_id everywhere below.

2. Read the script, and fix it if needed

This is the step the one-shot path skips.

curl -s https://api.botlobby.ai/v1/contents/CONTENT_ID \
-H "Authorization: Bearer $BOTLOBBY_API_KEY"

textover is the script. To change it you need the write scope:

curl -s -X PUT https://api.botlobby.ai/v1/contents/CONTENT_ID \
-H "Authorization: Bearer $BOTLOBBY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"textover": "In 2026, astronomers confirmed..."}'
Editing resets progress

A successful edit sets status back to NEW, discarding any audio or video already generated. Edit before generating, not after.

If a job is already running you get 409 with "code": "generation_in_progress". Resend with "cancel_generation": true to cancel it and apply the edit.

3. Generate the voiceover

curl -s -X POST https://api.botlobby.ai/v1/gen/tts \
-H "Authorization: Bearer $BOTLOBBY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"creation_id": "CONTENT_ID",
"audio_prompt": "Calm and documentary, slightly slower than normal"
}'

audio_prompt is a direction for the narrator, not the script — the script comes from the content item. It is capped at 2000 bytes of UTF-8 — bytes, not characters, so non-ASCII text reaches the limit sooner than its length suggests. Over that returns 400 rather than reserving credits and failing inside the worker.

Then wait for AUDIO_PROCESSED:

curl -s https://api.botlobby.ai/v1/gen/task-status/CONTENT_ID \
-H "Authorization: Bearer $BOTLOBBY_API_KEY"

See Polling and job status for a proper loop.

4. Choose a visual source

video_id decides what the viewer sees. It takes either a specific background or one of four mode sentinels.

A specific background

curl -s https://api.botlobby.ai/v1/gen/static-videos \
-H "Authorization: Bearer $BOTLOBBY_API_KEY"
[
{
"name": "1",
"cdn_url": "https://cdn.botlobby.ai/static-videos/1.mp4",
"thumbnail_cdn_url": "https://cdn.botlobby.ai/static-videos/1.jpg"
}
]
There is no id field

name is the video_id. "1" resolves to static-videos/1.mp4. Looking for an id and finding none is the usual first stumble here.

Or a sentinel

video_idWhat you get
stockStock footage and photography matched to the script
web_searchImages found on the web for the topic
generateAI illustrations drawn per beat of the script
customYour own uploaded background
caution

A sentinel selects a mode, not a file. Do not send video_file_name alongside one.

Visual modes vs. video_id

Your plan controls which visual modes you may use, and GET /subscription/current reports them under limits.content_limits.allowed_visual_modes. That list uses five names — the four sentinels above, plus static.

static is the odd one out: it is not a value you send. It is the mode that any non-sentinel video_id resolves to. Passing the library id "1" is using static mode.

Mode in allowed_visual_modesWhat to send as video_id
staticA name from GET /gen/static-videos, e.g. "1"
custom"custom"
stock"stock"
web_search"web_search"
generate"generate"
Don't send video_id: "static"

It isn't a sentinel, so it's treated as a library id — which means it passes the plan check and then fails at render, because no clip is named static. Send an actual id from GET /gen/static-videos.

Static and custom backgrounds are free to source; stock and web search carry a per-build credit surcharge that scales with video length.

5. Render

curl -s -X POST https://api.botlobby.ai/v1/gen/video \
-H "Authorization: Bearer $BOTLOBBY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"creation_id": "CONTENT_ID",
"video_id": "stock",
"orientation": "vertical",
"prefer_real_world": true,
"zoom_effect_enabled": true
}'

Options worth knowing:

FieldNotes
orientationvertical (9:16, default) or landscape (16:9)
seconds_per_imageHow long each image holds. Only changes cost under generate
prefer_real_worldUnder stock, searches Wikimedia Commons before Pexels — better for named people and current events
illustration_styleOnly meaningful with video_id: generate
custom_style_descriptionOnly when illustration_style is custom. Truncated at 200 characters — a style hint, not a second prompt
zoom_effect_enabledKen Burns zoom. Image-based sources only; static and custom backgrounds are real video
image_overlay_enabledSmall image over part of the frame. Static, custom and AI-generated backgrounds only

This endpoint is subject to your plan's daily video cap as well as the ordinary rate limit. Both appear as 429 with Retry-After.

Poll until VIDEO_PROCESSED.

6. Collect the result

curl -s https://api.botlobby.ai/v1/contents/CONTENT_ID \
-H "Authorization: Bearer $BOTLOBBY_API_KEY"

video_url and thumbnail_url are signed and time-limited. Fetch them fresh when you need them rather than storing the URLs; store the content_id instead.

Next