Videos and generation jobs
Render a video from a script you already have, or have the model write one first.
The lifecycle of a video
POST /videos accepts the work and returns straight away — the video is not rendered yet. Track it through status:
| status | Means |
|---|---|
DRAFT | Saved, no render yet |
PENDING | Accepted, waiting to enter the queue |
PENDING_DISPATCH | Waiting for a free slot in the account render queue |
RENDERING | Rendering |
GENERATING | AI generation in progress; cannot be touched |
READY | Rendered, waiting for you to publish |
QUEUED | Queued for publishing, or scheduled |
POSTED | Published — per-channel results live in targets |
FAILED | Failed; read errorCode and errorDetail |
Values are upper case and compared exactly. The targets array has its own per-channel set: QUEUED, PROCESSING, POSTED, PUBLICLY_AVAILABLE, UNAVAILABLE, FAILED, DRAFT.
Polling every 10–15 seconds is plenty. Bursts do not speed up rendering.
Creating a video
POST /videos → 201
Requires the Idempotency-Key header — see Shared conventions.
{
"title": "Morning bulletin 22/09",
"script": "Voice-over copy…",
"caption": "Today's headlines",
"hashtags": ["news", "morning"],
"connectionIds": ["4e1c…"],
"voiceId": "ngochuyennew",
"mediaUrls": ["https://…/image-1.jpg"],
"keepAsReady": true
}| Field | Meaning |
|---|---|
connectionIds | Target channels. Take id from GET /channels. |
voiceId | Voice-over. Browse voices in the web app. |
mediaUrls | Images used to build the frames. Upload via /media and use the returned url. |
keepAsReady | true = render and stop, waiting for you to publish. false = publish once rendered. |
scheduleAt | ISO 8601 publish time. Empty means publish as soon as the render finishes. |
templateId | Brand template from GET /templates; empty uses the default. |
bgmSource, bgmKey, bgmVolume | Background music and its level. |
youtubeConfig, facebookConfig | Platform-specific options such as visibility. |
The 201 response returns the new video plus a warnings array. Warnings do not block creation — currently there is exactly one: the channel is past the recommended posts-per-24-hours mark.
This call spends credits
Credits are held when the work is queued and settled when the render finishes. Check GET /credits first if you want certainty. See How credits are calculated.
Reading and polling
GET /videos?page=1&pageSize=25&status=RENDERING
GET /videos/{id}targets gives the per-channel outcome: channelId, platform, status, postUrl once live, and errorCode on failure. One video can succeed on one channel and fail on another.
videoUrl is the download link, non-null only after rendering. It expires under your plan's retention policy — download it if you need to keep it.
Render, publish, cancel
Render
POST /videos/{id}/render → 202Only for videos that have no render yet. Costs credits like a fresh render, so it needs an idempotency header.
Publish
POST /videos/{id}/publish → 202For videos created with keepAsReady: true. The body may override connectionIds, caption, hashtags, scheduleAt and comment (the first comment posted alongside). Omit connectionIds to keep the channels chosen at creation; omit scheduleAt to publish now.
This path needs posts:write, not videos:write: it is the step that goes public, kept separate so you can issue a render-only key.
Cancel publishing
DELETE /videos/{id}Removes pending publishes and returns the video to a ready state — the render is kept, so you can publish again later. A channel that is mid-publish blocks this, and a post already live cannot be taken down this way.
No idempotency header needed: cancelling twice is the same as cancelling once.
Edit title, caption, hashtags
PATCH /videos/{id}
{ "caption": "New caption", "hashtags": ["tips"] }Accepts only title, caption and hashtags; omitted fields stay as they are. No re-render, no change to status or schedule. Channels not yet published use the new text; a post already live is not changed. Requires videos:write, no idempotency header.
Reschedule
There is no separate reschedule endpoint: call DELETE /videos/{id} to remove the pending publish (the render is kept), then POST /videos/{id}/publish with the new scheduleAt. Leave connectionIds empty to keep the same channels.
Letting the model write
When you have no script yet, /jobs is the step before: queue work for the model, collect the result, then call POST /videos.
POST /jobs → 202
Idempotency-Key: …
{
"kind": "video-talk-script",
"params": { … }
}| kind | Produces |
|---|---|
video-talk-script | A script for a voice-over video |
storyboard | A storyboard for an explainer video |
storyboard-fit-extras | Step 2 of that flow: fitting leftovers into scenes |
comic-from-story | Comic panels from a story |
campaign-topics | Topic ideas for a campaign from a channel brief (briefChannel, count 1–200, language) |
Only the kinds listed on this page are accepted; any other kind is rejected with 422.
Each kind validates params against exactly the rules the web app uses, so a mistyped field is rejected at call time instead of failing silently in the background.
GET /jobs/{jobId}state:waiting,active,completed,failed,delayedorunknown.resultis present only whenstate = completed.errorCodeonly whenstate = failed. It is a code to branch on, not a sentence to display.
The enqueue call returns 202, not 200
The body carries only jobId and kind. No state is included on purpose: by the time we answer the job may already be running, finished, or still waiting — GET /jobs/{id} is the only honest source.
Knowledge videos
Send the source text and a knowledge video template; the server rewrites it scene by scene to fit the template, picks layouts, finds images and renders. Templates are created in the web app — take the id from GET /video-templates (only templates with kind = storyboard).
POST /knowledge/render → 201
Idempotency-Key: …
{
"templateId": "…",
"script": "Source text, 100–30000 characters…",
"title": "Why the sea is salty",
"voiceId": "ngochuyennew",
"language": "en"
}- Required:
templateIdandscript. Optional:title,caption,hashtags,language,voiceId,voiceVolume,bgmSource/bgmKey/bgmVolume,advanced,watermark. - A template that is not a knowledge template or has no scene plan returns 422
knowledge_template_required; a template you do not own returns 404 — both are rejected before any credit is held. - This endpoint only renders. Track it with
GET /videos/{id}(GENERATING→RENDERING→READY), then publish withPOST /videos/{id}/publish. When the account already has several knowledge videos rendering, a new one sits inPENDING_DISPATCH— waiting for its turn, not an error — and moves toGENERATINGon its own.
Not the same as the storyboard job
The storyboard job above only previews a scene script; its result cannot be fed in here. This endpoint rewrites the content to the template's own scene plan.
AI presenter and video clone
These two genres are built scene by scene, each scene fixed at 10 seconds and priced per second. The model step takes about a minute, so rendering also goes through /jobs instead of POST /videos. When the render job completes, result.postIds[0] is the video in your library — keep tracking it with GET /videos/{id}. Image keys come from POST /media/upload-ticket.
| kind | What it does | Spends credit |
|---|---|---|
ai-presenter-script | Writes per-scene dialogue from a topic, returns dialogues | No |
ai-presenter-render | Renders a presenter video from a portrait; scenes without dialogue get one written | Yes |
video-clone-analyze | Reads 2–5 screenshots of a sample video, returns analysisId (kept 24 hours) and a summary | No |
video-clone-render | Renders a new video in the analysed style from 1–5 of your input images | Yes |
AI presenter
POST /jobs
Idempotency-Key: …
{
"kind": "ai-presenter-render",
"params": {
"presenterImageKey": "uploads/…/presenter.jpg",
"setting": "production_studio",
"hologram": true,
"tone": "blue",
"cameraMove": "dolly_in",
"aspectRatio": "16:9",
"transition": "auto",
"style": "explainer",
"language": "en",
"topic": "Three AI trends this year",
"scenes": [
{ "shot": "medium" },
{ "shot": "close_up", "dialogue": "Thanks for watching." }
],
"connectionIds": [],
"allowComment": true,
"allowContentReuse": true,
"keepAsReady": true
}
}Video clone
Call video-clone-analyze first with { "frameKeys": [...], "summaryLanguage": "en" }, review the summary, then render:
{
"kind": "video-clone-render",
"params": {
"analysisId": "…",
"inputImageKeys": ["uploads/…/product.jpg"],
"sceneCount": 3,
"aspect": "source",
"language": "en",
"changeNote": "Swap the product for a lipstick",
"connectionIds": [],
"allowComment": true,
"allowContentReuse": true,
"keepAsReady": true
}
}Input images are not the sample screenshots
frameKeys are screenshots of the sample video, used only to read its style. inputImageKeys are your own images (product, character…) used to build the new video. An analysis expires after 24 hours; analyse again after that.
