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:

statusMeans
DRAFTSaved, no render yet
PENDINGAccepted, waiting to enter the queue
PENDING_DISPATCHWaiting for a free slot in the account render queue
RENDERINGRendering
GENERATINGAI generation in progress; cannot be touched
READYRendered, waiting for you to publish
QUEUEDQueued for publishing, or scheduled
POSTEDPublished — per-channel results live in targets
FAILEDFailed; 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
}
FieldMeaning
connectionIdsTarget channels. Take id from GET /channels.
voiceIdVoice-over. Browse voices in the web app.
mediaUrlsImages used to build the frames. Upload via /media and use the returned url.
keepAsReadytrue = render and stop, waiting for you to publish. false = publish once rendered.
scheduleAtISO 8601 publish time. Empty means publish as soon as the render finishes.
templateIdBrand template from GET /templates; empty uses the default.
bgmSource, bgmKey, bgmVolumeBackground music and its level.
youtubeConfig, facebookConfigPlatform-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 → 202

Only for videos that have no render yet. Costs credits like a fresh render, so it needs an idempotency header.

Publish

POST /videos/{id}/publish → 202

For 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": { … }
}
kindProduces
video-talk-scriptA script for a voice-over video
storyboardA storyboard for an explainer video
storyboard-fit-extrasStep 2 of that flow: fitting leftovers into scenes
comic-from-storyComic panels from a story
campaign-topicsTopic 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, delayed or unknown.
  • result is present only when state = completed.
  • errorCode only when state = 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: templateId and script. 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 with POST /videos/{id}/publish. When the account already has several knowledge videos rendering, a new one sits in PENDING_DISPATCH — waiting for its turn, not an error — and moves to GENERATING on 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.

kindWhat it doesSpends credit
ai-presenter-scriptWrites per-scene dialogue from a topic, returns dialoguesNo
ai-presenter-renderRenders a presenter video from a portrait; scenes without dialogue get one writtenYes
video-clone-analyzeReads 2–5 screenshots of a sample video, returns analysisId (kept 24 hours) and a summaryNo
video-clone-renderRenders a new video in the analysed style from 1–5 of your input imagesYes

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.