Overview
Creating a video returns avideo_id. Two endpoints read everything back from that id:
The first is the endpoint you already poll after creating a video — it now returns far more than the status-and-URL pair it started with. The scenes endpoint is new.
Video Details
Response
status—pending→processing→completedorfailedcreated_at,completed_at— Unix timestamps; the difference is your render timefailure_code,failure_message— on a failed video: branch on the code, log the message
Placement
title,folder_id— where the video lives in your workspacevideo_page_url— deep link to the video in the HeyGen appduration— final length in secondsoutput_language— BCP-47 code; present only on translated videos, which is also how you tell them apart
Media URLs are presigned and expire (the
Expires parameter says when). Download what you need rather than storing the URLs — re-fetch the details for a fresh link at any time.video_url yet, and only a failed video carries failure fields.
Video Scenes
GET /v3/videos/{video_id}/scenes opens the composition itself — for any video in your workspace, whichever surface created it: a studio request, a template render, an avatar video, or a video composed by hand in the editor. It describes the video as it stands now, including editor changes made after creation. When a brand glossary shapes the video’s speech, video.brand_glossary_id carries its id.
A video whose composition can’t be read answers with an error rather than an empty document: 404 video_not_found when there is no editor document to read (some translated videos, for example), and 400 when the video predates the current editor format.
Response
Roles, not kinds
A scene’s three content fields split by the role a thing plays:background fills the frame, elements are placed within it, script is the audio over it.
A video background or video element reads back with the same playback controls a studio scene accepts: playback (freeze / loop / fit_to_scene), trim, volume, speed_multiplier.
Reading elements
- Every element appears — the count is always truthful.
avatar,image, andvideoare described in full; anything else is a placeholder with just anidandtype(likemotion_graphicsabove). - Skip unrecognized types rather than erroring — the set is open and grows with the editor.
- Recurse into containers — a
groupormaskcarries its children underchildren, each described like a top-level element. - Avatar elements report what a create request can set —
avatar_id,engine,expressiveness,motion_prompt— so what you read back is what you could send back. New fields are added additively.
Reading scripts
Walking
scenes[].script[].text yields the full narration in playback order — for search indexing, compliance review, or driving a translation.
Scope
- Asset URLs (
url,audio_url) are returned when a live link exists at the time of the response; when the stored link has lapsed, the field is absent — the element itself is still reported. As with the deliverables above, download media you need rather than storing the URLs. - The response is scoped to composition content. Visual styling — element geometry, text-element contents, animations, per-scene caption styling, background music — lives outside it, so a rebuild from this response reproduces the content and narration, not the pixel-exact layout.
Putting the two together
1
Wait for the render
Poll
GET /v3/videos/{video_id} until status is completed — or skip polling with a webhook.2
Download the deliverables
Grab
video_url, subtitle_url, and friends before their presigned links expire.3
Index the composition
Call
GET /v3/videos/{video_id}/scenes — scripts for search, avatar and voice ids for auditing, scene structure for your own UI.404 with code video_not_found, from either endpoint.
