# AI clipping Source: https://heygen-1fa696a7.mintlify.app/ai-clipping Turn a long-form video into short, ready-to-share highlight clips with the HeyGen AI Clipping API — the model picks the best moments, cuts them to your target durations, and scores each clip's virality. ## Create a Clip Job * Endpoint: [`POST /v3/ai-clipping`](/reference/create-ai-clipping) * Purpose: Start a clipping job for a source video. Returns an `ai_clipping_id` to poll. ### Quick Example ```bash theme={null} curl -X POST "https://api.heygen.com/v3/ai-clipping" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "video": { "type": "url", "url": "https://example.com/interview.mp4" }, "title": "Founder interview", "output_settings": { "duration_types": ["30", "60"], "aspect_ratio": "portrait", "captions": false, "prompt": "Pull the moments where the founder talks about pricing and growth." } }' ``` ```json Response theme={null} { "data": { "ai_clipping_id": "edf8d2c44ba441b89f395072b3ef7e34" } } ``` ### Request Body | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `video` | object | Yes | — | Source video. Provide as `{ "type": "url", "url": "https://..." }` or `{ "type": "asset_id", "asset_id": "..." }` (from [`POST /v3/assets`](/reference/upload-asset) — see [Upload Assets](/docs/upload-assets)). | | `title` | string | No | — | Title for the job. Defaults to the source video's title when omitted. | | `input_language` | string | No | auto-detect | ISO-639-1 source language code (e.g. `en`, `es`). Omit to auto-detect. | | `output_settings` | object | No | — | Configuration for the produced clips — see [Output settings](#output-settings). | | `callback_url` | string | No | — | [Webhook](/docs/webhooks) URL — receives a POST when the job completes or fails. | | `callback_id` | string | No | — | Arbitrary ID echoed back in the webhook payload. | ### Output settings | Field | Type | Required | Default | Description | | - | - | - | - | - | | `duration_types` | array of strings | No | — | Target clip durations to produce: `"30"`, `"60"`, `"180"`, or `"long"` (1–4 entries). Each duration produces a separate clip. | | `aspect_ratio` | string | No | `portrait` | Framing for all produced clips: `portrait` (9:16, social-ready), `landscape` (16:9), or `square` (1:1). | | `captions` | boolean | No | `true` | Captions are burned into the clips by default. Set `false` for clean, caption-free footage — as in the example above. | | `caption_style` | string | No | — | Named caption style preset (e.g. `classic`, `bold`) when captions are on. Omit for the default style. | | `prompt` | string | No | — | Editorial guidance for the highlight model — which speaker, what topics (max 500 chars). When omitted, the model selects highlights on its own. | Want captions on some clips and clean footage on others? Captions are a per-job setting, so submit two jobs from the same source `video` — one with `"captions": false` — and pick per platform. To style burned-in captions on other footage, `caption_style` presets like `classic` and `bold` keep the look consistent. ## Get a Clip Job * Endpoint: [`GET /v3/ai-clipping/{job_id}`](/reference/get-ai-clipping) * Purpose: Fetch a clip job's live status and, as they render, its finished clips. ### Quick Example ```bash theme={null} curl -X GET "https://api.heygen.com/v3/ai-clipping/edf8d2c44ba441b89f395072b3ef7e34" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ### Path Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `job_id` | string | Yes | Unique job identifier returned by `POST /v3/ai-clipping` as `ai_clipping_id`. | ### Response ```json theme={null} { "data": { "id": "edf8d2c44ba441b89f395072b3ef7e34", "title": "Founder interview", "status": "completed", "input_language": "en", "source_duration": 1264.4, "progress": 100, "clips": [ { "id": "92799db89cdf444eb40a3d3db4378f4c", "status": "completed", "title": "Why we changed our pricing", "duration_seconds": 26.4, "aspect_ratio": "portrait", "virality_score": 65, "thumbnail_url": "https://resource2.heygen.ai/video_repurpose/.../1280x720.jpeg", "video_url": "https://resource2.heygen.ai/video_repurpose/.../1280x720.mp4?..." } ], "created_at": 1784649990 } } ``` ### Response Fields | Field | Type | Description | | - | - | - | | `id` | string | Unique job identifier. | | `title` | string or null | Display title for the job. | | `status` | string | Job lifecycle status: `pending`, `running`, `completed`, `failed`, or `cancelled`. | | `input_language` | string or null | Detected or supplied source language code. | | `source_duration` | number or null | Duration of the source video in seconds. | | `progress` | integer | Approximate progress (0–100). `100` when all clips are completed. | | `clips` | array | Produced clips. Populates as each clip renders — empty until the first one finishes. | | `clips[].id` | string | Unique clip identifier. | | `clips[].status` | string | Per-clip status: `pending`, `completed`, or `failed`. | | `clips[].title` | string or null | Model-generated clip title. | | `clips[].duration_seconds` | number or null | Final clip length in seconds. Populated when the clip completes. | | `clips[].aspect_ratio` | string or null | Framing of the finished clip. | | `clips[].virality_score` | integer or null | Model-predicted virality score (0–100). | | `clips[].thumbnail_url` | string or null | Pre-signed thumbnail URL. | | `clips[].video_url` | string or null | Pre-signed MP4 download URL. | | `callback_id` | string or null | Client-provided callback ID. | | `created_at` | integer | Unix timestamp (seconds) of job creation. | | `failure_message` | string or null | Error description. Only present when status is `failed`. | Each `video_url` and `thumbnail_url` is a pre-signed link with a limited lifetime. Download the file (or hand the URL to a downstream step) soon after the job completes rather than caching it for later. ## List Clip Jobs * Endpoint: [`GET /v3/ai-clipping`](/reference/list-ai-clipping) * Purpose: List clip jobs with cursor-based pagination. ### Quick Example ```bash theme={null} curl -X GET "https://api.heygen.com/v3/ai-clipping?limit=10" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ### Query Parameters | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `limit` | integer | No | `10` | Results per page. | | `token` | string | No | — | Opaque cursor token. Pass the `next_token` from a prior response to fetch the next page. | ### Response ```json theme={null} { "data": [ { "id": "edf8d2c44ba441b89f395072b3ef7e34", "title": "Founder interview", "status": "completed", "progress": 100, "created_at": 1784649990 } ], "has_more": false, "next_token": null } ``` The list endpoint reports coarse `progress`; poll [`GET /v3/ai-clipping/{job_id}`](#get-a-clip-job) for live in-flight progress on a specific job. ## Delete a Clip Job * Endpoint: [`DELETE /v3/ai-clipping/{job_id}`](/reference/delete-ai-clipping) * Purpose: Permanently delete a clip job and its clips. ### Quick Example ```bash theme={null} curl -X DELETE "https://api.heygen.com/v3/ai-clipping/edf8d2c44ba441b89f395072b3ef7e34" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ### Response ```json theme={null} { "data": { "id": "edf8d2c44ba441b89f395072b3ef7e34" } } ``` ## Polling Pattern Clip jobs are processed asynchronously. Poll until status reaches `completed` or `failed`. Status transitions: `pending` → `running` → `completed` | `failed` ```bash theme={null} while true; do STATUS=$(curl -s "https://api.heygen.com/v3/ai-clipping/$JOB_ID" \ -H "X-Api-Key: $HEYGEN_API_KEY" | jq -r '.data.status') echo "Status: $STATUS" [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break sleep 10 done ``` For long recordings, prefer a [`callback_url`](/docs/webhooks) over tight polling — HeyGen will POST you the finished job instead (`ai_clipping.success` / `ai_clipping.fail` [webhook events](/docs/webhook-events)). ## Asset Inputs The `video` field accepts two input formats: **By URL** — any publicly accessible HTTPS link: ```json theme={null} { "type": "url", "url": "https://example.com/recording.mp4" } ``` **By asset ID** — reference a file previously uploaded via [`POST /v3/assets`](/reference/upload-asset) (see [Upload Assets](/docs/upload-assets)): ```json theme={null} { "type": "asset_id", "asset_id": "asset_xyz789" } ``` ## Full Example ```python theme={null} import time import requests API_KEY = "YOUR_API_KEY" BASE = "https://api.heygen.com" HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"} def clip_video(video_url, prompt=None): """Start a clip job, wait for it, and return the finished clips.""" body = { "video": {"type": "url", "url": video_url}, "output_settings": { "duration_types": ["30", "60"], "aspect_ratio": "portrait", "captions": False, }, } if prompt: body["output_settings"]["prompt"] = prompt job_id = requests.post( f"{BASE}/v3/ai-clipping", headers=HEADERS, json=body ).json()["data"]["ai_clipping_id"] while True: job = requests.get(f"{BASE}/v3/ai-clipping/{job_id}", headers=HEADERS).json()["data"] if job["status"] in ("completed", "failed"): break time.sleep(10) if job["status"] == "failed": raise RuntimeError(job.get("failure_message") or "clip job failed") return job["clips"] for clip in clip_video("https://example.com/interview.mp4", prompt="Best product moments"): print(f"{clip['title']} ({clip['duration_seconds']}s) -> {clip['video_url']}") ``` Pairing clips with audio? The same [Tools](/background-music) suite covers [background music](/background-music) and [sound effects](/sound-effects) to score your cuts. # Assets Source: https://heygen-1fa696a7.mintlify.app/assets Upload, list, and manage images, audio, and video files via the HeyGen Assets API. Reference uploaded assets in any avatar video, translation, or lipsync. Upload images, videos, audio, or PDFs to get an `asset_id` you can reference in other endpoints — like `POST /v3/video-agents`, `POST /v3/videos`, or `POST /v3/avatars`. ## Upload an Asset ```bash theme={null} curl -X POST https://api.heygen.com/v3/assets \ -H "x-api-key: YOUR_API_KEY" \ -F "file=@./my-photo.png" ``` ```json Response theme={null} { "data": { "asset_id": "ast_abc123", "url": "https://files.heygen.com/asset/ast_abc123.png", "mime_type": "image/png", "size_bytes": 204800 } } ``` ## Supported File Types | Category | Formats | | - | - | | Image | PNG, JPEG | | Video | MP4, WebM | | Audio | MP3, WAV | | Document | PDF | Max file size: **32 MB**. MIME type is auto-detected from file bytes. For larger files, use the direct upload flow below. ## Direct Upload for Large Files `POST /v3/assets` proxies file bytes through the API, which is why it's capped at 32 MB. For larger files, use the presigned direct upload flow — three required steps: 1. [`POST /v3/assets/direct-uploads`](/reference/create-asset-upload) with `filename`, `content_type`, and exact `size_bytes` → returns `asset_id`, a presigned `upload_url`, and `upload_headers`. 2. `PUT` the raw file bytes to `upload_url`, sending `upload_headers` verbatim, before the URL expires (`expires_in_seconds`). 3. [`POST /v3/assets/{asset_id}/complete`](/reference/complete-asset-upload) to finalize. Idempotent. The `asset_id` is not usable until this step succeeds. ```bash theme={null} # 1. Initialize curl -X POST https://api.heygen.com/v3/assets/direct-uploads \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"filename": "footage.mp4", "content_type": "video/mp4", "size_bytes": 134217728}' # 2. PUT the file bytes to the returned upload_url (with upload_headers) curl -X PUT "UPLOAD_URL" -H "Content-Type: video/mp4" --upload-file ./footage.mp4 # 3. Complete curl -X POST https://api.heygen.com/v3/assets/ASSET_ID/complete \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Direct upload accepts up to 200 MB per video, image, or PDF file and 1.5 GB per audio file (also returned as `max_bytes` in the initialize response). See [Upload Assets](/docs/upload-assets#upload-large-files-direct-upload) for full examples in Python and Node.js. To upload up to 100 files in one call, use the [batch variant](/batch-assets) of this flow. ## Using Assets Once uploaded, reference the `asset_id` anywhere the API accepts asset inputs: ```json theme={null} // In POST /v3/video-agents (file attachments) { "prompt": "Explain this diagram", "files": [{ "type": "asset_id", "asset_id": "ast_abc123" }] } ``` ```json theme={null} // In POST /v3/avatars (photo avatar) { "type": "photo", "name": "My Avatar", "file": { "type": "asset_id", "asset_id": "ast_abc123" } } ``` Anywhere that accepts an asset also accepts a direct URL (`{"type": "url", "url": "https://..."}`) or base64 (`{"type": "base64", "media_type": "image/png", "data": "..."}`). URL inputs are capped at 16 MB for images and 32 MB for video, audio, and PDF, and signing the URL does not change that. For larger files, upload via the direct upload flow and pass the `asset_id`. Use `asset_id` when you need to reuse the same file across multiple requests. An `asset_id` does not expire: reuse it across requests and retries until you delete it. ## Delete an Asset ```bash theme={null} curl -X DELETE https://api.heygen.com/v3/assets/ast_abc123 \ -H "x-api-key: YOUR_API_KEY" ``` [`DELETE /v3/assets/{asset_id}`](/reference/delete-asset) removes the asset, and later requests that use its `asset_id` return `asset_not_found`. Deleting an avatar group with [`DELETE /v3/avatars/{group_id}`](/reference/delete-avatar-group) does not delete the assets used to create it, so delete each `asset_id` separately. See [Asset lifetime and deletion](/docs/upload-assets#asset-lifetime-and-deletion). # Audio to Video Source: https://heygen-1fa696a7.mintlify.app/audio-to-video Bring your own audio — a voice memo, a podcast segment, a dubbed track — and lip-sync it onto your avatar, digital twin, or any image with one HeyGen API call. ## Overview Audio to Video turns a recorded audio track into a talking video: you supply the audio, pick who says it, and HeyGen handles the delivery and render. It's the same [`POST /v3/videos`](/reference/create-video) endpoint used for script-driven videos — the difference is that you pass `audio_url` (or `audio_asset_id`) instead of `script`, and the video's length follows your audio, up to 30 minutes. Two subject choices, both accepting the same audio fields: | `type` | Subject | You provide | | - | - | - | | `avatar` | An avatar you already have — digital twin, studio avatar, or photo avatar look | `avatar_id` | | `image` | Any single-person image, animated on the fly | `image` (URL, asset ID, or base64) | Use it when the voice already exists: preserving a real person's recorded voice on their digital twin, dubbing localized audio onto one presenter, or lip-syncing audio produced by another tool. ## Quick Example Lip-sync an audio file onto an avatar: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "avatar_id": "YOUR_AVATAR_ID", "audio_url": "https://example.com/narration.mp3", "title": "Audio to video — narration", "aspect_ratio": "auto", "resolution": "1080p" }' ``` ```json Response theme={null} { "data": { "video_id": "4086e92fc7a54d67b62ef93b1ccf53db", "status": "waiting", "output_format": "mp4" } } ``` Generation is asynchronous. Poll [`GET /v3/videos/{video_id}`](/reference/get-video) until `status` is `completed`, then download `video_url` — or pass a `callback_url` to receive a [webhook](/docs/webhooks) instead. ## Audio Sources Provide exactly one audio source per request — audio fields are mutually exclusive with `script`: | Field | What it takes | | - | - | | `audio_url` | Public HTTPS URL of an audio file. | | `audio_asset_id` | Asset ID of an uploaded audio file (MP3 or WAV, max 32 MB) — from [`POST /v3/assets`](/reference/upload-asset). | One request renders up to **30 minutes** of audio. For a longer recording, split it into segments of 30 minutes or less and send one per request — or compose them as scenes of a [studio video](/studio-videos) to get a single output. See [Usage Limits](/docs/usage-limits#output-video-specifications). Have text and a voice instead of a recording? Two options: pass `script` + `voice_id` directly (skip the audio step entirely), or synthesize audio first with [`POST /v3/voices/speech`](/reference/generate-speech) — its response `audio_url` can be passed straight into `audio_url` here. The TTS route is handy when you want to reuse the same generated narration across several videos or archive it separately. ## With an Avatar or Digital Twin `"type": "avatar"` works with any avatar ID you own — a [digital twin](/generate-avatar-video) trained from real footage, a studio avatar, or a [photo avatar](/photo-avatar) look. ```json theme={null} { "type": "avatar", "avatar_id": "YOUR_AVATAR_ID", "audio_asset_id": "YOUR_AUDIO_ASSET_ID", "background": { "type": "color", "value": "#0b1220" } } ``` | Parameter | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Must be `avatar`. | | `avatar_id` | string | Yes | Video avatar or photo avatar look ID. Browse with [`GET /v3/avatars/looks`](/reference/list-avatar-looks). | | `audio_url` / `audio_asset_id` | string | One required | The audio to lip-sync — see [Audio Sources](#audio-sources). | | `engine` | object | No | Defaults to [Avatar IV](/avatar-iv). Pass `{ "type": "avatar_v" }` for [Avatar V](/avatar-v) on eligible looks (check `supported_api_engines` on the look). | | `motion_prompt` | string | No | Natural-language body-motion and gesture control. Photo avatars on either engine; video avatars on Avatar V. | | `expressiveness` | string | No | Expressiveness level for photo avatars on Avatar IV. Defaults to `low`. | | `background` | object | No | Background behind the avatar: `{ "type": "color", "value": "#RRGGBB" }` for a solid color, or `{ "type": "image", ... }` with a `url` or `asset_id`. | | `remove_background` | boolean | No | Remove the avatar background (video avatars must be trained with matting enabled). | | `output_format` | string | No | `mp4` (default) or `webm` for a transparent background — see [Transparent Background Videos](/transparent-background-videos). | ## With an Image `"type": "image"` animates any single-person image with lip-sync to your audio — no avatar creation step. It's [Image to Video](/image-to-video) with your own soundtrack. ```json theme={null} { "type": "image", "image": { "type": "asset_id", "asset_id": "YOUR_IMAGE_ASSET_ID" }, "audio_url": "https://example.com/narration.mp3" } ``` | Parameter | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Must be `image`. | | `image` | object | Yes | Image to animate: `{ "type": "url", "url": "..." }`, `{ "type": "asset_id", "asset_id": "..." }`, or `{ "type": "base64", "media_type": "image/png", "data": "..." }`. | | `audio_url` / `audio_asset_id` | string | One required | The audio to lip-sync — see [Audio Sources](#audio-sources). | | `motion_prompt` | string | No | Natural-language body-motion control. | | `expressiveness` | string | No | Expressiveness level. Defaults to `low`. | ## Output Settings Both subject types share the same output controls: | Parameter | Type | Default | Description | | - | - | - | - | | `title` | string | — | Display title in the HeyGen dashboard. | | `aspect_ratio` | string | `16:9` | `16:9`, `9:16`, `4:5`, `5:4`, `1:1`, or `auto` (preserves the source's aspect ratio — recommended). | | `resolution` | string | — | `720p` or `1080p`. | | `fit` | string | — | How the subject fills the canvas: `cover` (fill, may crop) or `contain` (fit entirely, may show background). | | `caption` | object | — | Caption settings. A sidecar subtitle file is returned via `subtitle_url`; add `"style": "default"` to also burn captions into the video. | | `callback_url` | string | — | [Webhook](/docs/webhooks) URL notified when the video is ready. | | `callback_id` | string | — | Caller-defined identifier echoed back in the webhook payload. | ## Full Example Record-to-video in one script: upload the audio, lip-sync it onto an avatar, and wait for the result. ```python theme={null} import time import requests API_KEY = "YOUR_API_KEY" BASE = "https://api.heygen.com" HEADERS = {"x-api-key": API_KEY} def audio_to_video(audio_path, avatar_id): """Upload an audio file, lip-sync it onto an avatar, return the video URL.""" with open(audio_path, "rb") as f: asset = requests.post( f"{BASE}/v3/assets", headers=HEADERS, files={"file": f} ).json()["data"] video_id = requests.post( f"{BASE}/v3/videos", headers={**HEADERS, "Content-Type": "application/json"}, json={ "type": "avatar", "avatar_id": avatar_id, "audio_asset_id": asset["id"], "aspect_ratio": "auto", "resolution": "1080p", }, ).json()["data"]["video_id"] while True: video = requests.get(f"{BASE}/v3/videos/{video_id}", headers=HEADERS).json()["data"] if video["status"] in ("completed", "failed"): break time.sleep(10) if video["status"] == "failed": raise RuntimeError(video.get("failure_message") or "video generation failed") return video["video_url"] print(audio_to_video("narration.mp3", "YOUR_AVATAR_ID")) ``` Swap the `json` payload for the [`image` shape](#with-an-image) to drive the same pipeline from a photo instead of an avatar. # Avatar III Source: https://heygen-1fa696a7.mintlify.app/avatar-iii Avatar III is a HeyGen rendering engine on the v3 API, built around a dedicated photo-to-video pipeline — a higher-quality re-engineered model for photo avatars and video avatars (digital twins and studio avatars). A single `engine` value resolves to the right product based on the avatar look type — mirroring how `avatar_iv` already serves both photo and video avatars: | Look type | Resolves to | Max resolution | | - | - | - | | `digital_twin` | Digital Twin | 4K | | `studio_avatar` | Digital Twin | 4K | | `photo_avatar` | Photo Avatar | 1080p | ## Supported avatar types `digital_twin`, `studio_avatar`, `photo_avatar` — pass the look's `avatar_id` in the request. Studio avatar looks are video avatars, so they take the same Digital Twin pipeline as `digital_twin` looks, including 4K support. For `motion_prompt`, `expressiveness`, or animating an arbitrary image, use [Avatar IV](/avatar-iv). ## Example request Select Avatar III by passing `"engine": { "type": "avatar_iii" }` in your [`POST /v3/videos`](/reference/create-video) request: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "avatar_id": "YOUR_PHOTO_AVATAR_LOOK_ID", "script": "Hello from Avatar III.", "voice_id": "YOUR_VOICE_ID", "resolution": "1080p", "engine": { "type": "avatar_iii" } }' ``` Video generation is asynchronous — the response returns a `video_id` you poll with `GET /v3/videos/{video_id}`. To compare engines, see [Models](/models). # Avatar IV Source: https://heygen-1fa696a7.mintlify.app/avatar-iv Avatar IV is the default HeyGen v3 rendering engine - the broadest avatar support, arbitrary image animation, motion_prompt, and expressiveness control. ## Supported avatar types `studio_avatar`, `digital_twin`, `photo_avatar`, `image` (arbitrary), `prompt` ## Exclusive features * `motion_prompt` — a natural-language string controlling body motion and hand gestures (e.g. `"walk towards the camera slowly"`). Available for photo avatars and arbitrary images. * `expressiveness` — controls energy and range of movement: `high`, `medium`, or `low`. Available for photo avatars and arbitrary images. Defaults to `low`. * **Arbitrary image support** — animate any image by setting `type: "image"`, with no registered avatar required. ## Example request Avatar IV is the default engine, so you can omit `engine` entirely in your [`POST /v3/videos`](/reference/create-video) request: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "avatar_id": "YOUR_LOOK_ID", "script": "Hello from Avatar IV.", "voice_id": "YOUR_VOICE_ID", "resolution": "1080p" }' ``` To request it explicitly — or to be unambiguous in code that switches engines — pass `"engine": { "type": "avatar_iv" }`: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "avatar_id": "YOUR_LOOK_ID", "script": "Hello from Avatar IV.", "voice_id": "YOUR_VOICE_ID", "resolution": "1080p", "engine": { "type": "avatar_iv" } }' ``` Video generation is asynchronous — the response returns a `video_id` you poll with `GET /v3/videos/{video_id}`. For a full walkthrough of creating a Digital Twin video and polling for completion, see the [Digital Twin guide](/generate-avatar-video). To compare engines, see [Models](/models); for the highest-fidelity motion, see [Avatar V](/avatar-v). # Avatar V Source: https://heygen-1fa696a7.mintlify.app/avatar-v Avatar V is HeyGen's highest-fidelity v3 rendering engine, using cross-reference-driven animation for the most natural motion and lip-sync. Opt-in per look. ## Supported avatar types `digital_twin` — for eligible looks. ## Supported parameters * `motion_prompt` — natural-language control of body motion and hand gestures. * `reference_look_id` *(optional)* — a `digital_twin` look to use as the animation reference. Must belong to the same avatar group as `avatar_id`, and a non-public one must clear its group's consent requirements. When omitted, the digital twin self-references. For `expressiveness` control, use [Avatar IV](/avatar-iv). ## Checking eligibility Avatar V is opt-in per look. Before using it, fetch the look and check `supported_api_engines`: ```bash theme={null} GET /v3/avatars/looks/{look_id} ``` ```json theme={null} { "id": "lk_abc123", "name": "My Digital Twin", "avatar_type": "digital_twin", "supported_api_engines": ["avatar_iv", "avatar_v"] } ``` Avatar V is available when `"avatar_v"` appears in `supported_api_engines`. ## Example request Select Avatar V by passing `"engine": { "type": "avatar_v" }` in your [`POST /v3/videos`](/reference/create-video) request: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "avatar_id": "YOUR_LOOK_ID", "script": "Hello from Avatar V.", "voice_id": "YOUR_VOICE_ID", "resolution": "1080p", "engine": { "type": "avatar_v" } }' ``` Video generation is asynchronous — the response returns a `video_id` you poll with `GET /v3/videos/{video_id}`. See the [Digital Twin guide](/generate-avatar-video) for the end-to-end request and polling flow, [Models](/models) for an engine comparison, or [Avatar IV](/avatar-iv) for the default engine. # Background music Source: https://heygen-1fa696a7.mintlify.app/background-music Search HeyGen's background-music catalog with natural language and get ready-to-use, pre-signed audio URLs for your videos via the HeyGen API. Find background music by describing the vibe you want — "upbeat lofi hip-hop", "tense cinematic riser", "subtle ambient corporate" — and get back ranked tracks, each with a pre-signed download URL. Search is semantic, not keyword-based, so plain-language descriptions work best. ## Search the Catalog Send a `GET` request to `/v3/audio/sounds` with a natural-language `query`: ```bash theme={null} curl -X GET "https://api.heygen.com/v3/audio/sounds?query=upbeat%20corporate%20background%20music&limit=3" \ -H "x-api-key: YOUR_API_KEY" ``` ```json Response theme={null} { "data": [ { "id": "4cbcca493220487bbae26a2c42dba5e9", "name": "Astral Generated Music: 4cbcca49", "description": "upbeat professional background music", "audio_url": "https://heygen-product.s3-accelerate.amazonaws.com/astral_generated_music/4cbcca49...wav?X-Amz-Algorithm=...", "duration": 30.0, "score": 0.933, "type": "music" }, { "id": "93a98c35d9654029be397d8d27a06da0", "name": "Astral Generated Music: 93a98c35", "description": "Modern, upbeat, and inspiring corporate background music with a light electronic beat.", "audio_url": "https://heygen-product.s3-accelerate.amazonaws.com/astral_generated_music/93a98c35...wav?X-Amz-Algorithm=...", "duration": 60.0, "score": 0.911, "type": "music" } ], "has_more": true, "next_token": "eyJvZmZzZXQiOiAzLCAiX3R5cGUiOiAibXVzaWMifQ==" } ``` ## Query Parameters | Parameter | Type | Default | Description | | - | - | - | - | | `query` | string | — | **Required.** Natural-language description of the audio you want, e.g. `tense cinematic riser`. Results are ranked by semantic similarity to this text. | | `limit` | integer | `10` | Maximum number of results to return (1–50). | | `min_score` | number | `0.7` | Minimum semantic similarity score (0–1). Tracks scoring below this are omitted. Raise it for tighter matches; lower it to widen the net. | | `type` | string | `music` | Audio content type. `music` (the default) searches the background-music catalog; set `sound_effects` to search [sound effects](/sound-effects). | | `token` | string | — | Opaque pagination cursor. Pass the `next_token` from a prior response to fetch the next page. | ## Response Fields | Field | Type | Description | | - | - | - | | `data[].id` | string | Stable identifier for the track. | | `data[].name` | string | Display name of the track. | | `data[].description` | string | Human-readable description of the track's mood and instrumentation. | | `data[].audio_url` | string | Pre-signed download URL for the audio file (WAV). | | `data[].duration` | number | Track length in seconds. | | `data[].score` | number | Semantic similarity score (0–1) against your `query`. Results are returned highest-first. | | `data[].type` | string | `music` for results from the background-music catalog. | | `has_more` | boolean | `true` if more results are available beyond this page. | | `next_token` | string | Cursor for the next page. Pass it as `token` on your next request. Absent when `has_more` is `false`. | Each `audio_url` is a pre-signed link with a limited lifetime. Download the file (or hand the URL to a downstream step) soon after searching rather than caching it for later. ## Paginate Through Results When `has_more` is `true`, pass the returned `next_token` as the `token` parameter to fetch the next page: ```bash theme={null} curl -X GET "https://api.heygen.com/v3/audio/sounds?query=upbeat%20corporate%20background%20music&limit=3&token=eyJvZmZzZXQiOiAzLCAiX3R5cGUiOiAibXVzaWMifQ==" \ -H "x-api-key: YOUR_API_KEY" ``` ## Full Example ```python theme={null} import requests from urllib.parse import urlencode API_KEY = "YOUR_API_KEY" BASE = "https://api.heygen.com" HEADERS = {"x-api-key": API_KEY} def search_music(query, limit=10, min_score=0.7): """Yield every matching track, following pagination.""" token = None while True: params = {"query": query, "limit": limit, "min_score": min_score} if token: params["token"] = token resp = requests.get( f"{BASE}/v3/audio/sounds?{urlencode(params)}", headers=HEADERS, ) page = resp.json() for track in page["data"]: yield track if not page.get("has_more"): break token = page["next_token"] # Grab the single best-matching track best = next(search_music("calm ambient piano for a product walkthrough", limit=1)) print(f"{best['name']} ({best['duration']}s, score {best['score']:.2f})") print(f"Download: {best['audio_url']}") ``` Searching from an AI agent instead of code? The same catalog is available through the [HeyGen MCP](/mcp/overview) via the `search_audio_sounds` tool. # Assets Source: https://heygen-1fa696a7.mintlify.app/batch-assets Upload up to 100 files in one batch: request presigned S3 slots in a single call, PUT the bytes in parallel, then finalize and poll one batch id. ## Overview Asset batches parallelize the [direct upload flow](/assets#direct-upload-for-large-files): instead of presigning, uploading, and completing files one by one, you request up to 100 presigned upload slots in a single call, PUT all the files in parallel, and finalize the whole batch with one request. The flow has three steps: 1. [`POST /v3/assets/direct-uploads/batches`](/reference/create-asset-upload-batch) — get an `asset_id` + presigned `upload_url` per file. 2. `PUT` each file's raw bytes to its `upload_url`, sending `upload_headers` verbatim. 3. [`POST /v3/assets/complete/batches`](/reference/complete-asset-upload-batch) — finalize, then poll [`GET /v3/assets/batches/{batch_id}`](/reference/get-asset-batch). A batch holds up to **100** files. No file bytes flow through the HeyGen API — uploads go straight to S3. On completion, each file is validated and ingested independently, so one bad file does not fail the rest. ## Step 1 — Create a Batch of Upload Slots * Endpoint: `POST /v3/assets/direct-uploads/batches` * Purpose: Presign up to 100 direct-to-S3 upload slots and return them synchronously with a `batch_id`. ### Quick Example ```bash theme={null} curl -X POST "https://api.heygen.com/v3/assets/direct-uploads/batches" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Webinar recordings — October", "files": [ { "filename": "session-1.mp4", "content_type": "video/mp4", "size_bytes": 157286400 }, { "filename": "session-2.mp4", "content_type": "video/mp4", "size_bytes": 183500800 } ] }' ``` ```json Response theme={null} { "data": { "batch_id": "batch_as_abc123", "items": [ { "asset_id": "asset_9f2c...", "upload_url": "https://heygen-uploads.s3.amazonaws.com/...", "upload_headers": { "Content-Type": "video/mp4" }, "expires_in_seconds": 86400, "max_bytes": 209715200, "status": "pending_upload" }, { "asset_id": "asset_81aa...", "upload_url": "https://heygen-uploads.s3.amazonaws.com/...", "upload_headers": { "Content-Type": "video/mp4" }, "expires_in_seconds": 86400, "max_bytes": 209715200, "status": "pending_upload" } ] } } ``` Slots are returned **in the submitted order**, one per file. ### Request Body | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `files` | array | Yes | — | Files to presign, each the same shape as [`POST /v3/assets/direct-uploads`](/reference/create-asset-upload). Between 1 and 100 items. | | `title` | string | No | `null` | Display name for the batch, shown in the HeyGen app. | | `callback_url` | string | No | `null` | Reserved for parity with the other batch APIs — track completion by [polling the batch](#step-3--complete-and-poll). | Each entry in `files`: | Field | Type | Required | Description | | - | - | - | - | | `filename` | string | Yes | Original filename for reference/metadata. The stored object's extension is derived from `content_type`. | | `content_type` | string | Yes | Declared MIME type (e.g. `video/mp4`, `image/png`, `audio/mpeg`, `application/pdf`). Verified against the stored bytes when the batch is completed. | | `size_bytes` | integer | Yes | Exact byte size of the file. Signed into the upload URL so it cannot be exceeded. | | `checksum_sha256` | string | No | SHA-256 of the file as hex. When provided, S3 enforces it on upload. | Each file can be up to 200 MB (209,715,200 bytes), or 1.5 GB for audio. The limit for each file's type is returned as `max_bytes`. A larger `size_bytes` fails the whole request with `Invalid file(s) at index N: File too large`. ### Idempotency Pass an `Idempotency-Key` header to make retries safe — replaying the same key returns the same batch and the same slots. ## Step 2 — Upload the Files `PUT` each file's raw bytes to its `upload_url`, sending the returned `upload_headers` verbatim. Uploads are plain S3 PUTs, so you can run them in parallel from any HTTP client: ```bash theme={null} curl -X PUT "https://heygen-uploads.s3.amazonaws.com/..." \ -H "Content-Type: video/mp4" \ --data-binary @session-1.mp4 ``` Each `upload_url` expires after `expires_in_seconds` — request a fresh batch if a slot lapses before you upload. ## Step 3 — Complete and Poll * Endpoint: `POST /v3/assets/complete/batches` * Purpose: Finalize every uploaded file in the batch. Call once, after all PUTs return `200`. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/assets/complete/batches" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "batch_id": "batch_as_abc123" }' ``` ```json Response theme={null} { "data": { "batch_id": "batch_as_abc123" } } ``` The call returns `202 Accepted` — validation and ingestion run asynchronously per file. The call is idempotent: repeating it re-drives the same batch. Poll [`GET /v3/assets/batches/{batch_id}`](/reference/get-asset-batch) for per-item progress: ```bash theme={null} curl -X GET "https://api.heygen.com/v3/assets/batches/batch_as_abc123?limit=100" \ -H "x-api-key: YOUR_API_KEY" ``` ```json Response theme={null} { "data": { "batch_id": "batch_as_abc123", "title": "Webinar recordings — October", "status": "processing", "total_items": 2, "counts_by_status": { "completed": 1, "processing": 1 }, "created_at": 1783891200, "items": [ { "item_index": 0, "status": "completed", "video_id": "asset_9f2c...", "error": null }, { "item_index": 1, "status": "processing", "video_id": null, "error": null } ], "has_more": false, "next_token": null } } ``` The batch read model is shared across the batch APIs, so the per-item id field is named `video_id` — for asset batches it holds the **asset id**. Once an item is `completed`, that asset id is usable anywhere assets are accepted, e.g. as `audio_asset_id` in [video creation](/generate-avatar-video) or as an `asset_id` source in [translation](/batch-video-translations) and [lipsync](/batch-lipsyncs) batches. ### Query Parameters | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `limit` | integer | No | `100` | Items per page, `1`–`100`. | | `token` | string | No | — | Opaque pagination cursor from a previous response's `next_token`. | The response shape matches the [video batch](/batch-videos#response-fields): aggregate `status` (`processing`, `completed`, or `failed`), `total_items`, `counts_by_status`, and a paged `items` array where each item carries `item_index`, `status` (`queued`, `processing`, `completed`, or `failed`), its id, and `error` details when failed. ## Bulk Statuses * Endpoint: `GET /v3/assets/statuses` * Purpose: Check up to 100 assets in one request — across batches, or for assets uploaded individually. ```bash theme={null} curl -X GET "https://api.heygen.com/v3/assets/statuses?batch_ids=batch_as_abc123" \ -H "x-api-key: YOUR_API_KEY" ``` | Parameter | Type | Required | Description | | - | - | - | - | | `asset_ids` | string | No | Comma-separated asset ids to look up. | | `batch_ids` | string | No | Comma-separated batch ids; each expands to its member assets. | Statuses are `queued`, `processing`, `completed`, or `failed`, plus `not_found` for unknown or unowned ids. # Lipsyncs Source: https://heygen-1fa696a7.mintlify.app/batch-lipsyncs Submit up to 100 lipsync requests in a single call. Re-sync many videos to new audio tracks together, then poll one batch id for per-item progress. ## Overview Lipsync batches let you submit many lipsync jobs in one request instead of calling [`POST /v3/lipsyncs`](/reference/create-lipsync) once per job. You send an array of lipsync payloads, get back a single `batch_id` right away, and poll that one id for the status and id of every item. Each item in a batch is a standard lipsync payload — the exact shape [`POST /v3/lipsyncs`](/reference/create-lipsync) accepts — so anything you can lipsync on its own can be batched, in either [Speed](/lipsync-speed) or [Precision](/lipsync-precision) mode. A batch holds up to **100** items. Each payload becomes exactly one batch item, and each item is created and processed independently — one bad source does not fail the rest. ## Create a Batch * Endpoint: `POST /v3/lipsyncs/batches` * Purpose: Queue up to 100 lipsync payloads and return a `batch_id` immediately. ### Quick Example ```bash theme={null} curl -X POST "https://api.heygen.com/v3/lipsyncs/batches" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Course module redubs", "callback_url": "https://example.com/hooks/heygen", "lipsyncs": [ { "video": { "type": "url", "url": "https://example.com/module-1.mp4" }, "audio": { "type": "url", "url": "https://example.com/module-1-v2.mp3" }, "title": "Module 1 — updated narration", "mode": "speed" }, { "video": { "type": "asset_id", "asset_id": "asset_abc123" }, "audio": { "type": "asset_id", "asset_id": "asset_def456" }, "title": "Module 2 — updated narration", "mode": "precision" } ] }' ``` ```json Response theme={null} { "data": { "batch_id": "batch_ls_abc123" } } ``` The call returns `202 Accepted` — the batch is queued, not finished. Use the returned `batch_id` to [poll for progress](#get-a-batch). ### Request Body | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `lipsyncs` | array | Yes | — | Lipsync payloads, each identical in shape to [`POST /v3/lipsyncs`](/reference/create-lipsync). Between 1 and 100 items. | | `title` | string | No | `null` | Display name for the batch, shown in the HeyGen app. | | `callback_url` | string | No | `null` | [Webhook](/docs/webhooks) URL invoked once when every item in the batch reaches a terminal state. | Each entry in `lipsyncs` takes the full set of lipsync options — `video` and `audio` (each as a `url` or `asset_id`), `mode` (`speed` or `precision`), plus captions, partial-range, and format controls. See [`POST /v3/lipsyncs`](/reference/create-lipsync) for every field. ### Idempotency Pass an `Idempotency-Key` header to make retries safe — replaying the same key returns the original batch instead of creating a duplicate. ## Get a Batch * Endpoint: `GET /v3/lipsyncs/batches/{batch_id}` * Purpose: Return the batch's aggregate status plus one page of items with their id and per-item status. ### Quick Example ```bash theme={null} curl -X GET "https://api.heygen.com/v3/lipsyncs/batches/batch_ls_abc123?limit=100" \ -H "x-api-key: YOUR_API_KEY" ``` ```json Response theme={null} { "data": { "batch_id": "batch_ls_abc123", "title": "Course module redubs", "status": "processing", "total_items": 2, "counts_by_status": { "completed": 1, "processing": 1 }, "created_at": 1783891200, "items": [ { "item_index": 0, "status": "completed", "video_id": "ls_9f2c...", "error": null }, { "item_index": 1, "status": "processing", "video_id": null, "error": null } ], "has_more": false, "next_token": null } } ``` The batch read model is shared across the batch APIs, so the per-item id field is named `video_id` — for lipsync batches it holds the **lipsync id**. Use it with [`GET /v3/lipsyncs/{lipsync_id}`](/reference/get-lipsync) for the full record and download URL. ### Query Parameters | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `limit` | integer | No | `100` | Items per page, `1`–`100`. | | `token` | string | No | — | Opaque pagination cursor from a previous response's `next_token`. | The response shape matches the [video batch](/batch-videos#response-fields): aggregate `status` (`processing`, `completed`, or `failed`), `total_items`, `counts_by_status`, and a paged `items` array where each item carries `item_index`, `status` (`queued`, `processing`, `completed`, or `failed`), its id, and `error` details when failed. ## Bulk Statuses * Endpoint: `GET /v3/lipsyncs/statuses` * Purpose: Check up to 100 lipsyncs in one request — across batches, or for jobs created individually. ```bash theme={null} curl -X GET "https://api.heygen.com/v3/lipsyncs/statuses?batch_ids=batch_ls_abc123" \ -H "x-api-key: YOUR_API_KEY" ``` | Parameter | Type | Required | Description | | - | - | - | - | | `lipsync_ids` | string | No | Comma-separated lipsync ids to look up. | | `batch_ids` | string | No | Comma-separated batch ids; each expands to its member lipsyncs. | Statuses are `queued`, `processing`, `completed`, or `failed`, plus `not_found` for unknown or unowned ids. ## Tracking completion * **Webhook (push).** Set `callback_url` on the batch to be notified once, when the last item reaches a terminal state. See [Webhooks](/docs/webhooks) to register an endpoint and verify signatures. * **Polling (pull).** Call `GET /v3/lipsyncs/batches/{batch_id}` and read `counts_by_status`. Items surface their lipsync id as soon as the job is created, so you can start fetching finished renders with [`GET /v3/lipsyncs/{lipsync_id}`](/reference/get-lipsync) while the rest of the batch is still processing. # Video Translations Source: https://heygen-1fa696a7.mintlify.app/batch-video-translations Submit up to 100 video translation requests in a single call. Fan one source video out to many languages, then poll one batch id for per-item progress. ## Overview Translation batches let you submit many translations in one request instead of calling [`POST /v3/video-translations`](/reference/create-video-translation) once per job. You send an array of translation payloads, get back a single `batch_id` right away, and poll that one id for the status and id of every item. Each item in a batch is a standard translation payload — the exact shape [`POST /v3/video-translations`](/reference/create-video-translation) accepts — so anything you can translate on its own can be batched, in either [Speed](/docs/video-translate) or [Precision](/docs/video-translation-precision) mode. A batch holds up to **100** items. A single payload targeting multiple `output_languages` expands to **one batch item per language**, and the expanded count is what the 100-item cap applies to. Each item is created and processed independently, so one bad source does not fail the rest. ## Create a Batch * Endpoint: `POST /v3/video-translations/batches` * Purpose: Queue up to 100 translation payloads and return a `batch_id` immediately. ### Quick Example One source video fanned out to three languages — this creates three batch items: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/video-translations/batches" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Q3 keynote — localized cuts", "callback_url": "https://example.com/hooks/heygen", "video_translations": [ { "video": { "type": "url", "url": "https://example.com/keynote.mp4" }, "title": "Q3 keynote", "output_languages": ["Spanish (Spain)", "French", "Japanese"], "mode": "speed" } ] }' ``` ```json Response theme={null} { "data": { "batch_id": "batch_tr_abc123" } } ``` The call returns `202 Accepted` — the batch is queued, not finished. Use the returned `batch_id` to [poll for progress](#get-a-batch). ### Request Body | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `video_translations` | array | Yes | — | Translation payloads, each identical in shape to [`POST /v3/video-translations`](/reference/create-video-translation). Up to 100 items after language expansion. | | `title` | string | No | `null` | Display name for the batch, shown in the HeyGen app. | | `callback_url` | string | No | `null` | [Webhook](/docs/webhooks) URL invoked once when every item in the batch reaches a terminal state. | Each entry in `video_translations` takes the full set of translation options — `video` (as a `url` or `asset_id`), `output_languages` (use [`GET /v3/video-translations/languages`](/reference/list-supported-translation-languages) for valid values), `mode` (`speed` or `precision`), plus captions, [brand glossary](/docs/brand-glossary), stock voice, partial-range, and format controls. See [`POST /v3/video-translations`](/reference/create-video-translation) for every field. ### Idempotency Pass an `Idempotency-Key` header to make retries safe — replaying the same key returns the original batch instead of creating a duplicate. ## Get a Batch * Endpoint: `GET /v3/video-translations/batches/{batch_id}` * Purpose: Return the batch's aggregate status plus one page of items with their id and per-item status. ### Quick Example ```bash theme={null} curl -X GET "https://api.heygen.com/v3/video-translations/batches/batch_tr_abc123?limit=100" \ -H "x-api-key: YOUR_API_KEY" ``` ```json Response theme={null} { "data": { "batch_id": "batch_tr_abc123", "title": "Q3 keynote — localized cuts", "status": "processing", "total_items": 3, "counts_by_status": { "completed": 2, "processing": 1 }, "created_at": 1783891200, "items": [ { "item_index": 0, "status": "completed", "video_id": "vt_9f2c...", "error": null }, { "item_index": 1, "status": "completed", "video_id": "vt_81aa...", "error": null }, { "item_index": 2, "status": "processing", "video_id": null, "error": null } ], "has_more": false, "next_token": null } } ``` The batch read model is shared across the batch APIs, so the per-item id field is named `video_id` — for translation batches it holds the **video translation id**. Use it with [`GET /v3/video-translations/{video_translation_id}`](/reference/get-video-translation) for the full record and download URL. ### Query Parameters | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `limit` | integer | No | `100` | Items per page, `1`–`100`. | | `token` | string | No | — | Opaque pagination cursor from a previous response's `next_token`. | The response shape matches the [video batch](/batch-videos#response-fields): aggregate `status` (`processing`, `completed`, or `failed`), `total_items`, `counts_by_status`, and a paged `items` array where each item carries `item_index`, `status` (`queued`, `processing`, `completed`, or `failed`), its id, and `error` details when failed. ## Bulk Statuses * Endpoint: `GET /v3/video-translations/statuses` * Purpose: Check up to 100 translations in one request — across batches, or for jobs created individually. ```bash theme={null} curl -X GET "https://api.heygen.com/v3/video-translations/statuses?batch_ids=batch_tr_abc123" \ -H "x-api-key: YOUR_API_KEY" ``` | Parameter | Type | Required | Description | | - | - | - | - | | `video_translation_ids` | string | No | Comma-separated translation ids to look up. | | `batch_ids` | string | No | Comma-separated batch ids; each expands to its member translations. | Statuses are `queued`, `processing`, `completed`, or `failed`, plus `not_found` for unknown or unowned ids. ## Tracking completion * **Webhook (push).** Set `callback_url` on the batch to be notified once, when the last item reaches a terminal state, or subscribe to `video_translate.success` / `video_translate.fail` [webhook events](/docs/webhook-events) for per-item notifications. * **Polling (pull).** Call `GET /v3/video-translations/batches/{batch_id}` and read `counts_by_status`. Items surface their translation id as soon as the job is created, so you can start fetching finished translations with [`GET /v3/video-translations/{video_translation_id}`](/reference/get-video-translation) while the rest of the batch is still processing. # Videos Source: https://heygen-1fa696a7.mintlify.app/batch-videos Submit up to 100 video creation requests in a single call. Queue avatar, image, and cinematic avatar videos together, then poll one batch id for per-item progress. ## Overview Video batches let you submit many videos in one request instead of calling [`POST /v3/videos`](/reference/create-video) once per video. You send an array of video payloads, get back a single `batch_id` right away, and poll that one id for the status and `video_id` of every item. Each item in a batch is a standard video creation payload — the exact shape [`POST /v3/videos`](/reference/create-video) accepts — so any [Digital Twin](/generate-avatar-video), [image](/image-to-video), or [Cinematic Avatar](/cinematic-avatar) video you can create on its own can be batched. A batch holds up to **100** items. Submission is asynchronous: the response acknowledges the batch, and each video renders on its own. Poll the batch to collect video ids as they become available. ## Create a Batch * Endpoint: `POST /v3/videos/batches` * Purpose: Queue up to 100 video creation payloads and return a `batch_id` immediately. ### Quick Example ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos/batches" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "October product update — regional cuts", "callback_url": "https://example.com/hooks/heygen", "videos": [ { "type": "avatar", "avatar_id": "YOUR_DIGITAL_TWIN_LOOK_ID", "script": "Hello from the North America team!", "voice_id": "YOUR_VOICE_ID" }, { "type": "avatar", "avatar_id": "YOUR_DIGITAL_TWIN_LOOK_ID", "script": "Hello from the EMEA team!", "voice_id": "YOUR_VOICE_ID" } ] }' ``` ```json Response theme={null} { "data": { "batch_id": "batch_abc123" } } ``` The call returns `202 Accepted` — the batch is queued, not finished. Use the returned `batch_id` to [poll for progress](#get-a-batch). ### Request Body | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `videos` | array | Yes | — | Video creation requests, each identical in shape to [`POST /v3/videos`](/reference/create-video). Between 1 and 100 items. | | `title` | string | No | `null` | Display name for the batch, shown in the HeyGen app. | | `callback_url` | string | No | `null` | [Webhook](/docs/webhooks) URL invoked once when every item in the batch reaches a terminal state. | Each entry in `videos` is discriminated by `type`: | `type` | Item shape | Guide | | - | - | - | | `avatar` | Avatar video from a script and voice | [Digital Twin](/generate-avatar-video) | | `image` | Video from a still image | [Image to Video](/image-to-video) | | `cinematic_avatar` | Cinematic Avatar video | [Cinematic Avatar](/cinematic-avatar) | ### Idempotency Pass an `Idempotency-Key` header to make retries safe — replaying the same key returns the original batch instead of creating a duplicate. If a request with that key is still being processed, the API responds with `409`. ## Get a Batch * Endpoint: `GET /v3/videos/batches/{batch_id}` * Purpose: Return the batch's aggregate status plus one page of items with their `video_id` and per-item status. ### Quick Example ```bash theme={null} curl -X GET "https://api.heygen.com/v3/videos/batches/batch_abc123?limit=100" \ -H "x-api-key: YOUR_API_KEY" ``` ```json Response theme={null} { "data": { "batch_id": "batch_abc123", "title": "October product update — regional cuts", "status": "processing", "total_items": 2, "counts_by_status": { "completed": 1, "processing": 1 }, "created_at": 1711929600, "items": [ { "item_index": 0, "status": "completed", "video_id": "vid_9f2c...", "error": null }, { "item_index": 1, "status": "processing", "video_id": null, "error": null } ], "has_more": false, "next_token": null } } ``` ### Query Parameters | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `limit` | integer | No | `100` | Items per page, `1`–`100`. | | `token` | string | No | — | Opaque pagination cursor from a previous response's `next_token`. | ### Response Fields | Field | Type | Description | | - | - | - | | `batch_id` | string | Batch identifier. | | `title` | string | Batch display name, if one was set. | | `status` | string | Aggregate status derived from item states: `processing`, `completed`, or `failed`. | | `total_items` | integer | Number of items submitted in this batch. | | `counts_by_status` | object | Item counts keyed by item status. | | `created_at` | integer | Batch creation time as a Unix timestamp. | | `items` | array | One page of batch items, ordered by `item_index`. | | `has_more` | boolean | Whether more items exist beyond this page. | | `next_token` | string | Cursor for the next page; `null` on the last page. | Each entry in `items`: | Field | Type | Description | | - | - | - | | `item_index` | integer | Zero-based position of this item in the submitted `videos` array. | | `status` | string | Item status: `queued`, `processing`, `completed`, or `failed`. | | `video_id` | string | Video id, present once the underlying video has been created. Use it with [`GET /v3/videos/{video_id}`](/reference/get-video) for the full record and download URL. | | `error` | object | Failure details, present when `status` is `failed`. | ## Tracking completion You have two ways to know when work finishes, and they pair well: * **Webhook (push).** Set `callback_url` on the batch to be notified once, when the last item reaches a terminal state. See [Webhooks](/docs/webhooks) to register an endpoint and verify signatures. * **Polling (pull).** Call `GET /v3/videos/batches/{batch_id}` and read `counts_by_status`. Individual items surface a `video_id` the moment their video is created, so you can start downloading finished videos while the rest of the batch is still rendering. For large batches, page through `items` with `limit` and `next_token` until `has_more` is `false`, then fetch each finished video with [`GET /v3/videos/{video_id}`](/reference/get-video). # Changelog Source: https://heygen-1fa696a7.mintlify.app/changelog Track every HeyGen API change, new endpoint, deprecation, and version bump in the API changelog. Subscribe via RSS or webhook to get notified on the next. **`language` is optional for professional voices** [`POST /v3/models/audio/voices`](/reference/create-or-retrain-an-audio-voice) in `professional` mode no longer needs `language`. Leave it out and training detects the language of each recording. The voice's `language` is `null` until it is `ACTIVE`. See [Professional Voice Clone](/docs/voices/professional-voice-clone). **Listen to a voice before you use it** [`GET /v3/models/audio/voices`](/reference/list-audio-voices) and [`GET /v3/models/audio/voices/{voice_id}`](/reference/get-an-audio-voice) now return `preview_audio_url` for every `ACTIVE` voice: instant, professional and public. It is a URL to a WAV of the voice reading a short preview script in its language. The URL can expire, so read the voice again rather than storing it. See [Instant Voice Clone](/docs/voices/heygen-voice-instant-clone#manage-voices). **Give an instant voice the sound of a setting** [`POST /v3/models/audio/voices`](/reference/create-or-retrain-an-audio-voice) in `instant` mode now accepts `image`: a picture of the speaker in their setting. The voice takes on the ambience of the image's background, so a quiet room keeps it clean and a busy street adds matching background sound. `image` cannot be combined with `gender` or `age`. See [Instant Voice Clone](/docs/voices/heygen-voice-instant-clone). **Public voices, and character timestamps on completed speech** * [`GET /v3/models/audio/voices`](/reference/list-audio-voices) takes `type`: `private` (default) lists your workspace's voices, and `public` lists HeyGen's public voices. Any workspace can read a public voice with [`GET /v3/models/audio/voices/{voice_id}`](/reference/get-an-audio-voice) and speak it on [`POST /v3/models/audio/tts`](/reference/generate-model-speech) and [`POST /v3/models/audio/tts/stream`](/reference/stream-speech). Public voices can't be deleted. * [`POST /v3/models/audio/tts`](/reference/generate-model-speech) now accepts `with_timestamps`. When `true`, the response adds `original_characters` with the start and end time of each character. See [Character timestamps](/docs/voices/heygen-voice-speech#character-timestamps). **Set pronunciations and forced translations per language** [`POST`](/reference/create-brand-glossary) and [`PATCH /v3/brand-glossaries`](/reference/update-brand-glossary) now accept `pronunciation_language` on `terms` entries and `target_language` on `forced_translations` entries, as `{ "scope": "specific", "code": "fr" }` or `{ "scope": "all" }`. * Omit the field for an all-languages rule. Existing requests and responses are unchanged. * A language-specific rule takes precedence over an all-languages rule for that language. * The same term can appear once per language in each list. * A term in both `do_not_translate_terms` and `forced_translations` stays untranslated. * See [Brand Glossary](/docs/brand-glossary#language-specific-rules). **Faster, cheaper speech for instant voices** [`POST /v3/models/audio/tts`](/reference/generate-model-speech) and [`POST /v3/models/audio/tts/stream`](/reference/stream-speech) now accept `"model": "heygen-voice-1-turbo"`. Turbo speaks instant voices at lower latency; `heygen-voice-1` stays the default. Professional voices speak through `heygen-voice-1`. See [Text to Speech](/docs/voices/heygen-voice-speech). **Instant voices on the HeyGen Voice model** [`POST /v3/models/audio/voices`](/reference/create-or-retrain-an-audio-voice) now takes `"mode": "instant"`: one recording in, a HeyGen Voice `voice_id` out, usually `ACTIVE` within seconds. No training and no voice clone slot. See [Instant Voice Clone](/docs/voices/heygen-voice-instant-clone). * `mode` is now required on every create and retrain call: `instant` or `professional`. * List, get, and delete on `/v3/models/audio/voices` cover both kinds; `mode` tells them apart. * [`POST /v3/models/audio/tts`](/reference/generate-model-speech) and [`POST /v3/models/audio/tts/stream`](/reference/stream-speech) speak instant voices too. `model` defaults to `heygen-voice-1`; `expressiveness_boost` (`0.0`–`1.0`) is instant-only; `seed`, `speed`, `pitch_shift`, `pitch_variance`, and `` pauses stay professional-only. See [Text to Speech](/docs/voices/heygen-voice-speech). * Creating an instant voice is free during the preview. **Render HeyGen Video at 1080p** [`POST /v3/models/videos`](/reference/create-heygen-video) now accepts `resolution: "1080p"` alongside `480p`, `768p` and `2k`. The default stays `768p`. * `1080p` bills at the `2k` rate: 3× the `768p` rate, for output seconds and reference-video seconds alike. * `16:9` returns 1890 × 1080 and `9:16` returns 1080 × 1890. * `text_to_video` and `reference_to_video` render `1080p` in `16:9` or `9:16`, the same as `2k`. Set `aspect_ratio` explicitly for `reference_to_video`, which otherwise defaults to `adaptive`. * `image_to_video` follows the first frame at every resolution. * See [HeyGen Video](/docs/models/heygen-video) for the full parameter reference. **Enterprise workspaces get higher request-rate limits** Request-rate limits now depend on your plan. Pay-As-You-Go and subscription limits are unchanged. Workspaces on an Enterprise API plan get higher limits per user and endpoint: * **Read:** 1,000 per minute (was 300). * **Write:** 30 per second (was 10). * **Heavy:** 120 per minute (was 30). * **Status polling:** 1,500 per minute (was 500), and 1,800 per minute per IP address (was 600). * Enterprise workspaces can ask their account team for custom limits on specific endpoints. * A rate-limit `429` now carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `X-RateLimit-Scope` alongside `Retry-After`. * See [Usage Limits](/docs/usage-limits#rate-limiting) for which endpoints fall in each type and the endpoints with fixed limits. **Render HeyGen Video at 2K** [`POST /v3/models/videos`](/reference/create-heygen-video) now accepts `resolution: "2k"` alongside `480p` and `768p`. The default stays `768p`. * `2k` bills at 3× the `768p` rate, for output seconds and reference-video seconds alike. * `text_to_video` and `reference_to_video` render `2k` in `16:9` or `9:16`. Set `aspect_ratio` explicitly for `reference_to_video`, which otherwise defaults to `adaptive`. * `image_to_video` follows the first frame at every resolution. * See [HeyGen Video](/docs/models/heygen-video) for the full parameter reference. **Turn a video into a template and mark what changes, without opening the editor** New endpoints for creating Studio templates over the API. Make one video, promote it to a template, declare its variables, then generate a video per viewer with the existing [`POST /v3/templates/{template_id}`](/reference/generate-video-from-template). * [`POST /v3/templates`](/reference/create-template-from-video) creates a private template from a video you generated with `POST /v3/videos` or saved in the HeyGen editor. * [`GET /v3/templates/{template_id}`](/reference/get-template) now returns `composition`, every part of each scene with its `id` and `bindable_as`, plus an `edit_version`. * [`PUT /v3/templates/{template_id}/variables`](/reference/set-template-variables) sets the full variable list. Bind avatars, voices, images, video and audio to a part by `element_id`, and turn exact phrases into `{{name}}` placeholders with `match`. Send `expected_edit_version` to get `409` `stale_edit_version` instead of overwriting a concurrent change. * [`PATCH /v3/templates/{template_id}`](/reference/update-template) renames a template and [`DELETE /v3/templates/{template_id}`](/reference/delete-template) removes it. * Character variables take `avatar_id`. `avatar_look_id` and `character_id` are still accepted. * **Deprecations, removed on October 31, 2026:** the `avatar_look_id`, `character_id` and `character_type` names on character variables (send and read `avatar_id`), and the older `scenes` field on `GET /v3/templates/{template_id}` (read `composition.scenes`). `scene_ids` is unaffected. Until then both names are accepted and echoed, and `scenes` keeps its current shape. * See the new [Create Studio Template via API](/create-template) guide. **Generate a short video from a prompt, a first frame, or reference media** The public API reference now includes [`POST /v3/models/videos`](/reference/create-heygen-video) and [`GET /v3/models/videos/{video_id}`](/reference/get-heygen-video) for the `heygen-video-1` model. * Choose `text_to_video`, `image_to_video`, or `reference_to_video` with `mode`. Generate 5–15 second videos at 480p or 768p with a prompt of up to 5,000 characters. * Image-to-video follows the first frame's proportions. Reference-to-video requires at least one image or video and accepts up to nine images, three videos, and three audio recordings, with twelve references total. * Creation returns `202` with a `video_id`. Poll the status endpoint until `completed`, `failed`, or `cancelled`; completed results include a download URL, and unsuccessful results include failure details. * Send an `Idempotency-Key` header when submitting so a retry does not start a second generation. API keys need `videos:write` to create and `videos:read` to poll. **Choose an ElevenLabs model for one speech request** [`POST /v3/voices/speech`](/reference/generate-speech) now accepts `settings.model_id` with `engine: "elevenlabs"`. Supported models include `eleven_v4`, `eleven_v3`, `eleven_multilingual_v2`, `eleven_flash_v2`, `eleven_flash_v2_5`, and `eleven_turbo_v2_5`. * The setting overrides the voice's saved model preference for this request only. Omit `settings` to retain that preference. * The deprecated `elevenlabs_v3` engine retains its existing behavior while callers migrate. New integrations should select the model through `settings.model_id`. **Recognize Assets library folders when reading and placing results** [`GET /v3/folders/{folder_id}`](/reference/get-folder) documents `asset` alongside `mixed`, `video`, and `video_translate` folder types. Existing Assets library folder IDs are also accepted by routes with a `folder_id` destination. * Assets library folders are created in the HeyGen app and are flat: [`POST /v3/folders`](/reference/create-folder) cannot create them or nest another folder inside them. * The HeyGen app displays only assets in Assets library folders, so a video or translation placed there may not appear in that view. **Create folders and file outputs into them over the API** New endpoints: [`POST /v3/folders`](/reference/create-folder) creates a folder, and [`GET /v3/folders/{folder_id}`](/reference/get-folder) returns one so a stored id can be checked before use. This is the v3 successor to `POST /v1/folders/create`. * Send `name` (1 to 256 characters) and an optional `parent_id` to nest the folder inside another one. Leave `parent_id` out to create it at the workspace root. Build a tree one level at a time, passing each new `folder_id` as the next `parent_id`. * `type` is `mixed` (the default, same as the web app's New folder action), `video`, or `video_translate`. Every type accepts videos and translations. * Pass the returned `folder_id` on [`POST /v3/videos`](/reference/create-video), [`POST /v3/video-translations`](/reference/create-video-translation), [`POST /v3/video-translations/proofreads`](/reference/create-proofread-session), [`POST /v3/lipsyncs`](/reference/create-lipsync), or [`POST /v3/templates/{template_id}`](/reference/generate-video-from-template) to place the output in that folder. A `folder_id` outside your workspace is rejected. * Send an `Idempotency-Key` header so a retried create does not leave a duplicate folder behind. **Use explicit pauses with a professional HeyGen voice clone** The text input documentation for [`POST /v3/models/audio/tts`](/reference/generate-model-speech) and [`POST /v3/models/audio/tts/stream`](/reference/stream-speech) now clarifies support for `` with professional voice clones. Pause values are in seconds, with a maximum of five seconds per pause; other SSML tags and break tags for instant voice clones are unsupported. **Revise the scenes you name and keep the rest** [`POST /v3/video-agents/{session_id}`](/reference/send-message-or-request-revision) now accepts an `edit_plan`: one natural-language change per scene, applied together in a single turn. Untouched scenes keep their script and visuals, so a two-scene fix stays a two-scene fix. * Read scene ids and the document's `edit_version` from [`GET /v3/videos/{video_id}/scenes`](/reference/get-video-scenes), then send each item with its `scene_id`, `text`, `scene_snapshot_video_id`, and `edit_version`. `message` is optional when a plan is sent. * Works on sessions created in `generate` or `chat` mode. The response returns the new working draft in `video_id`; poll [`GET /v3/videos/{video_id}`](/reference/get-video) for status. * The whole plan is validated before submission: a scene outside the snapshot returns `400` `invalid_parameter`, and a snapshot that changed since it was read returns `409` `stale_edit_version`. Up to 50 items per request. * See the new [Edit Scenes](/docs/edit-scenes) guide, with a before/after example. **Dress an avatar you already own in a curated set of looks** New endpoint: [`POST /v3/avatars/looks`](/reference/create-avatar-looks) generates new looks for an existing avatar. Pass one look you own as `reference_look_id` and a curated `template_id`; the results join that look's character. Their ids are returned immediately, but video generation must wait for the looks to finish processing. * `type: "look_pack"` applies a Look Pack, a curated set built around one theme, and the pack decides how many looks it produces. Generation is 16:9, and `aspect_ratio: "9:16"` center-crops the delivered frames to portrait. * `type: "template"` restyles the reference look into a single look template, returning two looks in the template's aspect ratio. * Public templates work for every workspace; enterprise custom packs assigned to a workspace work the same way on this endpoint. * The response returns the character as `group_id` and one id per look. Poll [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look) until each reports `completed` or `failed`. * Send an `Idempotency-Key` header so a retry cannot start a second run: a retry while the original is in flight returns `409` `request_in_progress`, and one after it finishes replays the original response. * See the new [Look Packs and Templates](/docs/avatar-look-packs) guide. **One guide per creation path** The Create Avatar guide is now a router into four focused guides: [Video to Avatar](/docs/avatar-from-video), [Photo to Avatar](/docs/avatar-from-photo), [Prompt to Avatar](/docs/avatar-from-prompt), and [Look Packs and Templates](/docs/avatar-look-packs). Each one carries the request, the polling loop, the failure codes, and the render step for that path. [Create Avatar](/docs/create-avatar) keeps the shared response shape and the avatars-versus-looks model. **Inspect your key, set an avatar's default voice, and clean up glossaries and proofread sessions** * [`GET /v3/api_keys/self`](/reference/get-current-api-key) returns the name, `scope_mode`, `scopes`, and expiration of the key making the request. Works with any key. See [API Key](/docs/api-key#restricting-key-permissions). * [`PATCH /v3/avatars/{group_id}`](/reference/update-avatar-group) sets `default_voice_id` on an avatar you own. See [Avatars](/docs/avatars#get-a-single-avatar-group). * [`DELETE /v3/brand-glossaries/{brand_glossary_id}`](/reference/delete-brand-glossary) removes a glossary. See [Brand Glossary](/docs/brand-glossary#delete). * [`GET /v3/video-translations/proofreads`](/reference/list-proofread-sessions) lists proofread sessions and [`DELETE /v3/video-translations/proofreads/{proofread_id}`](/reference/delete-proofread-session) removes one. See [Video Translation Precision](/docs/video-translation-precision#list-and-delete-proofread-sessions). **Run above your included concurrency on demand** Enterprise workspaces can now turn on burst instead of buying a higher permanent concurrency cap. Set an extra cap of up to +50 under **Capacity → Concurrency** in [Usage](https://app.heygen.com/developers/usage). The cap applies to avatar renders and video translations separately, so [`POST /v3/videos`](/reference/create-video) and [`POST /v3/video-translations`](/reference/create-video-translation) each accept requests up to *included + extra* concurrent workflows of their type before returning `429`. * Workflows admitted while every included slot is busy bill at **1.5×** your contract rate; workflows that land in an included slot bill at the normal rate. You only pay the premium for workflows that actually burst. * Burst workflows are tagged **Burst** in the [Activity tab](https://app.heygen.com/developers/usage?tab=activity), and a Burst panel in the Usage sidebar totals the premium for the period. * Burst is shared across the web app and the API. It is enabled per workspace by your account team; see [Burst Concurrency](/docs/burst) and the updated [Usage Limits](/docs/usage-limits#concurrency-limits). **Set do-not-translate terms and forced translations over the API** [`POST /v3/brand-glossaries`](/reference/create-brand-glossary) and [`PATCH /v3/brand-glossaries/{brand_glossary_id}`](/reference/update-brand-glossary) now accept two translation lists alongside `terms`: * `do_not_translate_terms`: `[{ "term": "HeyGen" }]` keeps each term untranslated in every target language. * `forced_translations`: `[{ "term": "CEO", "translation": "Geschäftsführer" }]` replaces each term with its exact translation, inserted verbatim. * Both lists behave like `terms` on `PATCH`: send a list to replace it, an empty list to clear it, omit it to leave it alone. [`GET /v3/brand-glossaries/{brand_glossary_id}`](/reference/get-brand-glossary) returns them whenever a glossary has rules, including glossaries set up in the HeyGen app. * The rules apply whenever the glossary is used by a translation feature: over the API on [video translation](/docs/video-translate), and in the HeyGen app when a Studio or Video Agent draft is translated. Plain video generation and text-to-speech use `terms` alone. Tone settings stay managed in the HeyGen app. * Migrating off `/v1/brand_voice` before the October 31, 2026 sunset: `vocabulary` maps to `terms`, `blacklist` to `do_not_translate_terms`, `whitelist` to `forced_translations`. See the [mapping table](/docs/brand-glossary#migrating-from-v1-brand-voice). **Train a studio-grade voice clone on the HeyGen Voice model** Professional voice cloning is now available on the [HeyGen Voice](/docs/models/heygen-voice) model, HeyGen's in-house voice model. Train a dedicated voice adapter from 1–10 recordings of the same speaker totaling 20+ minutes, then generate speech with it. * `POST /v3/models/audio/voices` creates a voice, or retrains an existing one when `voice_id` is supplied. Poll `GET /v3/models/audio/voices/{voice_id}` until `status` is `ACTIVE`. See [HeyGen Professional Clone](/docs/voices/professional-voice-clone). * `POST /v3/models/audio/tts` returns one completed 44.1 kHz WAV; `POST /v3/models/audio/tts/stream` streams ordered audio parts over Server-Sent Events with optional word timestamps. See [HeyGen Voice Speech](/docs/voices/heygen-voice-speech). * This is a paid feature: each voice occupies a purchased professional voice clone slot, bought on the [Usage page](https://platform.heygen.com/usage). Each slot includes five trainings per monthly billing period, and synthesis bills at 0.6 API credits per generated minute. A voice beyond the workspace's slot limit returns `voice_expired` until a slot is added. * For a clone from a single short recording, HeyGen Instant Clone remains available to all API plans. **Brand the look and the narration in one request** [`POST /v3/video-agents`](/reference/create-video-agent-session) now accepts `brand_glossary_id`, so the narration the agent writes and voices pronounces your terms your way — the same field already accepted by [Studio](/studio-videos), [Templates](/templates), and [Video Translate](/docs/video-translate) requests. * Combines with `brand_kit_id` on the same request, making Video Agent the one endpoint that takes both: the kit shapes what you see, the glossary shapes what you hear. * The glossary is set at session creation and covers the whole session; a follow-up message in an [interactive session](/docs/interactive-sessions) can swap the brand kit but not the glossary. * Pronunciation affects the synthesized audio only — captions and subtitles keep the original spelling. See [Brand Glossary](/docs/brand-glossary) and [On Brand](/docs/on-brand). **Inspect the full composition behind any video id** New endpoint: [`GET /v3/videos/{video_id}/scenes`](/reference/get-video-scenes) returns a video's scene-by-scene composition — what each scene shows and says — together with the video-level context needed to read it (title, aspect ratio, resolution, captions, brand glossary). * Works for any video in your workspace, whichever surface created it: a [studio request](/studio-videos), a [template render](/templates), an [avatar video](/generate-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. * Each scene splits by role: `background` fills the frame, `elements` are placed within it, `script` is the audio over it — synthesized speech (verbatim text, voice id, voice settings), uploaded audio, or a held silence. Walking `scenes[].script[].text` yields the full narration in playback order. * Element types are an open set: `avatar`, `image`, and `video` are described in full, containers carry their children, and other types arrive as placeholders to skip. * See the new [Video Details](/video-details) guide for how this pairs with [`GET /v3/videos/{video_id}`](/reference/get-video) — which now returns the full deliverable set: `gif_url`, `captioned_video_url`, `subtitle_url`, `completed_at`, `folder_id`, `video_page_url`, and failure details alongside the MP4 and thumbnail. **Correct a kit's role assignments, or remove the kit** * [`PATCH /v3/brand-kits/{brand_kit_id}`](/reference/update-brand-kit) sets `name`, `color_roles`, `logo_roles`, and `font_roles`. An omitted field is left unchanged; a role object you send replaces the stored one in full. Role edits need an assembled kit and return `409` while `status` is `loading`. * [`DELETE /v3/brand-kits/{brand_kit_id}`](/reference/delete-brand-kit) removes a kit and the colors, logos, and fonts it holds. Videos already generated keep their branding. * The colors, logos, and font files themselves stay curated in the HeyGen app, where a kit keeps its `brand_kit_id`. See [Brand Kits](/docs/brand-kits). **Import a brand's logos, colors, and fonts over the API** [`POST /v3/brand-kits`](/reference/create-brand-kit) builds a brand kit from a public website URL, so a workspace can go from a domain to on-brand [Video Agent](/docs/video-agent) output without opening the HeyGen app. * The response returns a `brand_kit_id` that is usable immediately with `status: "loading"`. Poll [`GET /v3/brand-kits/{brand_kit_id}`](/reference/get-brand-kit) every 2 to 5 seconds until `status` is `completed` before relying on the imported colors, logos, or fonts. * The detail response adds `color_roles`, `logo_roles`, and `font_roles`, naming which asset plays which part in the brand's visual system. * Send an `Idempotency-Key` header to make retries safe. See [Brand Kits](/docs/brand-kits). **Manage custom pronunciations programmatically** Brand glossaries can now be created and edited over the API, so a per-tenant vocabulary can be provisioned and kept in sync from your own system. * [`POST /v3/brand-glossaries`](/reference/create-brand-glossary) creates a glossary from a `name` and an optional list of `terms`. Names are unique per workspace, compared without regard to case; a duplicate returns `409`. * [`PATCH /v3/brand-glossaries/{brand_glossary_id}`](/reference/update-brand-glossary) replaces `name` or `terms` in full — an omitted field is untouched, and an empty `terms` array clears the list. * Translation rules stay managed in the HeyGen web app and are never modified by an update made here. See [Brand Glossary](/docs/brand-glossary). **Level 2 consent is no longer gated behind a whitelist** Uploading a pre-recorded consent video (Level 2) is now available to every Enterprise account. Previously the flow required whitelisting; that gate has been removed. * Enterprise customers can send `consent_video` on [`POST /v3/avatars/{group_id}/consent`](/reference/create-avatar-consent) without requesting access first. * The webcam flow (Level 1) is unchanged and remains available to all customers. Skip-consent (Level 3) still requires an indemnity agreement — [contact sales](https://www.heygen.com/contact-us/sales). * See [Avatar Consent](/docs/avatar-consent) for the three levels and the required consent statement. **Apply custom pronunciations to direct video-creation requests** [`POST /v3/videos`](/reference/create-video) and [`POST /v3/videos/batches`](/reference/create-video-batch) now accept `brand_glossary_id` for both `"type": "avatar"` and `"type": "image"` payloads. * The glossary applies when HeyGen synthesizes audio from a `script`; caller-supplied `audio_url` and `audio_asset_id` inputs are unchanged. * Custom pronunciations affect generated speech only. Captions and subtitle files retain the script's original spelling. * Discover available IDs with [`GET /v3/brand-glossaries`](/reference/list-brand-glossaries), then inspect individual term mappings with [`GET /v3/brand-glossaries/{brand_glossary_id}`](/reference/get-brand-glossary). **Caption sidecars are generated for every completed job** Caption-generation flags on video translation and lipsync requests are now deprecated and ignored. Captions are always generated; clients choose whether to display or download them. * `enable_caption` is deprecated on `POST /v3/video-translations`, `POST /v3/video-translations/batches`, `POST /v3/lipsyncs`, and `POST /v3/lipsyncs/batches`. * The `captions` flag is likewise deprecated when generating a video from a completed translation proofread. * Completed video-translation responses expose their SRT and VTT caption URLs regardless of the submitted flag. Existing requests remain compatible. **Photo avatars can render without an animation-reference look** When an Avatar V photo avatar's group has no eligible digital-twin or curated reference look, video creation now renders directly from the photo instead of rejecting the request. * The fallback works when `reference_look_id` is omitted and no eligible look can be selected automatically. * `motion_prompt` still requires an animation reference for Avatar V photo avatars. Supply an eligible reference look when you need prompted body motion or hand gestures. **Control how custom terms are pronounced, and read a glossary's terms over the API** A brand glossary maps a term to how it should be spoken, such as `HeyGen` as `hey-jen`. Two additions and one rename cleanup: * **[HeyGen Studio](/studio-videos) accepts `brand_glossary_id`.** Pass it on a `"type": "studio"` request to apply the glossary to every scene whose audio is synthesized from a `script`. Scenes that supply their own `audio_url` or `audio_asset_id` are unaffected. Pronunciations change the generated audio only: captions and the `.srt` sidecar keep the original spelling. * **New endpoint: [`GET /v3/brand-glossaries/{brand_glossary_id}`](/reference/get-brand-glossary).** Returns a glossary with its full term list, so you can confirm exactly which terms are remapped and how. Previously [`GET /v3/brand-glossaries`](/reference/list-brand-glossaries) could tell you a glossary existed but not what was in it. * **`brand_voice_id` is now deprecated in favor of `brand_glossary_id`** across the v3 endpoints that accept it, including [`POST /v3/templates/{template_id}`](/reference/generate-video-from-template), which now accepts both names. Existing requests keep working, since `brand_voice_id` continues to resolve to the same glossary. **Every Video Agent scene is now composed and rendered with Hyperframes** The [Video Agent](/docs/video-agent) now authors every animated stat, chart, caption, and scene transition as a [Hyperframes](/hyperframes-overview) composition — scenes are built in code, not assembled from a fixed template set. In practice, that means the agent can produce any look you can describe: pass a `style_id` for a curated preset, or write a **style paragraph** in your prompt and the agent renders to it literally. * New guide: [Prompting Guide](/docs/prompting-guide) — how to write a prompt that lands on the first cut, including the anatomy of a style paragraph (name, palette, art direction, motion, transitions, vibe). * Updated [Prompt to Video](/docs/video-agent) to document the Hyperframes-powered rendering path. * No API changes required — existing [`POST /v3/video-agents`](/reference/create-video-agent-session) requests automatically benefit. **Narrate over an existing clip and control how it fills the scene** `video` scenes in a [HeyGen Studio](/studio-videos) request now accept an optional voiceover — the same audio inputs a narrated `image` scene takes (`script` + `voice_id`, `audio_url`, or `audio_asset_id`). When present, the voiceover drives the scene's duration instead of the clip's own length. * **Scene-duration alignment:** a new `playback.mode` chooses how the clip fills the voiceover-driven duration — `freeze` (hold the last frame, the default), `loop` (repeat the clip), or `fit_to_scene` (adjust playback speed to end exactly with the voiceover). * The voiceover and the clip's own audio compose: keep using `playback.volume` / `mute` to bring the clip down under narration. * See [Video Scenes](/studio-videos#video-scenes) in the HeyGen Studio guide. **Compose avatar clips, images, and video footage into one video with a single API call** [`POST /v3/videos`](/reference/create-video) accepts a new creation mode, `"type": "studio"` — send an ordered list of whole-frame scenes and get back one rendered MP4. HeyGen owns the layout: every scene is rendered onto one global output canvas, scene durations are derived server-side, and a single `video_id` tracks the all-or-nothing render. * New guide: [HeyGen Studio](/studio-videos) under Video Composition. * **Three scene types**, mixable in one request (1–50 scenes): `avatar_video` (an avatar or lip-synced image speaking a script or audio track, with engine selection, motion prompts, and solid-color backgrounds), `image` (a still held for a set `duration` or narrated), and `video` (an existing clip, with `playback` volume/mute control). * **Global output settings** set once per request: `aspect_ratio`, `resolution`, captions (sidecar `subtitle_url` plus optional burn-in), and webhooks. * **Not available in batches:** a studio request already composes many scenes into one video, so `"type": "studio"` is not accepted by [`POST /v3/videos/batches`](/reference/create-video-batch). Send studio videos one per request. **Every high-volume workflow now has a native batch API** Batch creation, introduced for [videos](/batch-videos), now covers video translations, lipsyncs, and asset uploads. Each accepts up to 100 payloads per call, returns a single `batch_id`, processes items independently (one bad item does not fail the rest), and supports `Idempotency-Key` retries. * New guides under Batches: [Video Translations](/batch-video-translations), [Lipsyncs](/batch-lipsyncs), and [Assets](/batch-assets). * **Translations:** [`POST /v3/video-translations/batches`](/reference/create-video-translation-batch) — a payload targeting multiple `output_languages` expands to one item per language. Replaces the CSV-script workflow previously documented as Bulk Video Translation. * **Lipsyncs:** [`POST /v3/lipsyncs/batches`](/reference/create-lipsync-batch) — payloads identical in shape to `POST /v3/lipsyncs`. * **Assets:** [`POST /v3/assets/direct-uploads/batches`](/reference/create-asset-upload-batch) presigns up to 100 direct-to-S3 slots in one call; finalize with [`POST /v3/assets/complete/batches`](/reference/complete-asset-upload-batch). * **Bulk status checks:** [`GET /v3/video-translations/statuses`](/reference/bulk-video-translation-statuses), [`GET /v3/lipsyncs/statuses`](/reference/bulk-lipsync-statuses), [`GET /v3/assets/statuses`](/reference/bulk-asset-statuses), and [`GET /v3/videos/statuses`](/reference/bulk-video-statuses) — up to 100 ids per request, by id or by batch. **Discover Studio templates and generate videos from them over the API** You can now drive [HeyGen Studio](https://app.heygen.com/avatar/studio) templates programmatically — list them, read each one's variable schema, and render a video by filling the variables. * New guide: [HeyGen Studio Template](/templates) under Video Composition. * **List & inspect:** [`GET /v3/templates`](/reference/list-templates) and [`GET /v3/templates/{template_id}`](/reference/get-template) return each template's variable schema (text, image, video, audio, character, voice) and scenes. * **Generate:** [`POST /v3/templates/{template_id}`](/reference/generate-video-from-template) renders a video from your variable values, with options for `scene_ids`, `dimension`, `fps`, captions/subtitles, sharing, and webhooks. **Submit up to 100 videos in a single request** Queue many videos at once instead of calling [`POST /v3/videos`](/reference/create-video) per video. * New guide: [Videos](/batch-videos) under Batches. * **Create:** [`POST /v3/videos/batches`](/reference/create-video-batch) accepts up to 100 payloads — each identical in shape to `POST /v3/videos` (`avatar`, `image`, or `cinematic_avatar`) — and returns a single `batch_id`. * **Track:** [`GET /v3/videos/batches/{batch_id}`](/reference/get-video-batch) returns aggregate status and per-item `video_id`s; a `callback_url` fires once when the whole batch finishes. **Submit a pre-recorded consent video for a digital twin** [`POST /v3/avatars/{group_id}/consent`](/reference/create-avatar-consent) now accepts an optional `consent_video` field (as a `url`, `asset_id`, or `base64` asset). When supplied, the video is submitted directly for review — the avatar subject skips the hosted webcam page, and the response contains the avatar group without a consent `url`. * Available to Enterprise API accounts with your account whitelisted — please [reach out to sales](https://www.heygen.com/contact-us/sales) or your account team to request access. The same applies to skipping the consent flow entirely. * The webcam flow is unchanged and remains the default — see [Avatar Consent](/docs/avatar-consent) for the full breakdown of consent levels. **Compose your avatar videos into finished, produced videos** You can now combine a HeyGen avatar video with background music, sound effects, and Hyperframes graphics into a single composed render — Hyperframes acts as the compositor that lays an avatar clip into a designed scene, scores it, and renders it to one MP4. * New guide: [Hyperframes + HeyGen](/hyperframes-heygen) — an end-to-end pipeline wiring [`POST /v3/videos`](/reference/create-video), [`GET /v3/audio/sounds`](/background-music) (music and sound effects), and [`POST /v3/hyperframes/renders`](/hyperframes) together. * New page: [Introduction to Hyperframes](/hyperframes-overview) — what the framework is, its core concepts, and the ways to render. * Pass the avatar `video_url` and audio `audio_url`s into the render as `variables`, so one composition bundle produces many finished videos. **Support for the Avatar III engine** We have expanded the capabilities of `POST /v3/videos` to include support for the Avatar III engine. * **`POST /v3/videos`:** * Added support for the `avatar_iii` engine configuration. * Updated the `engine` property to include `AvatarIIIEngineConfig` in the `oneOf` schema definition, allowing for more flexible engine selection during video creation. **Expanded API functionality and improved asset management** We have introduced several new endpoints and configuration options to enhance your workflow: * **Voice Management**: Added `DELETE /v3/voices/{voice_id}` for better voice library control. * **Avatar & Video Configuration**: * `POST /v3/avatars`: Added `avatar_id` as an optional property for more granular prompt-based avatar creation. * `POST /v3/videos`: Added `reference_look_id` support within the `AvatarVEngineConfig` to refine video generation. **Clarified Stock Voice configuration for Video Translation** Updated `POST /v2/video_translate` and `POST /v3/video-translations` to better reflect Enterprise capabilities. * The `stock_voice_config` documentation has been updated to clarify that this feature uses preset HeyGen voices instead of cloning the original speaker's voice. * `preferred_stock_voice_ids` is now explicitly defined as an optional pinning mechanism for stock voices. * Note: Stock voice usage is an Enterprise feature gated by account permissions; please contact your HeyGen account team for access. **`avatar_id` is now the visual reference for prompt avatars** [`POST /v3/avatars`](/reference/create-avatar) with `type: "prompt"` now accepts an optional `avatar_id` — an existing look used as the visual reference for the generation. `avatar_group_id` no longer drives the reference image. * Added `avatar_id` (optional): the look whose image conditions the generation. The new look is saved to the referenced avatar's group; if `avatar_group_id` is also provided, the avatar must belong to that group and the result is saved there. Returns 404 `AVATAR_NOT_FOUND` if the avatar doesn't exist, 400 `INVALID_PARAMETER` if it has no usable image or doesn't belong to the given group. * Changed `avatar_group_id`: now only selects the group the generated avatar is saved to. It no longer conditions the generation on one of the group's looks. * Changed `reference_images`: no longer requires `avatar_group_id` — it can be used on its own. * **Migration Note:** if you passed `avatar_group_id` to keep a character's identity consistent across prompt-generated looks, pass the base look's ID as `avatar_id` instead. Requests with only `avatar_group_id` still succeed, but generate purely from the prompt. * `type: "digital_twin"` and `type: "photo"` are unchanged. See [Prompt to Avatar](/docs/avatar-from-prompt) for the full behavior matrix. **Expanded Audio Search with Sound Effects** The audio search functionality has been updated to include support for sound effects, allowing for more versatile audio retrieval beyond just background music. * Updated `GET /v3/audio/sounds` to include `sound_effects` as a searchable type. * The `type` query parameter now supports `sound_effects` in addition to the default `music`. * API responses for `GET /v3/audio/sounds` will now include items with the `sound_effects` type. * **Migration Note:** Clients that strictly validate the `type` field in the response against an enum list may need to update their schema definitions to include the new `sound_effects` value to prevent parsing errors. **Expanded usage insights for user accounts** We have updated the user profile endpoints to provide better visibility into your account's credit consumption. You can now track your allocation and remaining balance directly through the API. * Added `included_credits` and `remaining_credits` fields to the response objects for: * `GET /v1/user/me` * `GET /v3/users/me` **Updated motion prompt capabilities for video generation** The `motion_prompt` parameter for video generation now supports additional configurations for hand gestures and broader engine compatibility. * Updated `motion_prompt` in `POST /v3/videos`: * Now supports natural-language control for both body motion and hand gestures. * Expanded support for photo avatars across both engines. * Added support for video avatars specifically when using `engine.type: 'avatar_v'`. **Generate cinematic avatar video from a single prompt** [`POST /v3/videos`](/reference/create-video) now supports **Cinematic Avatar** — a prompt-driven video type that composes scene, motion, and framing from a natural-language prompt plus one to three avatar looks (with optional reference media). No script or voice required. * Added the `CreateVideoFromCinematicAvatar` schema to `POST /v3/videos` via an additive request discriminator (`cinematic_avatar`). Existing `CreateVideoFromAvatar` and `CreateVideoFromImage` requests are unaffected — this is a **non-breaking** change. * Billed per video, not by duration. Choose any length from **4 to 15 seconds** via the `duration` parameter. * Supports **720p** and **1080p** output. No migration required. See the [Cinematic Avatar guide](/avatar-shots) for the full parameter list. **Expansion of Asset Management and Audio capabilities** We have introduced new endpoints to streamline asset handling and provide better access to audio resources. * `POST /v3/assets/direct-uploads`: Initialize a direct upload process for your custom assets. * `POST /v3/assets/{asset_id}/complete`: Finalize the upload process for a specific asset. * `GET /v3/audio/sounds`: Retrieve a list of available audio sounds for your projects. **HyperFrames render resolution is now strictly typed** We have updated the `POST /v3/hyperframes/renders` endpoint to enforce strict resolution settings. * **Breaking Change:** The `resolution` property in `POST /v3/hyperframes/renders` is now strictly typed as a string enum (`1080p`, `4k`). Null values are no longer accepted. * The `resolution` property now defaults to `1080p`. * Added optional `aspect_ratio` support for `POST /v3/hyperframes/renders` and response schemas for `GET /v3/hyperframes/renders` and `GET /v3/hyperframes/renders/{render_id}`. **Migration Steps:** Ensure that any hardcoded `resolution` values sent to `POST /v3/hyperframes/renders` match the new `1080p` or `4k` enum strings. If you were previously sending `null` to indicate a default, you can now safely omit the property. **Breaking change: `resolution` is now a tier; `aspect_ratio` is a separate field** `POST /v3/hyperframes/renders` previously took a single `resolution` value that conflated aspect ratio and resolution tier into one of six presets (`landscape`, `landscape-4k`, `portrait`, `portrait-4k`, `square`, `square-4k`). The same flat shape lived on `GET /v3/hyperframes/renders/{render_id}`. Effective immediately, both endpoints decompose that single field into two: * `resolution` ∈ `1080p` | `4k` — output resolution tier. Defaults to `1080p`. * `aspect_ratio` ∈ `16:9` | `9:16` | `1:1` — output aspect ratio. Defaults to `16:9` (landscape). `9:16` is portrait; `1:1` is square. The new shape matches the existing `/v3/videos` endpoint's `resolution` + `aspect_ratio` fields. **Migration table** Map each legacy preset to the new pair: | OLD `resolution` | NEW | | - | - | | `landscape` | `{ "resolution": "1080p", "aspect_ratio": "16:9" }` | | `landscape-4k` | `{ "resolution": "4k", "aspect_ratio": "16:9" }` | | `portrait` | `{ "resolution": "1080p", "aspect_ratio": "9:16" }` | | `portrait-4k` | `{ "resolution": "4k", "aspect_ratio": "9:16" }` | | `square` | `{ "resolution": "1080p", "aspect_ratio": "1:1" }` | | `square-4k` | `{ "resolution": "4k", "aspect_ratio": "1:1" }` | Requests using the legacy preset strings now return a 422 validation error — only the field shape moves. **Not yet supported in this enum surface**: `720p`, `4:5`, `5:4`, and `auto`. These will follow in a separate update once the render pipeline supports the additional values. **CLI** `npx hyperframes cloud render` is updated in lockstep: the `--resolution` flag now accepts `1080p` | `4k`, and a new `--aspect-ratio` flag accepts `16:9` | `9:16` | `1:1`. Legacy values are rejected at the CLI layer with the same migration mapping. **Introducing Brand Glossaries for Video Translation** We have introduced Brand Glossaries to help maintain consistency in your video translations. You can now define custom term mappings to ensure specific terminology is translated accurately according to your brand guidelines. * Added `GET /v3/brand-glossaries` to list and discover your available brand glossaries. * Added `brand_glossary_id` as an optional request parameter to `POST /v2/video_translate` and `POST /v3/video-translations/proofreads`. * The `brand_voice_id` parameter in these endpoints now acts as a legacy alias for `brand_glossary_id`, ensuring backward compatibility for your existing integrations. **Refined API documentation and aspect ratio defaults** We have updated our API documentation across various endpoints to provide clearer guidance on usage and functionality. Additionally, we have updated the default behavior for aspect ratio selection. * Updated `POST /v2/videos` and `POST /v3/videos` to clarify that the `aspect_ratio` defaults to `'16:9'` if not specified. * Documentation descriptions for endpoints across Avatars, Lipsync, Video Agents, and Webhooks have been streamlined for improved clarity. * Updated API tags for `GET /v3/brand-kits`, moving them under the `Brand` category. **Generate videos in square, portrait, landscape, and source-matched ratios** `POST /v2/videos` and [`POST /v3/videos`](/reference/create-video) now accept four additional values for `aspect_ratio`: * `1:1` — square, great for feed posts * `4:5` — portrait, optimized for Instagram and LinkedIn feeds * `5:4` — landscape variant for feed placements * `auto` — detects the source's dimensions (avatar footage or uploaded image) and preserves the original ratio, falling back to `16:9` when the source can't be read Combine these with the existing `16:9` and `9:16` options to target multiple placements from a single integration. **Retrieve metadata for an individual asset** [`GET /v3/assets/{asset_id}`](/reference/get-asset) returns metadata for an asset in your workspace — including owner, upload timestamp, file type, and a publicly accessible URL. Use it to look up uploads on demand instead of paginating through the full asset list. **Record the exact consent language presented to the avatar subject** [`POST /v3/avatars/{group_id}/consent`](/reference/create-avatar-consent) now accepts an optional `consent_text` field. Pass the wording the subject agreed to so you have a clear audit trail alongside the recorded consent. The field is optional — existing integrations continue to work unchanged. **Clearer signal when a user-supplied URL can't be fetched** Requests that include a URL for video, image, audio, or any other resource can now return a dedicated `download_failed` error (HTTP `400`) when the URL can't be downloaded. The `message` field tells you which URL failed and why. Common causes: * URL isn't publicly accessible (auth required, private video, restricted sharing). * URL is malformed or points to a page rather than a direct file. * Remote server refused the connection or returned an error. * Google Drive links must be shared with **Anyone with the link**. * YouTube/Vimeo videos must be **public** — unlisted or private videos aren't supported. The `resource_limit_exceeded` error message also now covers instant-avatar redo attempts and verified avatar group slots, with guidance to wait for limits to reset where applicable. See the [error codes reference](/docs/error-codes) for the full list. **Added Idempotency support and expanded API capabilities** We have introduced support for the `Idempotency-Key` header across key endpoints to ensure safe retries for POST requests. Additionally, several endpoints now return a `409 Conflict` status to handle concurrent requests or state conflicts. * **Idempotency-Key Support:** Added to `POST /v3/assets`, `POST /v3/avatars`, `POST /v3/avatars/{group_id}/consent`, `POST /v3/lipsyncs`, `POST /v3/video-translations`, `POST /v3/video-translations/proofreads`, `POST /v3/video-translations/proofreads/{proofread_id}/generate`, `POST /v3/videos`, `POST /v3/webhooks/endpoints`, and `POST /v3/webhooks/endpoints/{endpoint_id}/rotate-secret`. * **New Endpoints:** Added `DELETE /v3/assets/{asset_id}`, `DELETE /v3/avatars/looks/{look_id}`, and `DELETE /v3/avatars/{group_id}`. * **Default Values:** The `aspect_ratio` parameter now defaults to `16:9` in `POST /v2/videos` and `POST /v3/videos`. **Strict schema validation for frame rate modes** The `fps_mode` property in `POST /v3/video-translations` has been updated to use a strict enum, ensuring more predictable behavior for video output. * **Breaking Change:** The `fps_mode` property is now restricted to an enum. Supported values are now explicitly defined as `vfr`, `cfr`, and `passthrough`. * **Migration:** Ensure your integration passes one of these three strings. Previously provided custom values may now be rejected. **Webhook payload field names now match what's actually delivered** The documented payload for the `avatar_video.success` webhook event has been corrected. If you wired up handlers from the previous documentation, double-check the field names you're reading — the live payload uses these keys: * `video_id`, `url`, `gif_download_url`, `video_page_url`, `video_share_page_url`, `folder_id`, `callback_id` No change to the webhook delivery itself — this aligns the [webhook events reference](/docs/webhook-events) with the payload the API has been sending. **Video engine updates and new brand kit integration** We have updated the video generation workflow and introduced support for Brand Kits across Video Agents. Note that the `engine` parameter structure for `POST /v3/videos` has changed, which is a breaking change for existing integrations. * **Breaking Change:** The `engine` property in `POST /v3/videos` now requires an object structure (e.g., `{"type": "avatar_v"}`) instead of a string. `ApiAvatarEngine` has been removed. * **Video Scaling:** Added a new optional `fit` parameter to `POST /v3/videos` for both `CreateVideoFromAvatar` and `CreateVideoFromImage` request types. * **Brand Kits:** Added `brand_kit_id` as an optional parameter to `POST /v3/video-agents` and `POST /v3/video-agents/{session_id}`. * **New Endpoint:** Added `GET /v3/brand-kits` to retrieve available brand kit resources. * **Parameter Constraints:** Note that `expressiveness` and `motion_prompt` in `POST /v3/videos` are now strictly for Avatar IV and are not supported when `engine.type` is set to `avatar_v`. **New configuration options for Video Translation and Video Generation** We have introduced support for Stock TTS in video translations and added explicit engine selection for avatar-based video generation. * **Video Translation:** Added `stock_voice_config` to `POST /v2/video_translate` and `POST /v3/video-translations`. This allows users to opt into Stock TTS instead of using Voice Cloning. * **Video Generation:** Added an optional `engine` field to `POST /v3/videos` (for `CreateVideoFromAvatar` requests). You can now explicitly select between Avatar IV and Avatar V engines. If omitted, the system defaults to Avatar IV. **Updated schema definitions for Avatar engine support** Metadata for supported API engines has been updated across the avatar look endpoints to ensure consistency when retrieving avatar configurations. * Updated `supported_api_engines` fields in: * `POST /v3/avatars` * `GET /v3/avatars/looks` * `GET /v3/avatars/looks/{look_id}` * `PATCH /v3/avatars/looks/{look_id}` **Fine-tune watermark size, transparency, and position** The `watermark` object on `POST /v3/videos` now accepts three new optional fields for finer control over how your watermark renders: * `scale` (number, `0`–`2`, default `1.0`) — adjust the watermark size relative to its native resolution. * `opacity` (number, `0`–`1`, default `1.0`) — control transparency. * `placement` — choose an anchor corner (`top_left`, `top_right`, `bottom_left`, `bottom_right`) and apply fractional `offset_x` / `offset_y` values for precise positioning. All fields are optional and backward-compatible. Omitting them preserves the existing bottom-right default behavior. **Programmatic voice cloning is now available** You can now create and manage voice clones directly from the API, no dashboard step required. * `POST /v3/voices/clone` initiates a clone from a reference audio sample. * `GET /v3/voices/{voice_id}` returns clone status and details so you can poll until processing completes. * Use the resulting `voice_id` anywhere a voice is accepted (`POST /v3/videos`, `POST /v3/voices/speech`, etc.). See the [voices overview](/docs/voices/overview) for details. **Render captions directly into your videos** `POST /v2/videos` and `POST /v3/videos` now accept a `caption.style` field. Set it to burn captions into the rendered video instead of (or in addition to) consuming the sidecar subtitle file at `subtitle_url`. Useful for social platforms where viewers watch with sound off and you want captions baked into the asset. **Apply your own watermark to generated videos** `POST /v3/videos` accepts a new optional `watermark` property on both `CreateVideoFromAvatar` and `CreateVideoFromImage` requests. Pass a PNG or JPEG image to overlay it onto the rendered output — handy for branding, attribution, or moderation marks. Available as a premium option for select Enterprise customers — [contact support](https://help.heygen.com) to request access. **`watermark` now uses the `WatermarkInput` schema** The inline schema previously used for the `watermark` field on video generation requests has been replaced with the dedicated `WatermarkInput` type. The shape of the property has changed — update existing payloads to match the new schema before upgrading. **Empty `title` queries are no longer accepted** `GET /v2/videos` and `GET /v3/videos` now require at least one character for the `title` query parameter (`minLength` increased from 0 to 1). Omit the parameter entirely if you don't want to filter by title — sending an empty string will return a validation error. **Better error feedback when the voice clone limit is reached** `POST /v3/voices/clone` now returns the `resource_limit_reached` error code (HTTP 400) when your account has hit its voice clone quota, instead of a generic validation error. The response message tells you to delete unused clones or contact support to raise the limit. See the [error codes reference](/docs/error-codes#resource-limit-reached) for handling guidance. **Expanded tool coverage for the HeyGen MCP server** The HeyGen Remote MCP server now includes tools for managing avatars, videos, lipsync, and video translation — making more of the API accessible to AI agents like Claude, Cursor, Gemini CLI, and Manus. * Added avatar management tools: `create_digital_twin`, `create_photo_avatar`, `create_prompt_avatar`, `create_avatar_consent`, `list_avatar_looks`, `get_avatar_look`, `update_avatar_look`, and more. * Added full video CRUD: `create_video_from_avatar`, `create_video_from_image`, `list_videos`, `get_video`, `delete_video`. * Added lipsync and video translation management tools. * Added `design_voice` for finding voices from a natural-language description. * See the [MCP overview](/mcp/overview) for the full tool list. **CLI command surface synced with v3 API** The HeyGen CLI now covers all v3 endpoints, including Video Agent, Lipsync, Video Translation (with Proofreads), Webhooks, and Assets. * Added `--wait` flag for blocking until async operations complete, with configurable `--timeout`. * Added `--request-schema` and `--response-schema` flags to inspect API schemas without authentication. * Added `--force` flag for non-interactive destructive operations in CI. * See the [CLI commands](/commands) and [features](/features) pages for usage details. **v1 and v2 endpoints sunset on October 31, 2026** A formal deprecation timeline is now in place for the v1 and v2 API. Both versions remain fully operational through October 31, 2026, after which they will be retired. * Studio API (multi-scene) and Template API will continue to be supported on v2 until a v3 equivalent is available. * See the [endpoint version comparison](/endpoint-version-comparison) for a full migration checklist and feature comparison. **More granular error responses across the API** New error codes provide clearer feedback when requests fail, making it easier to handle edge cases in your integration. * `ai_vendor_access_restricted` — workspace AI vendor policy blocks the request. * `unlimited_mode_disabled` — avatar doesn't support unlimited mode. * `voice_unavailable` — cloned voice failed processing or expired. * `ephemeral_upload_disabled` — eager upload temporarily disabled for the account. * `gateway_timeout` — external resource could not be fetched in time. * See the full [error codes reference](/docs/error-codes) for details and troubleshooting. **New requirement for Starfish engine compatibility** Text-to-speech generation endpoints now require the use of voices that support the Starfish engine. * Updated `POST /v1/audio/text_to_speech` and `POST /v3/voices/speech` documentation. * Developers should filter for compatible voices by passing `engine=starfish` when calling the voice listing endpoints. **Standardized Asset ID descriptions** Documentation across multiple endpoints has been clarified to consistently refer to asset IDs originating from the HeyGen asset upload endpoint. No functional changes were made to the API behavior. * Applies to request bodies for: * `POST /v3/avatars` * `POST /v3/lipsyncs` * `POST /v3/video-agents` and `POST /v3/video-agents/{session_id}` * `POST /v3/video-translations`, `POST /v3/video-translations/proofreads`, and `PUT /v3/video-translations/proofreads/{proofread_id}/srt` * `POST /v3/videos` **New endpoint for listing Video Agents** We have introduced a new endpoint to allow developers to retrieve a list of all existing video agents associated with their account. * Added `GET /v3/video-agents`: Use this endpoint to fetch your video agents, enabling easier integration and management of your agent instances. **Updated error codes for Avatar endpoints** We have updated the error response codes for avatar-related endpoints to provide more specific feedback when a group cannot be located. * `GET /v3/avatars/{group_id}`: The 404 response error code has been updated from `not_found` to `avatar_group_not_found`. * `POST /v3/avatars/{group_id}/consent`: The 404 response error code has been updated from `not_found` to `avatar_group_not_found`. **Advanced voice customization and output formatting** We have introduced new parameters to provide finer control over generated audio and video output quality. * Added `volume` and `engine_settings` to `voice_settings` for `POST /v2/videos` and `POST /v3/videos`. These settings apply when using text-to-speech (`script` + `voice_id`). * Added `output_format` to `POST /v3/videos` for both `CreateVideoFromAvatar` and `CreateVideoFromImage` request schemas. **Improved error handling for webhook management** We have updated our webhook endpoints to provide more consistent and descriptive error responses. * Added a `409` conflict response to `POST /v3/webhooks/endpoints` to better handle registration errors. * Standardized error codes for `404` responses across `DELETE`, `PATCH`, and `POST /v3/webhooks/endpoints/{endpoint_id}/rotate-secret` by updating the error code to `webhook_not_found`. **Support for custom output formats in video generation** You can now specify a preferred output format when creating videos. The API response now includes the `output_format` field to confirm the format used for your generated video. * Added optional `output_format` request property to `POST /v2/videos`. * Added `output_format` to the response body of `POST /v2/videos` (200 OK). * Added `output_format` to the response body of `POST /v3/videos` (200 OK). **Comprehensive API Documentation Updates** We have updated the endpoint descriptions across our entire V3 API to provide clearer guidance, better parameter context, and more precise functionality definitions. While the underlying API logic remains consistent, the improved documentation clarifies how to integrate with our latest engine versions and features. * **Video Generation**: `POST /v3/videos` now officially documents support for the Avatar IV engine and upcoming Avatar V. * **Avatars**: Clarified workflows for `POST /v3/avatars` (asynchronous training) and added guidance on the mandatory consent flow for private avatars via `POST /v3/avatars/{group_id}/consent`. * **Video Agent**: Streamlined descriptions for session-based interactions, clearly distinguishing between `generate` (one-shot) and `chat` (multi-turn) modes. * **Lipsync & Translation**: Updated documentation for `POST /v3/lipsyncs` and `POST /v3/video-translations` to emphasize the `speed` vs. `precision` mode selection for output quality. * **Webhooks**: Clarified that `PATCH /v3/webhooks/endpoints/{endpoint_id}` performs a full replacement of the event types array. * **Assets**: Updated supported MIME types for `POST /v3/assets` to include refined file type lists. **Added caption\_url to Lipsync and Video Translation responses** You can now retrieve the `caption_url` for generated lipsyncs and video translations, providing direct access to the generated caption files. * `GET /v3/lipsyncs` and `GET /v3/lipsyncs/{lipsync_id}` * `PATCH /v3/lipsyncs/{lipsync_id}` * `GET /v3/video-translations` and `GET /v3/video-translations/{video_translation_id}` * `PATCH /v3/video-translations/{video_translation_id}` **Updated documentation for avatar consent** Clarified the implementation details for the avatar consent flow to ensure a smoother user experience. * `POST /v3/avatars/{group_id}/consent`: Updated documentation to clarify that the returned URL must be presented directly to the user in a browser to complete the consent process. **Support for avatar-default voices** You can now generate videos using an avatar's default voice without explicitly specifying a `voice_id`. When creating a video, if `voice_id` is omitted while `avatar_id` is present, the system will automatically use the avatar's default voice. * Updated `POST /v3/videos`: The `voice_id` requirement has been relaxed for both `CreateVideoFromAvatar` and `CreateVideoFromImage` schemas, allowing the system to fall back to the avatar's default voice. **Enhanced capabilities for Video Agent interactions** We have updated the description and scope of the `POST /v3/video-agents/{session_id}` endpoint to better reflect its versatility in managing agent-led workflows. * Updated the endpoint description to clarify support for answering agent-posed questions and requesting specific edits or revisions. * The request body schema has been updated to better align with these extended conversational and editing capabilities. **New 'thinking' status for Video Agents** We have introduced a new `thinking` state to the Video Agent response object to provide better visibility into agent processing workflows. * Updated `POST /v3/video-agents` * The `status` field in the response now includes the `thinking` enum value. * Integration note: Ensure your client-side parsers are prepared to handle this new status value in the response body. **Updated Video Agent session retrieval and new video listing** We have refactored how resource data is handled in Video Agent sessions to improve performance. Additionally, we have introduced a new endpoint to fetch videos associated with a session. * **Breaking Change:** The `resources` property has been removed from the response body of `GET /v3/video-agents/{session_id}`. * **Migration:** To access resource details previously found in the session object, please use the new `GET /v3/video-agents/{session_id}/resources/{resource_id}` endpoint. * **New Endpoint:** Added `GET /v3/video-agents/{session_id}/videos` to retrieve a list of videos generated within a specific agent session. **Breaking change: Restructured Video Agent session management** We have updated the Video Agent API to simplify session handling. Please note that the previous `/v3/video-agents/sessions` path structure is deprecated and removed. * **Removed endpoints:** `POST /v3/video-agents/sessions`, `GET /v3/video-agents/sessions/{session_id}`, `POST /v3/video-agents/sessions/{session_id}/messages`, `GET /v3/video-agents/sessions/{session_id}/resources`, and `POST /v3/video-agents/sessions/{session_id}/stop` have been removed. * **Migration:** Replace existing calls with the new flattened endpoints under `/v3/video-agents/{session_id}`. * **New endpoints added:** * `GET /v3/video-agents/{session_id}` * `POST /v3/video-agents/{session_id}` * `GET /v3/video-agents/{session_id}/resources/{resource_id}` * `POST /v3/video-agents/{session_id}/stop` **New configuration options for Video Agent sessions** The `POST /v3/video-agents` endpoint now supports advanced control over session flow. * Added `mode`: Supports `generate` (default, one-shot) and `chat` (multi-turn, allows revisions and follow-ups). * Added `auto_proceed`: Enables automated progression through storyboards. * Added `skip_agentic_stop`: Provides granular control over agent stopping behavior. **API Operation ID update** The operation ID for `GET /v3/users/me` has been updated from `getUserMeV3` to `getCurrentUserV3` to maintain consistency across our SDKs. **Added support for custom voice creation** We have introduced a new endpoint to allow developers to programmatically create and add new voices to their HeyGen account. * Added `POST /v3/voices` to the API. **Refactored POST /v3/videos request body** We have updated the `POST /v3/videos` endpoint to use a discriminated union for improved type safety and flexibility. This change replaces the legacy flat request structure with dedicated schemas for creating videos from avatars versus images. * **Breaking Change:** The request body structure has been completely overhauled. You must now specify a type discriminator: use `CreateVideoFromAvatar` for digital twins/avatars or `CreateVideoFromImage` for custom image animation. * **Migration:** All properties previously passed at the top level of the request (e.g., `avatar_id`, `image_url`, `voice_id`, `script`) must now be nested within the appropriate schema based on the video source. * The operation ID for this endpoint has been updated from `createAvatarVideoV3` to `createVideo`. **Enhanced error messaging across all endpoints** We have updated the error response schemas and examples across the entire API suite. Developers can now expect more consistent and detailed error responses for common issues, including: * Improved `400 Bad Request` messages with clearer parameter validation feedback. * Standardized `401 Unauthorized` responses when API keys are missing or expired. * Consistent `429 Rate Limited` responses that align with standard retry headers. * Better descriptive error messages for resource-specific failures (e.g., `404 Not Found` for specific IDs). These updates ensure that your integrations can better handle exceptions and debugging. **HeyGen for Developers — New v3 API Surface** We've launched a new set of v3 endpoints across the HeyGen API, bringing a consistent interface, cursor-based pagination, and a unified asset input model to all major resources. What's new: * All v3 endpoints share a standard error format, cursor-based pagination (`has_more` / `next_token`), and consistent authentication via `X-Api-Key` or OAuth bearer token. * Asset inputs now use a type-discriminated union — pass files as `{ "type": "url", "url": "..." }`, `{ "type": "asset_id", "asset_id": "..." }`, or `{ "type": "base64", "media_type": "...", "data": "..." }` across all endpoints. * New and updated endpoints include: Video Agent (`POST /v3/video-agents`), Videos (`POST /v3/videos`), Voices (`GET /v3/voices`, `POST /v3/voices/speech`), Video Translations (`POST /v3/video-translations`), Overdub (`POST /v3/overdubs`), Avatars (`POST /v3/avatars`), Assets (`POST /v3/assets`), Webhooks (`/v3/webhooks/*`), and User (`GET /v3/users/me`). The v1/v2 endpoints continue to work, but we recommend migrating to v3 for all new integrations. # Cinematic Avatar Source: https://heygen-1fa696a7.mintlify.app/cinematic-avatar Generate cinematic avatar video from a single prompt with the HeyGen API. Combine up to three avatar looks with reference videos and images — no script or voice required. Cinematic Avatar is a prompt-driven variant of [`POST /v3/videos`](/reference/create-video). Instead of a script and a voice, you describe the shot you want in natural language and hand HeyGen up to three avatar looks (plus optional reference media). The Seedance pipeline composes the scene, motion, and framing for you. ## Prerequisites One to three avatar look IDs. Use `GET /v3/avatars/looks` to browse your looks and copy the `id` field for each one you want in the shot. A prompt describing the scene, action, and framing. This replaces the `script` + `voice_id` you'd use for a [Digital Twin video](/generate-avatar-video). ## Step 1 — Write your prompt The `prompt` (1–10,000 characters) is the creative brief for the shot. Describe what the avatar is doing, the setting, the camera, and the mood — e.g. *"A founder walks through a sunlit startup office, gesturing toward a whiteboard, shot handheld in a documentary style."* See [Writing Effective Video Prompts](/writing-effective-video-prompts) for guidance. ## Step 2 — Pick your avatar looks Pass `avatar_id` as an **array** of 1–3 look IDs. Multiple looks let HeyGen feature more than one avatar in the same shot: ```bash theme={null} curl -X GET "https://api.heygen.com/v3/avatars/looks?ownership=private" \ -H "x-api-key: YOUR_API_KEY" ``` Copy the `id` of each look you want into the array. ## Step 3 — Create the video Send a `POST` to `/v3/videos` with `type: "cinematic_avatar"`: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "cinematic_avatar", "prompt": "A founder walks through a sunlit startup office, gesturing toward a whiteboard, shot handheld in a documentary style.", "avatar_id": ["YOUR_LOOK_ID"], "aspect_ratio": "16:9", "resolution": "1080p", "duration": 10, "title": "Founder office walkthrough" }' ``` The response is the same shape as any other `/v3/videos` job: ```json theme={null} { "data": { "video_id": "abc123", "status": "pending", "output_format": "mp4" } } ``` ### Adding reference media Use `references` to steer style, motion, or composition with your own videos and images. Each reference is a `url`, `asset_id`, or `base64` asset input: ```json theme={null} { "type": "cinematic_avatar", "prompt": "Match the lighting and camera movement of the reference clip.", "avatar_id": ["YOUR_LOOK_ID"], "references": [ { "type": "url", "url": "https://your-cdn.com/reference-clip.mp4" }, { "type": "asset_id", "asset_id": "YOUR_UPLOADED_ASSET_ID" } ] } ``` Avatar looks and references share a combined media budget: **at most 3 videos and 9 images** total across `avatar_id` and `references`. Upload your own files first with [Assets](/assets) to get an `asset_id`. ## Step 4 — Poll for completion Generation is asynchronous. Poll [`GET /v3/videos/{video_id}`](/reference/create-video) until `status` is `completed`: ```bash theme={null} curl -X GET "https://api.heygen.com/v3/videos/YOUR_VIDEO_ID" \ -H "x-api-key: YOUR_API_KEY" ``` | Status | Meaning | | - | - | | `pending` | Queued for processing | | `processing` | Video is being generated | | `completed` | Ready — `video_url` is available | | `failed` | Something went wrong — check `failure_message` | ## Full example ```python theme={null} import requests import time API_KEY = "YOUR_API_KEY" BASE = "https://api.heygen.com" HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"} # 1. Create the Cinematic Avatar video resp = requests.post(f"{BASE}/v3/videos", headers=HEADERS, json={ "type": "cinematic_avatar", "prompt": "A founder walks through a sunlit startup office, gesturing toward a whiteboard, shot handheld in a documentary style.", "avatar_id": ["YOUR_LOOK_ID"], "aspect_ratio": "16:9", "resolution": "1080p", "duration": 10, }) video_id = resp.json()["data"]["video_id"] print(f"Video created: {video_id}") # 2. Poll until done while True: status_resp = requests.get(f"{BASE}/v3/videos/{video_id}", headers=HEADERS) data = status_resp.json()["data"] print(f"Status: {data['status']}") if data["status"] == "completed": print(f"Download: {data['video_url']}") break elif data["status"] == "failed": print(f"Error: {data.get('failure_message')}") break time.sleep(10) ``` ## Parameters | Parameter | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Must be `"cinematic_avatar"` | | `prompt` | string | Yes | 1–10,000 characters describing the shot | | `avatar_id` | array | Yes | 1–3 avatar look IDs | | `references` | array | No | Up to 3 videos / 9 images (shared with `avatar_id`) as `url`, `asset_id`, or `base64` inputs | | `aspect_ratio` | string | No | `16:9` (default), `9:16`, or `1:1` | | `resolution` | string | No | `720p` (default) or `1080p` | | `duration` | integer | No | 4–15 seconds, default `10`. Omit when `auto_duration` is `true` | | `auto_duration` | boolean | No | Let HeyGen pick the duration. Default `false` | | `enhance_prompt` | boolean | No | Auto-expand a short prompt into a richer description. Default `false` | | `title` | string | No | Display name in the HeyGen dashboard | ## Using webhooks instead of polling Pass a `callback_url` when creating the video and HeyGen will POST to it when the job finishes — register an endpoint via `POST /v3/webhooks/endpoints` and subscribe to `avatar_video.success` and `avatar_video.fail` as covered in [Webhooks](/docs/webhooks). # Overview Source: https://heygen-1fa696a7.mintlify.app/cli Generate AI avatar videos from your terminal with the HeyGen CLI. Authenticate once, then script video creation, translation, and lipsync into any local. The HeyGen CLI gives developers and AI agents command-line access to HeyGen's video platform. It wraps the v3 API, outputs structured JSON by default, and works out of the box in scripts, CI pipelines, and agent workflows. ## 1. Install the CLI ```bash theme={null} curl -fsSL https://static.heygen.ai/cli/install.sh | bash ``` This installs the latest stable release into `~/.local/bin`. Verify the installation: ```bash theme={null} heygen --version ``` The CLI ships as a single binary with no runtime prerequisites. macOS (Apple Silicon and Intel) and Linux (x64 and arm64) are supported. Windows support is coming soon — WSL is recommended in the meantime. ## 2. Authenticate Log in with your API key from [API keys page](https://platform.heygen.com/api-keys): ```bash theme={null} heygen auth login ``` Paste your API key when prompted. The key is stored locally at `~/.heygen/credentials`. For CI/Docker/agent environments, set the environment variable instead — it takes precedence over stored credentials: ```bash theme={null} export HEYGEN_API_KEY=your-api-key ``` Verify your credentials: ```bash theme={null} heygen auth status ``` ### Log in with OAuth As an alternative to an API key, log in with your HeyGen account via OAuth: ```bash theme={null} heygen auth login --oauth ``` This opens your browser to sign in at HeyGen.com. Logging in one way replaces the other stored credential. Non-interactive shells (piped input, `CI=true`, or `HEYGEN_NONINTERACTIVE=1`) default to the API-key flow — pass `--oauth` explicitly in automation. Set `BROWSER=none` or `HEYGEN_NO_BROWSER=1` to print the sign-in URL instead of opening a browser. ## 3. Create a Video Send a prompt to the Video Agent and let it handle avatar, voice, and layout: ```bash theme={null} heygen video-agent create --prompt "A presenter explaining our product launch in 30 seconds" ``` ```json Output theme={null} { "data": { "session_id": "sess_abc123", "status": "generating", "video_id": "vid_xyz789", "created_at": 1711288320 } } ``` The CLI returns immediately with structured JSON. Your video is generating in the background. For full control over every parameter, use `video create` with a JSON body: ```bash theme={null} heygen video create -d '{ "type": "avatar", "avatar_id": "avt_angela_01", "script": "Welcome to our Q4 earnings call.", "voice_id": "1bd001e7e50f421d891986aad5e3e5d2" }' ``` Use `--request-schema` on any command to discover the expected JSON fields — no auth required: ```bash theme={null} heygen video create --request-schema heygen video-agent create --request-schema ``` ## 4. Check Status Poll for the result using the `video_id` returned from step 3: ```bash theme={null} heygen video get vid_xyz789 ``` ```json Output theme={null} { "data": { "id": "vid_xyz789", "title": "Product launch explainer", "status": "completed", "video_url": "https://files.heygen.com/video/vid_xyz789.mp4", "thumbnail_url": "https://files.heygen.com/thumb/vid_xyz789.jpg", "duration": 32.5, "created_at": 1711288320, "completed_at": 1711288452 } } ``` Status moves through `pending` → `processing` → `completed` or `failed`. If the video fails, the response includes `failure_code` and `failure_message` fields. **Tip:** Add `--wait` to the create command to block until the video is ready instead of polling manually. The default timeout is 20 minutes — override with `--timeout 30m`. On timeout, the CLI exits with code `4` and prints the last known resource state along with a hint to resume polling manually. ## 5. Download the Video Once complete, download to a local file: ```bash theme={null} heygen video download vid_xyz789 --output-path ./launch-video.mp4 ``` ```json Output theme={null} { "asset": "video", "message": "Downloaded video to ./launch-video.mp4", "path": "./launch-video.mp4" } ``` If the video was created with captions enabled, you can download the captioned version: ```bash theme={null} heygen video download vid_xyz789 --asset captioned --output-path ./launch-captioned.mp4 ``` # Commands Source: https://heygen-1fa696a7.mintlify.app/commands Reference every HeyGen CLI command with flags, examples, and expected output. Covers video, avatar, voice, translate, lipsync, and config commands in one place. All commands follow the pattern `heygen `. The command surface is auto-generated from HeyGen's OpenAPI specification — when new v3 endpoints ship, the CLI picks them up automatically. Run `heygen --help` for detailed usage and examples on any command. Use `--request-schema` or `--response-schema` on any command to see the full JSON schema for its request or response — no auth required. ## Ai Clipping Turn long-form videos into ready-to-share short clips with captions | Command | API Endpoint | Description | | - | - | - | | `heygen ai-clipping create` | `POST /v3/ai-clipping` | Create AI Clipping | | `heygen ai-clipping delete ` | `DELETE /v3/ai-clipping/{job_id}` | Delete AI Clipping | | `heygen ai-clipping get ` | `GET /v3/ai-clipping/{job_id}` | Get AI Clipping | | `heygen ai-clipping list` | `GET /v3/ai-clipping` | List AI Clipping | ### Flags for `ai-clipping create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--callback-id ` | Opaque client identifier echoed verbatim in webhook payloads. Mirrors /v3/video-translations callback\_id. | | `--callback-url ` | HTTPS URL to receive per-job webhook callbacks. Mirrors /v3/video-translations callback\_url. Per-job callback\_url deliveries are NOT HMAC-signed: authenticate them by verifying TLS and matching the echoed callback\_id, and do not trust an unverified body. To receive a signed payload, register a webhook endpoint with a secret (the signature header is sent only to registered endpoints). | | `--input-language ` | ISO-639-1 source language code (e.g. 'en', 'es'). Omit for auto-detect. | | `--title ` | Title for the job. Defaults to the source video's title if omitted. | ### Flags for `ai-clipping list` | Flag | Description | | - | - | | `--limit ` | Maximum number of items per page. Defaults to 10 (lower than other v3 lists) because each item embeds its full clips array. | | `--token ` | Opaque cursor token for the next page. | ## Asset Upload files for use in video creation | Command | API Endpoint | Description | | - | - | - | | `heygen asset batches get ` | `GET /v3/assets/batches/{batch_id}` | Get Asset Batch | | `heygen asset complete batches create` | `POST /v3/assets/complete/batches` | Complete Asset Upload Batch | | `heygen asset complete create ` | `POST /v3/assets/{asset_id}/complete` | Complete Asset Upload | | `heygen asset create` | `POST /v3/assets` | Upload Asset | | `heygen asset delete ` | `DELETE /v3/assets/{asset_id}` | Delete Asset | | `heygen asset direct-uploads batches create` | `POST /v3/assets/direct-uploads/batches` | Create Asset Upload Batch | | `heygen asset direct-uploads create` | `POST /v3/assets/direct-uploads` | Create Asset Upload | | `heygen asset get ` | `GET /v3/assets/{asset_id}` | Get Asset | | `heygen asset list` | `GET /v3/assets` | List Assets | | `heygen asset statuses list` | `GET /v3/assets/statuses` | Bulk Asset Statuses | ### Flags for `asset batches get` | Flag | Description | | - | - | | `--limit ` | Items per page (1-100). | | `--token ` | Opaque pagination cursor from a previous response. | ### Flags for `asset complete batches create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--batch-id ` | Identifier returned by POST /v3/assets/direct-uploads/batches. (required) | ### Flags for `asset complete create` | Flag | Description | | - | - | | `--checksum-sha-256 ` | Optional SHA256 (hex) cross-check. | ### Flags for `asset create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--file ` | File to upload (image, video, audio, PDF, or SRT subtitle). Max 32 MB. (required) | ### Flags for `asset direct-uploads batches create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--callback-url ` | Reserved. Asset completion does not emit a webhook. | | `--title ` | Display name for the batch, shown in the HeyGen app. | ### Flags for `asset direct-uploads create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--checksum-sha-256 ` | Optional SHA256 of the file as hex. When provided, S3 enforces it on upload. | | `--content-type ` | Declared MIME type (e.g. 'video/mp4', 'image/png', 'audio/mpeg', 'application/pdf', 'application/zip'). Verified against the stored bytes at completion. (required) | | `--filename ` | Original filename for reference/metadata. The stored object's extension is derived from content\_type. (required) | | `--size-bytes ` | Exact byte size of the file. Signed into the upload URL so it cannot be exceeded. (required) | ### Flags for `asset list` | Flag | Description | | - | - | | `--username ` | Username of the workspace member whose assets to list — the same value as asset items' 'owner' field. Required while this endpoint is in beta; it will become an optional filter in a future release. (required) | | `--limit ` | Maximum number of assets to return per page (1-100). | | `--token ` | Opaque cursor from a previous response's next\_token. Omit for the first page. | | `--folder-id ` | Optional folder filter. Omit to list ALL workspace assets across folders. Pass a folder id to list that folder only, or an empty value (folder\_id=) for root-level assets (assets not filed into any folder). | ### Flags for `asset statuses list` | Flag | Description | | - | - | | `--asset-ids ` | Comma-separated asset ids to look up. | | `--batch-ids ` | Comma-separated batch ids; each expands to its member assets. | ## Audio Search the background-music and sound-effects catalog | Command | API Endpoint | Description | | - | - | - | | `heygen audio sounds list` | `GET /v3/audio/sounds` | Search audio (music or sound effects) | ### Flags for `audio sounds list` | Flag | Description | | - | - | | `--query ` | Natural-language description of the audio you want, e.g. 'upbeat lofi hip-hop' or 'tense cinematic riser'. Results are ranked by semantic similarity to this text. (required) | | `--type ` | Audio content type to search: 'music' (background-music catalog) or 'sound\_effects' (SFX catalog). Defaults to 'music'. | | `--limit ` | Maximum number of results to return (1-50). | | `--min-score ` | Minimum semantic similarity score (0-1). Tracks scoring below this are omitted. | | `--token ` | Opaque cursor token for the next page, taken from 'next\_token' in a prior response. | ## Avatar List and manage avatars and looks | Command | API Endpoint | Description | | - | - | - | | `heygen avatar consent create ` | `POST /v3/avatars/{group_id}/consent` | Create Avatar Consent | | `heygen avatar create` | `POST /v3/avatars` | Create Avatar | | `heygen avatar delete ` | `DELETE /v3/avatars/{group_id}` | Delete Avatar Group | | `heygen avatar get ` | `GET /v3/avatars/{group_id}` | Get Avatar Group | | `heygen avatar list` | `GET /v3/avatars` | List Avatar Groups | | `heygen avatar look-templates list` | `GET /v3/avatars/look-templates` | List Look Templates | | `heygen avatar looks create` | `POST /v3/avatars/looks` | Create Avatar Looks | | `heygen avatar looks delete ` | `DELETE /v3/avatars/looks/{look_id}` | Delete Avatar Look | | `heygen avatar looks get ` | `GET /v3/avatars/looks/{look_id}` | Get Avatar Look | | `heygen avatar looks list` | `GET /v3/avatars/looks` | List Avatar Looks | | `heygen avatar looks update ` | `PATCH /v3/avatars/looks/{look_id}` | Update Avatar Look | | `heygen avatar update ` | `PATCH /v3/avatars/{group_id}` | Update Avatar Group | ### Flags for `avatar consent create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--reroute-url ` | Callback URL where the user is redirected after completing consent. Defaults to HeyGen's consent completion page. | ### Flags for `avatar create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | ### Flags for `avatar list` | Flag | Description | | - | - | | `--ownership ` | Filter by ownership: 'public' for preset avatars, or 'private' for your own. Omit for all. | | `--limit ` | Maximum number of items to return per page (1-50). | | `--token ` | Opaque cursor token for the next page. | ### Flags for `avatar look-templates list` | Flag | Description | | - | - | | `--type ` | Filter by generation method: 'look\_pack' for Look Packs, 'template' for single look templates. Omit for both. | | `--gender ` | Filter to templates built for 'male' or 'female'. Templates with no gender of their own are returned for either value. Omit for all. | | `--limit ` | Maximum number of items to return per page (1-50). | | `--token ` | Opaque cursor token for the next page. | ### Flags for `avatar looks create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | ### Flags for `avatar looks list` | Flag | Description | | - | - | | `--group-id ` | Filter looks to a specific avatar group. Returns only looks belonging to this group. | | `--avatar-type ` | Filter by avatar type: 'studio\_avatar', 'digital\_twin', or 'photo\_avatar'. | | `--ownership ` | Filter by ownership: 'public' for preset avatars, or 'private' for your own. Omit for all. | | `--limit ` | Maximum number of items to return per page (1-50). | | `--token ` | Opaque cursor token for the next page. | ### Flags for `avatar looks update` | Flag | Description | | - | - | | `--name ` | New display name for the look. | ### Flags for `avatar update` | Flag | Description | | - | - | | `--default-voice-id ` | Voice ID to set as the avatar's default voice, replacing the current one. The voice is also linked to the avatar group if it was not already. Accepts any voice available to your workspace, including imported voice clones. Only supported for avatars you own. | ## Background Removal | Command | API Endpoint | Description | | - | - | - | | `heygen background-removal create` | `POST /v3/background-removals` | Create Background Removal | | `heygen background-removal delete ` | `DELETE /v3/background-removals/{job_id}` | Delete Background Removal | | `heygen background-removal get ` | `GET /v3/background-removals/{job_id}` | Get Background Removal | | `heygen background-removal list` | `GET /v3/background-removals` | List Background Removals | ### Flags for `background-removal create` | Flag | Description | | - | - | | `--layers ` | Output layers to return. Any of: 'foreground' (subject on a transparent background), 'mask' (grayscale alpha matte), 'background' (the scene with the subject removed). Defaults to all three. | | `--request-id ` | Client-provided idempotency key — *the* idempotency mechanism for this endpoint. Re-sending the same `request_id` returns the *original* job (same `id`, same `status`, same charge) instead of creating a new one — the dedup is over (`space_id`, `request_id`), not over the response body, so a duplicate `request_id` with different content still collides on the original job. The HTTP `Idempotency-Key` header is *not* honored here; pick a per-content `request_id` if you need per-content dedup. | | `--title ` | Optional human-readable title for the job. | ### Flags for `background-removal list` | Flag | Description | | - | - | | `--limit ` | Maximum number of jobs to return (1-100). | | `--token ` | Opaque pagination cursor from a previous response's next\_token. | ## Brand Brand-related resources — brand kits (colors, fonts, logos) and brand glossaries (custom term translations) | Command | API Endpoint | Description | | - | - | - | | `heygen brand glossaries create` | `POST /v3/brand-glossaries` | Create Brand Glossary | | `heygen brand glossaries delete ` | `DELETE /v3/brand-glossaries/{brand_glossary_id}` | Delete Brand Glossary | | `heygen brand glossaries get ` | `GET /v3/brand-glossaries/{brand_glossary_id}` | Get Brand Glossary | | `heygen brand glossaries list` | `GET /v3/brand-glossaries` | List Brand Glossaries | | `heygen brand glossaries update ` | `PATCH /v3/brand-glossaries/{brand_glossary_id}` | Update Brand Glossary | | `heygen brand kits create` | `POST /v3/brand-kits` | Create Brand Kit | | `heygen brand kits delete ` | `DELETE /v3/brand-kits/{brand_kit_id}` | Delete Brand Kit | | `heygen brand kits get ` | `GET /v3/brand-kits/{brand_kit_id}` | Get Brand Kit | | `heygen brand kits list` | `GET /v3/brand-kits` | List Brand Kits | | `heygen brand kits update ` | `PATCH /v3/brand-kits/{brand_kit_id}` | Update Brand Kit | ### Flags for `brand glossaries create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--name ` | Display name for the glossary. Surrounding whitespace is removed, and the trimmed name must be 1-64 characters and unique within your workspace, compared without regard to case. (required) | ### Flags for `brand glossaries list` | Flag | Description | | - | - | | `--limit ` | Maximum number of brand glossaries to return (1-100). Default 10. | | `--token ` | Opaque pagination cursor from a previous response's `next_token`. Omit for the first page. | ### Flags for `brand glossaries update` | Flag | Description | | - | - | | `--name ` | New display name. Surrounding whitespace is removed, and the trimmed name must be 1-64 characters and unique within your workspace, compared without regard to case. Omit to keep the current name. | ### Flags for `brand kits create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--name ` | Name for the brand kit, up to 256 characters. Defaults to the brand name detected on the site, or its domain. | | `--url ` | Public website URL to build the brand kit from. HeyGen visits the site and imports the brand assets it finds there, including logos, colors and font files. By calling this endpoint you confirm you have the rights and licenses necessary to upload, store and use those assets in HeyGen. (required) | ### Flags for `brand kits list` | Flag | Description | | - | - | | `--limit ` | Results per page (1-100). | | `--token ` | Opaque cursor token for the next page. Obtained from next\_token in a previous response. | ### Flags for `brand kits update` | Flag | Description | | - | - | | `--name ` | New display name for the brand kit, 1-256 characters. Leading and trailing whitespace is trimmed. | ## Filler Word Removal | Command | API Endpoint | Description | | - | - | - | | `heygen filler-word-removal create` | `POST /v3/filler-word-removals` | Create Filler Word Removal | | `heygen filler-word-removal get ` | `GET /v3/filler-word-removals/{filler_word_removal_id}` | Get Filler Word Removal | ### Flags for `filler-word-removal create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--callback-id ` | Opaque client identifier echoed verbatim in webhook payloads. | | `--callback-url ` | HTTPS URL to receive per-job webhook callbacks (filler\_word\_removal.success / filler\_word\_removal.fail). Delivery is best-effort with limited retries — poll GET /v3/filler-word-removals/\{id} as the source of truth (a terminal GET may precede its webhook). Per-job callback\_url deliveries are NOT HMAC-signed: authenticate them by verifying TLS and matching the echoed callback\_id, and do not trust an unverified body. To receive a signed payload, register a webhook endpoint with a secret via /v3/webhooks/endpoints. | | `--title ` | Display title for the job. Defaults to 'Filler Removal' if omitted. | ## Folder Create folders to organize videos and translations in the workspace, and look up any folder, Assets library folders included, by id | Command | API Endpoint | Description | | - | - | - | | `heygen folder create` | `POST /v3/folders` | Create Folder | | `heygen folder get ` | `GET /v3/folders/{folder_id}` | Get Folder | ### Flags for `folder create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--name ` | Folder name, 1-256 characters. Sibling folders may share a name. (required) | | `--parent-id ` | ID of the folder to create this one in. Omit, pass null, or pass an empty string to create it at the workspace root. The parent must be a folder in the caller's workspace, of a kind this endpoint creates, that is not in the trash. Assets library folders are flat and cannot hold folders. | | `--type ` | Kind of folder. 'mixed' is what the HeyGen web app's New folder action creates; 'video\_translate' is what the Video Translate page creates. Folders the Assets library creates have kind 'asset'; they cannot be created or nested under here. Every kind is accepted as a folder\_id destination on POST /v3/videos, POST /v3/video-translations and the other routes that take one, but the HeyGen app files only assets in an 'asset' folder, so a render placed in one may not appear in the Assets library view. | ## Lipsync Dub or replace audio on existing videos | Command | API Endpoint | Description | | - | - | - | | `heygen lipsync batches create` | `POST /v3/lipsyncs/batches` | Create Lipsync Batch | | `heygen lipsync batches get ` | `GET /v3/lipsyncs/batches/{batch_id}` | Get Lipsync Batch | | `heygen lipsync create` | `POST /v3/lipsyncs` | Create Lipsync | | `heygen lipsync delete ` | `DELETE /v3/lipsyncs/{lipsync_id}` | Delete Lipsync | | `heygen lipsync get ` | `GET /v3/lipsyncs/{lipsync_id}` | Get Lipsync | | `heygen lipsync list` | `GET /v3/lipsyncs` | List Lipsyncs | | `heygen lipsync statuses list` | `GET /v3/lipsyncs/statuses` | Bulk Lipsync Statuses | | `heygen lipsync update ` | `PATCH /v3/lipsyncs/{lipsync_id}` | Update Lipsync | ### Flags for `lipsync batches create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--callback-url ` | Webhook URL invoked once when every item in the batch reaches a terminal state. | | `--title ` | Display name for the batch, shown in the HeyGen app. | ### Flags for `lipsync batches get` | Flag | Description | | - | - | | `--limit ` | Items per page (1-100). | | `--token ` | Opaque pagination cursor from a previous response. | ### Flags for `lipsync create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--callback-id ` | ID included in webhook payload | | `--callback-url ` | Webhook URL for completion notifications | | `--disable-music-track` | Remove background music | | `--enable-caption` | Deprecated and ignored: captions are always generated; whether to display them is a download-side choice. | | `--enable-dynamic-duration` | Allow dynamic duration adjustment | | `--enable-speech-enhancement` | Enhance speech quality | | `--enable-watermark` | Add watermark to output | | `--end-time ` | End time in seconds for partial lipsync | | `--folder-id ` | Destination folder ID in the caller's workspace, for example one returned by POST /v3/folders. Omit, pass null, or pass an empty string to place the result at the workspace root. The id must name a folder that is not in the trash and that the caller can write to. Assets library folders are accepted, but the HeyGen app files only assets in them, so a result placed there may not appear in the Assets library view. An unknown id, a folder in another workspace, a project of another kind such as a brand kit, or a trashed folder is rejected with 404 before anything is created. | | `--fps-mode ` | Frame rate mode: 'vfr', 'cfr', or 'passthrough'. | | `--keep-the-same-format` | Preserve the source video's encoding specs (resolution, bitrate). | | `--mode ` | Quality mode: 'speed' (faster) or 'precision' (higher quality, uses avatar inference) | | `--start-time ` | Start time in seconds for partial lipsync | | `--title ` | Title for the lipsync job | ### Flags for `lipsync list` | Flag | Description | | - | - | | `--limit ` | Maximum number of items per page | | `--token ` | Opaque cursor token for the next page | ### Flags for `lipsync statuses list` | Flag | Description | | - | - | | `--lipsync-ids ` | Comma-separated lipsync ids to look up. | | `--batch-ids ` | Comma-separated batch ids; each expands to its member lipsyncs. | ### Flags for `lipsync update` | Flag | Description | | - | - | | `--title ` | New title for the lipsync (required) | ## Model | Command | API Endpoint | Description | | - | - | - | | `heygen model audio tts create` | `POST /v3/models/audio/tts` | Generate Speech | | `heygen model audio voices create` | `POST /v3/models/audio/voices` | Create or Retrain an Audio Voice | | `heygen model audio voices delete ` | `DELETE /v3/models/audio/voices/{voice_id}` | Delete an Audio Voice | | `heygen model audio voices get ` | `GET /v3/models/audio/voices/{voice_id}` | Get an Audio Voice | | `heygen model audio voices list` | `GET /v3/models/audio/voices` | List Audio Voices | | `heygen model images create` | `POST /v3/models/images` | Create HeyGen Image | | `heygen model images get ` | `GET /v3/models/images/{generation_id}` | Get HeyGen Image | | `heygen model videos create` | `POST /v3/models/videos` | Create HeyGen Video | | `heygen model videos get ` | `GET /v3/models/videos/{video_id}` | Get HeyGen Video | ### Flags for `model audio tts create` | Flag | Description | | - | - | | `--expressiveness-boost ` | Expressiveness boost for instant voices on HeyGen Voice 1.0, from 0.0 to 1.0. Omit for professional voices. | | `--language ` | Language code of `text`. Regional tags normalize to their supported base language, such as `en-US` to `en`. Omit it to detect the language from `text`. A confident reading that matches the letters of `text` is used, and the request fails with `invalid_parameter` if HeyGen Voice does not speak that language. Otherwise the first of these that HeyGen Voice speaks is used: the language the voice was created in, if `text` is written in that language's script; a less certain reading that a second reading of `text` without its numbers and brand names agrees with; the usual language of the script `text` is mostly written in, such as English for Latin letters; and, only when that script has no usual language, the reading itself. If none of them applies, the request fails with `invalid_parameter`. The `Content-Language` response header names the language used. | | `--model ` | heygen-voice-1: instant and professional voices. heygen-voice-1-turbo: heygen-voice-1 instant voices only, at lower latency. | | `--pitch-shift ` | Pitch shift in semitones for professional clones. Ignored for instant clones. | | `--pitch-variance ` | Pitch variation multiplier for professional clones; 1.0 preserves the voice default. Ignored for instant clones. | | `--seed ` | Optional best-effort deterministic generation seed for professional clones. Ignored for instant clones. | | `--speed ` | Speech speed multiplier for professional clones. Ignored for instant clones, including Turbo. | | `--text ` | Plain text to synthesize. A professional voice clone takes \ pauses (seconds, up to 5 per pause, e.g. \); other SSML, and break tags for an instant voice clone, are not supported. (required) | | `--voice-id ` | Identifier of the voice used for speech synthesis. (required) | | `--with-timestamps` | Whether to include `original_characters` in the response: the start and end time of each character of `text`. | ### Flags for `model audio voices create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | ### Flags for `model audio voices list` | Flag | Description | | - | - | | `--type ` | `private` lists the voices your workspace created. `public` lists the voices HeyGen provides to every workspace. | | `--limit ` | Maximum number of voices to return per page. | | `--token ` | Opaque pagination token returned by a previous request. | ### Flags for `model images create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--aspect-ratio ` | Output aspect ratio. Omit to match the reference image after EXIF orientation. Explicit presets are exact; inferred ratios round to the nearest pixel. Output resolution follows the model's canvas policy; read width and height from each result. | | `--mode ` | Generation operation. Only identity\_remix is supported. Defaults to identity\_remix when omitted. | | `--model ` | Required identity-focused image model. Only heygen-image-1 is supported. (required) | | `--num-images ` | Number of images to generate in this job. Supports one to five. | | `--seed ` | Seed for the first image. Each subsequent image increments the seed by one. A random seed is chosen when omitted. | ### Flags for `model videos create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | ## Template Generate videos from reusable templates by replacing their variables | Command | API Endpoint | Description | | - | - | - | | `heygen template create` | `POST /v3/templates` | Create Template from Video | | `heygen template delete ` | `DELETE /v3/templates/{template_id}` | Delete Template | | `heygen template generate ` | `POST /v3/templates/{template_id}` | Generate Video from Template | | `heygen template get ` | `GET /v3/templates/{template_id}` | Get Template | | `heygen template list` | `GET /v3/templates` | List Templates | | `heygen template update ` | `PATCH /v3/templates/{template_id}` | Update Template | | `heygen template variables update ` | `PUT /v3/templates/{template_id}/variables` | Set Template Variables | ### Flags for `template create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--name ` | Template name. Defaults to the source video's title. | | `--video-id ` | ID of a video in your workspace to turn into a template. The video must have been generated with POST /v3/videos or saved in the HeyGen editor; its draft becomes the template's starting point. (required) | ### Flags for `template generate` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--brand-glossary-id ` | Brand glossary controlling how custom terms are pronounced in generated speech. Discover IDs via GET /v3/brand-glossaries. | | `--brand-voice-id ` | Legacy field name for `brand_glossary_id`. Both are accepted and resolve to the same workspace record. Prefer `brand_glossary_id`. | | `--callback-id ` | Opaque ID echoed back in webhook events for this video | | `--callback-url ` | URL called with the video result in addition to registered webhook endpoints | | `--caption` | Whether to burn captions into the video | | `--enable-sharing` | Whether the generated video's share page is publicly accessible | | `--folder-id ` | Destination folder ID in the caller's workspace, for example one returned by POST /v3/folders. Omit, pass null, or pass an empty string to place the result at the workspace root. The id must name a folder that is not in the trash and that the caller can write to. Assets library folders are accepted, but the HeyGen app files only assets in them, so a result placed there may not appear in the Assets library view. An unknown id, a folder in another workspace, a project of another kind such as a brand kit, or a trashed folder is rejected with 404 before anything is created. Supported only for single-video creation; batch video items cannot set their own destination folder. | | `--fps ` | Output frame rate. One of 25, 30, or 60. | | `--include-gif` | Whether to include a GIF preview in the webhook payload | | `--keep-text-vertically-centered` | When true, replaced text elements are vertically re-centered based on their rendered height | | `--reorder-music` | When true (default), background audio tracks move with their scenes. When false, tracks stay pinned to layout positions. | | `--scene-ids ` | Scene IDs to render, in order (repeats allowed). Scenes must already exist in the template; the API can select, reorder, and repeat scenes but cannot create new ones. Omit to render all scenes in template order. | | `--title ` | Title for the generated video | ### Flags for `template list` | Flag | Description | | - | - | | `--limit ` | Maximum number of templates to return per page | | `--token ` | Opaque pagination token from a previous response's next\_token | ### Flags for `template update` | Flag | Description | | - | - | | `--name ` | New template name (required) | ### Flags for `template variables update` | Flag | Description | | - | - | | `--expected-edit-version ` | The template's edit\_version from your last GET. When given, the update is rejected with 409 stale\_edit\_version if the template changed since. Omit to overwrite unconditionally. | ## User Account information and billing | Command | API Endpoint | Description | | - | - | - | | `heygen user me get` | `GET /v3/users/me` | Get Current User | ## Video Create, list, retrieve, and delete videos | Command | API Endpoint | Description | | - | - | - | | `heygen video batches create` | `POST /v3/videos/batches` | Create Video Batch | | `heygen video batches get ` | `GET /v3/videos/batches/{batch_id}` | Get Video Batch | | `heygen video create` | `POST /v3/videos` | Create Video | | `heygen video delete ` | `DELETE /v3/videos/{video_id}` | Delete Video | | `heygen video get ` | `GET /v3/videos/{video_id}` | Get Video | | `heygen video list` | `GET /v3/videos` | List Videos | | `heygen video scenes get ` | `GET /v3/videos/{video_id}/scenes` | Get Video Scenes | | `heygen video statuses list` | `GET /v3/videos/statuses` | Bulk Video Statuses | ### Flags for `video batches create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--callback-url ` | Webhook URL invoked once when every item in the batch reaches a terminal state. | | `--folder-id ` | Destination folder ID for the batch container. The videos remain grouped inside the newly created batch; the batch itself is placed in this folder. Omit, pass null, or pass an empty string to place the batch at the workspace root. | | `--title ` | Display name for the batch, shown in the HeyGen app. | ### Flags for `video batches get` | Flag | Description | | - | - | | `--limit ` | Items per page (1-100). | | `--token ` | Opaque pagination cursor from a previous response. | ### Flags for `video create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | ### Flags for `video list` | Flag | Description | | - | - | | `--limit ` | Maximum number of items to return per page | | `--token ` | Opaque pagination cursor from a previous response | | `--folder-id ` | Filter videos by folder ID | | `--title ` | Filter videos by title substring | ### Flags for `video statuses list` | Flag | Description | | - | - | | `--video-ids ` | Comma-separated video ids to look up. | | `--batch-ids ` | Comma-separated batch ids; each expands to its member videos. | ## Video Agent Create videos from text prompts using AI | Command | API Endpoint | Description | | - | - | - | | `heygen video-agent create` | `POST /v3/video-agents` | Create Video Agent Session | | `heygen video-agent get ` | `GET /v3/video-agents/{session_id}` | Get Video Agent Session | | `heygen video-agent list` | `GET /v3/video-agents` | List Video Agent Sessions | | `heygen video-agent resources get ` | `GET /v3/video-agents/{session_id}/resources/{resource_id}` | Get Session Resource | | `heygen video-agent send ` | `POST /v3/video-agents/{session_id}` | Send Video Agent Message | | `heygen video-agent stop ` | `POST /v3/video-agents/{session_id}/stop` | Stop Video Agent Session | | `heygen video-agent styles list` | `GET /v3/video-agents/styles` | List Video Agent Styles | | `heygen video-agent videos list ` | `GET /v3/video-agents/{session_id}/videos` | List Session Videos | ### Flags for `video-agent create` | Flag | Description | | - | - | | `--avatar-id ` | Specific avatar ID to use | | `--brand-glossary-id ` | Brand glossary ID controlling how custom terms are pronounced in the generated video's narration (for example, saying 'HeyGen' as 'hey-jen'). Pronunciation is applied to the synthesized audio only, so caption and subtitle text still show the original script wording. Discover IDs via GET /v3/brand-glossaries. | | `--brand-kit-id ` | Brand kit ID to apply brand colors, fonts, and logos to the generated video. In enterprise workspaces with a locked brand policy, only the workspace default brand kit is accepted; omit this field to apply the workspace default automatically. | | `--callback-id ` | Optional callback ID included in webhook payload | | `--callback-url ` | Webhook URL for completion/failure notifications | | `--incognito-mode` | When enabled, disables memory injection and extraction for this session | | `--members ` | Email addresses to grant access to. Valid only with `visibility: selected`, and every address must belong to a member of the caller's workspace (max 100). May be omitted or empty, leaving the session owner-only until members are added. | | `--mode ` | Session mode. 'generate' produces one video (fire-and-forget). 'chat' enables multi-turn interaction — the agent may pause for decisions and accept follow-up messages. | | `--orientation ` | Video orientation. If not provided, auto-detected from content. | | `--prompt ` | The message/prompt for video generation (1-10000 characters) (required) | | `--style-id ` | Style ID from GET /v3/video-agents/styles. Applies a curated visual template to the generated video. | | `--visibility ` | Who can open the session. `private`: the owner only. `space`: everyone in the workspace can open and drive the session. `selected`: the owner plus the workspace members listed in `members`. Defaults to `space`. Applies to the session and its conversation; does not change existing sessions. | | `--voice-id ` | Specific voice ID to use for narration | ### Flags for `video-agent list` | Flag | Description | | - | - | | `--limit ` | Results per page (1-100). | | `--token ` | Opaque cursor token for the next page. Obtained from next\_token in a previous response. | ### Flags for `video-agent send` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--avatar-id ` | Override avatar for this message | | `--brand-kit-id ` | Brand kit ID to apply for this message. In enterprise workspaces with a locked brand policy, only the workspace default brand kit is accepted. | | `--message ` | Text message to the agent. Required when edit\_plan is omitted; may be omitted or empty when edit\_plan is provided. | | `--voice-id ` | Override voice for this message | ### Flags for `video-agent stop` This command takes a structured request body. Pass it with `-d` and run the command with `--request-schema` to see all fields. ### Flags for `video-agent styles list` | Flag | Description | | - | - | | `--tag ` | Filter by tag (e.g., 'cinematic', 'retro-tech', 'iconic-artist', 'pop-culture', 'handmade', 'print'). | | `--limit ` | Results per page (1-100). | | `--token ` | Opaque cursor token for the next page. Obtained from next\_token in a previous response. | ## Video Translate Translate videos into other languages | Command | API Endpoint | Description | | - | - | - | | `heygen video-translate batches create` | `POST /v3/video-translations/batches` | Create Video Translation Batch | | `heygen video-translate batches get ` | `GET /v3/video-translations/batches/{batch_id}` | Get Video Translation Batch | | `heygen video-translate create` | `POST /v3/video-translations` | Create Video Translation | | `heygen video-translate delete ` | `DELETE /v3/video-translations/{video_translation_id}` | Delete Video Translation | | `heygen video-translate get ` | `GET /v3/video-translations/{video_translation_id}` | Get Video Translation | | `heygen video-translate languages list` | `GET /v3/video-translations/languages` | List Supported Translation Languages | | `heygen video-translate list` | `GET /v3/video-translations` | List Video Translations | | `heygen video-translate proofreads create` | `POST /v3/video-translations/proofreads` | Create Proofread Session | | `heygen video-translate proofreads delete ` | `DELETE /v3/video-translations/proofreads/{proofread_id}` | Delete Proofread Session | | `heygen video-translate proofreads generate ` | `POST /v3/video-translations/proofreads/{proofread_id}/generate` | Generate Video from Proofread | | `heygen video-translate proofreads get ` | `GET /v3/video-translations/proofreads/{proofread_id}` | Get Proofread Session | | `heygen video-translate proofreads list` | `GET /v3/video-translations/proofreads` | List Proofread Sessions | | `heygen video-translate proofreads srt get ` | `GET /v3/video-translations/proofreads/{proofread_id}/srt` | Download Proofread SRT | | `heygen video-translate proofreads srt update ` | `PUT /v3/video-translations/proofreads/{proofread_id}/srt` | Upload Proofread SRT | | `heygen video-translate statuses list` | `GET /v3/video-translations/statuses` | Bulk Video Translation Statuses | | `heygen video-translate update ` | `PATCH /v3/video-translations/{video_translation_id}` | Update Video Translation | ### Flags for `video-translate batches create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--callback-url ` | Webhook URL invoked once when every item in the batch reaches a terminal state. | | `--title ` | Display name for the batch, shown in the HeyGen app. | ### Flags for `video-translate batches get` | Flag | Description | | - | - | | `--limit ` | Items per page (1-100). | | `--token ` | Opaque pagination cursor from a previous response. | ### Flags for `video-translate create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--brand-glossary-id ` | Brand glossary ID for custom term translations (e.g. translate 'Reformer' as the Pilates equipment, not 'political activist'). Alias for the legacy `brand_voice_id` field. Discover IDs via GET /v3/brand-glossaries. | | `--brand-voice-id ` | Brand glossary ID for custom term translations. Legacy field name for `brand_glossary_id` — both are accepted and resolve to the same workspace record. Discover IDs via GET /v3/brand-glossaries. | | `--callback-id ` | ID included in webhook payload | | `--callback-url ` | Webhook URL for completion notifications | | `--disable-music-track` | Remove background music | | `--enable-caption` | Deprecated and ignored: captions are always generated; whether to display them is a download-side choice. | | `--enable-dynamic-duration` | Allow dynamic duration adjustment | | `--enable-speech-enhancement` | Enhance speech quality | | `--enable-watermark` | Add watermark to output | | `--end-time ` | End time in seconds for partial translation | | `--folder-id ` | Destination folder ID in the caller's workspace, for example one returned by POST /v3/folders. Omit, pass null, or pass an empty string to place the result at the workspace root. The id must name a folder that is not in the trash and that the caller can write to. Assets library folders are accepted, but the HeyGen app files only assets in them, so a result placed there may not appear in the Assets library view. An unknown id, a folder in another workspace, a project of another kind such as a brand kit, or a trashed folder is rejected with 404 before anything is created. | | `--fps-mode ` | Frame rate mode for the output video. 'vfr' = variable frame rate, 'cfr' = constant frame rate, 'passthrough' = match the source. Only takes effect when a custom 'audio' track is provided. | | `--input-language ` | Source language code (auto-detected if omitted) | | `--keep-the-same-format` | Preserve the source video's encoding specs (resolution, bitrate). | | `--mode ` | Translation quality mode: 'speed' (faster) or 'precision' (higher quality, uses avatar inference) | | `--output-languages ` | Target language names (e.g. 'Chinese (Cantonese, Traditional)', 'Spanish (Spain)', 'English'). Use GET /v3/video-translations/languages for valid values. Use one for single translation, multiple for batch. (required) | | `--speaker-num ` | Number of speakers (improves speaker separation) | | `--srt-role ` | Which video the subtitle applies to: 'input' (source) or 'output' (translated). | | `--start-time ` | Start time in seconds for partial translation | | `--title ` | Title for the translation job | | `--translate-audio-only` | Only translate audio, keep original video | ### Flags for `video-translate list` | Flag | Description | | - | - | | `--limit ` | Maximum number of items per page | | `--token ` | Opaque cursor token for the next page | ### Flags for `video-translate proofreads create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--brand-glossary-id ` | Brand glossary ID for custom term translations (e.g. translate 'Reformer' as 'Pilates equipment', not 'political activist'). Alias for the legacy `brand_voice_id` field. Discover IDs via GET /v3/brand-glossaries. | | `--brand-voice-id ` | Brand glossary ID for custom term translations. Legacy field name for `brand_glossary_id` — both are accepted and resolve to the same workspace record. Discover IDs via GET /v3/brand-glossaries. | | `--disable-music-track` | Remove background music | | `--enable-speech-enhancement` | Enhance speech quality | | `--enable-video-stretching` | Allow dynamic duration adjustment | | `--folder-id ` | Destination folder ID in the caller's workspace, for example one returned by POST /v3/folders. Omit, pass null, or pass an empty string to place the result at the workspace root. The id must name a folder that is not in the trash and that the caller can write to. Assets library folders are accepted, but the HeyGen app files only assets in them, so a result placed there may not appear in the Assets library view. An unknown id, a folder in another workspace, a project of another kind such as a brand kit, or a trashed folder is rejected with 404 before anything is created. | | `--keep-the-same-format` | Preserve the source video's encoding specs (resolution, bitrate) | | `--mode ` | Translation quality mode: 'speed' (faster) or 'precision' (higher quality) | | `--output-languages ` | Target language codes. Use one for single proofread, multiple for batch. (required) | | `--speaker-num ` | Number of speakers (improves speaker separation) | | `--title ` | Title for the proofread job (required) | ### Flags for `video-translate proofreads generate` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--callback-id ` | ID included in webhook payload | | `--callback-url ` | Webhook URL for completion notifications | | `--captions` | Deprecated and ignored: captions are always generated; whether to display them is a download-side choice. | | `--translate-audio-only` | Only translate audio, keep original video | ### Flags for `video-translate proofreads list` | Flag | Description | | - | - | | `--limit ` | Maximum number of items per page | | `--token ` | Opaque cursor token for the next page | ### Flags for `video-translate proofreads srt update` This command takes a structured request body. Pass it with `-d` and run the command with `--request-schema` to see all fields. ### Flags for `video-translate statuses list` | Flag | Description | | - | - | | `--video-translation-ids ` | Comma-separated video translation ids to look up. | | `--batch-ids ` | Comma-separated batch ids; each expands to its member video translations. | ### Flags for `video-translate update` | Flag | Description | | - | - | | `--title ` | New title for the video translation (required) | ## Voice Create speech audio and manage voices | Command | API Endpoint | Description | | - | - | - | | `heygen voice clone create` | `POST /v3/voices/clone` | Clone a Voice | | `heygen voice create` | `POST /v3/voices` | Design a Voice | | `heygen voice delete ` | `DELETE /v3/voices/{voice_id}` | Delete a Voice | | `heygen voice get ` | `GET /v3/voices/{voice_id}` | Get Voice | | `heygen voice list` | `GET /v3/voices` | List Voices | | `heygen voice speech create` | `POST /v3/voices/speech` | Generate Speech | ### Flags for `voice clone create` | Flag | Description | | - | - | | `--language ` | Language hint for the voice (e.g., 'en', 'es'). Auto-detected if omitted. | | `--remove-background-noise` | Remove background noise from the audio before cloning. | | `--voice-name ` | Display name for the cloned voice. Up to 100 characters; names longer than 64 characters are stored truncated to 64. (required) | ### Flags for `voice create` | Flag | Description | | - | - | | `--gender ` | Filter by gender: 'male' or 'female'. | | `--locale ` | BCP-47 locale tag to filter by (e.g., 'en-US', 'pt-BR'). | | `--prompt ` | Natural language description of the desired voice (e.g., 'warm, confident female narrator'). (required) | | `--seed ` | Controls which batch of results to return. seed=0 returns the top matches, seed=1 the next batch, etc. Same prompt + seed always returns the same voices. | ### Flags for `voice list` | Flag | Description | | - | - | | `--type ` | Voice type: 'public' for the shared library or 'private' for your cloned voices. | | `--engine ` | Filter by stored engine mapping. Use 'heygen\_voice' for HeyGen Voice; 'orca' remains accepted as its legacy alias. Also accepts 'starfish', 'elevenlabs' or internal engine names. Response engine values retain 'orca' for compatibility. The deprecated elevenlabs\_v3 input alias is also accepted. Available engines may also include engines prepared on first use. | | `--language ` | Filter by language (e.g. 'English'). | | `--gender ` | Filter by gender ('male' or 'female'). | | `--limit ` | Results per page (1-100). | | `--token ` | Opaque cursor token for the next page. | ### Flags for `voice speech create` | Flag | Description | | - | - | | `--engine ` | Speech engine override for this request only. Omit to use the voice's saved default, including your saved preference. Voices without a concrete saved default retain Starfish. Unsupported defaults or overrides return an error; engines are never silently substituted. Use 'heygen\_voice' for HeyGen Voice; 'orca' remains accepted as its legacy alias. Responses retain 'orca' for compatibility. ElevenLabs model variants are selected through settings.model\_id. The deprecated elevenlabs\_v3 engine retains its existing behavior while callers and saved defaults migrate. | | `--force-regenerate` | Bypass HeyGen's speech cache and request fresh synthesis. Normal speech quota applies. Defaults to false. | | `--input-type ` | Type of the input: 'text' for plain text, 'ssml' for SSML markup. Defaults to 'text'. | | `--language ` | Base language code (e.g. 'en', 'pt', 'zh'). Optional — auto-detected from text when omitted. | | `--locale ` | Locale tag for Starfish (e.g. 'zh-HK'). ElevenLabs does not support locale selection; use language instead. | | `--speed ` | Speed multiplier (0.5-2.0). | | `--text ` | Text to synthesize (1-5000 characters). Break tags must express time in seconds (for example, \); millisecond values are not supported. (required) | | `--voice-id ` | Voice ID from the voice catalog. Professional voice clones use model speech generation instead. (required) | ## Webhook Create, list, and manage webhook endpoints and events | Command | API Endpoint | Description | | - | - | - | | `heygen webhook endpoints create` | `POST /v3/webhooks/endpoints` | Create Webhook Endpoint | | `heygen webhook endpoints delete ` | `DELETE /v3/webhooks/endpoints/{endpoint_id}` | Delete Webhook Endpoint | | `heygen webhook endpoints list` | `GET /v3/webhooks/endpoints` | List Webhook Endpoints | | `heygen webhook endpoints rotate-secret ` | `POST /v3/webhooks/endpoints/{endpoint_id}/rotate-secret` | Rotate Webhook Signing Secret | | `heygen webhook endpoints update ` | `PATCH /v3/webhooks/endpoints/{endpoint_id}` | Update Webhook Endpoint | | `heygen webhook event-types list` | `GET /v3/webhooks/event-types` | List Webhook Event Types | | `heygen webhook events list` | `GET /v3/webhooks/events` | List Webhook Events | ### Flags for `webhook endpoints create` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | | `--entity-id ` | Optional entity ID to scope this endpoint to a specific resource (e.g. a personalized video project). | | `--events ` | Event types to subscribe to. Omit or set to null to receive all events. | | `--url ` | Publicly accessible HTTPS URL that will receive webhook POST requests. (required) | ### Flags for `webhook endpoints list` | Flag | Description | | - | - | | `--limit ` | Maximum number of endpoints to return (1-100). Default: 10. | | `--token ` | Opaque pagination cursor from a previous response's next\_token. | ### Flags for `webhook endpoints rotate-secret` | Flag | Description | | - | - | | `--idempotency-key ` | Optional client-supplied key for safely retrying mutations. Subsequent calls within 24 hours that share this key replay the original response — even if the request body differs slightly (a warning is logged). A retry that arrives while the original is still in flight gets a 409 `request_in_progress`. Keys must be 1–255 characters from `[A-Za-z0-9_:.-]`; a UUID is a safe default. Scope is per-endpoint and per-resource: the same key on a different route or path parameter is independent. Example: 550e8400-e29b-41d4-a716-446655440000 | ### Flags for `webhook endpoints update` | Flag | Description | | - | - | | `--events ` | New list of event types to subscribe to. Replaces the existing list. | | `--url ` | New URL for the endpoint. Must be publicly accessible HTTPS. | ### Flags for `webhook events list` | Flag | Description | | - | - | | `--event-type ` | Filter events by type, e.g. 'avatar\_video.success'. | | `--entity-id ` | Filter events by entity ID. | | `--limit ` | Maximum number of events to return (1-100). Default: 10. | | `--token ` | Opaque pagination cursor from a previous response's next\_token. | ## Authentication | Command | Description | | - | - | | `heygen auth login` | Authenticate interactively (prompts for API key) | | `heygen auth login --oauth` | Log in via browser OAuth — uses subscription credits ([free usage](/docs/for-ai-agents#free-usage)) | | `heygen auth status` | Verify stored credentials and show account info | For CI/Docker, use the `HEYGEN_API_KEY` environment variable instead. It takes precedence over stored credentials. ## Utility Commands | Command | Description | | - | - | | `heygen config set ` | Set a persistent config value | | `heygen config get ` | Read a config value | | `heygen config list` | Show all config values and their sources | | `heygen update` | Self-update to the latest version | | `heygen update --version ` | Update to a specific version (e.g. `v0.1.0`) | ### Config keys | Key | Values | Description | | - | - | - | | `output` | `json`, `human` | Default output format (default: `json`) | | `analytics` | `true`, `false` | Enable or disable anonymous usage analytics | # Create Studio Template via API Source: https://heygen-1fa696a7.mintlify.app/create-template Create a template from a video you generated with the API or saved in the HeyGen editor, then mark which parts change. The API version of "Create template" and the editor's variable panel, so the whole template loop runs from code.
Create Studio templates **via the API**: turn a video into a reusable template and mark which parts change, without opening the HeyGen editor. Make one good video, promote it to a template, declare its variables, then generate a personalized version per agent, listing or customer. ## What this adds You could already **use** templates over the API: list them, read them, and generate videos from them. Creating a template and marking its variables needed the HeyGen editor. The authoring endpoints add that first half, so the whole loop runs from code. | Step | Before | Via the API | | - | - | - | | Turn a video into a template | **Create template** on the Videos page | `POST /v3/templates` | | Mark the parts that change | The editor's **API variable** menu, or typing `{{name}}` | `PUT /v3/templates/{template_id}/variables` | | Rename or delete it | The template library | `PATCH` / `DELETE /v3/templates/{template_id}` | | Generate videos | `POST /v3/templates/{template_id}` | Same endpoint, unchanged | It works with videos you generate through the API, so the whole pipeline runs without a manual step. Authoring is about **what changes**, not layout. Positions, timing and scenes come from the source video. To change those, edit the template in the HeyGen editor. Built your template in the HeyGen editor instead? See [Building a Studio Template](/templates-guide). ## The endpoints | Endpoint | What it does | Scopes | | - | - | - | | [`POST /v3/templates`](/reference/create-template-from-video) | Create a template from a video | `templates:write`, `videos:read` | | [`GET /v3/templates/{template_id}`](/reference/get-template) | Read a template and its composition | `templates:read` | | [`PUT /v3/templates/{template_id}/variables`](/reference/set-template-variables) | Declare the template's variables | `templates:write` | | [`POST /v3/templates/{template_id}`](/reference/generate-video-from-template) | Generate a video from the template | `templates:read`, `videos:write` | | [`PATCH /v3/templates/{template_id}`](/reference/update-template) | Rename a template | `templates:write` | | [`DELETE /v3/templates/{template_id}`](/reference/delete-template) | Delete a template | `templates:write` | The rest of this page builds one real-estate listing template end to end. The agent introduces a property over a photo of the house, and four things become swappable: **the agent** (avatar), **their voice**, **the listing photo**, and **four phrases** in the script. ## Before you start * An **API key**, sent as the `x-api-key` header. * A **Creator role or higher** in the workspace, with template creation allowed in the workspace settings. Otherwise the authoring endpoints return `403` `resource_access_denied`. * A **source video**. It must be one you generated with `POST /v3/videos`, or one you created or saved in the HeyGen editor. URL-to-Video and older editor videos can't be used (`template_source_unsupported`). * Your own **IDs**. The video, template and element IDs in the examples won't exist in your workspace. Use the ones your responses return. Brandon and Caroline are public avatars and voices, so those IDs work anywhere. Want something you can run as-is? [The whole flow in one script](#the-whole-flow-in-one-script) passes each ID to the next step for you. ## Step 1: Make the source video Write the script with real values, not placeholders. Anything you later want to swap should appear as plain text you can point at, like `42 Maple Lane`. Here the listing photo is the background, and the avatar's own background is removed so the photo shows behind them. ```bash cURL theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d @- <<'JSON' { "type": "avatar", "title": "Listing - 42 Maple Lane (template source)", "avatar_id": "Brandon_Business_Standing_Front_public", "voice_id": "513b14b431b64a578c467c480dd0a9c3", "script": "Hi, I'm Brandon with Bayview Realty. Take a look at 42 Maple Lane: three bedrooms, a sunny kitchen, and a backyard made for summer. It's listed at $749,000. Book your private tour today.", "remove_background": true, "background": { "type": "image", "url": "https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=1920&q=80" }, "aspect_ratio": "16:9", "resolution": "1080p" } JSON ``` ```python Python theme={null} import requests API = "https://api.heygen.com" HEADERS = {"x-api-key": "YOUR_API_KEY"} video = requests.post(f"{API}/v3/videos", headers=HEADERS, json={ "type": "avatar", "title": "Listing - 42 Maple Lane (template source)", "avatar_id": "Brandon_Business_Standing_Front_public", "voice_id": "513b14b431b64a578c467c480dd0a9c3", "script": ( "Hi, I'm Brandon with Bayview Realty. Take a look at 42 Maple Lane: three bedrooms, " "a sunny kitchen, and a backyard made for summer. It's listed at $749,000. " "Book your private tour today." ), "remove_background": True, "background": {"type": "image", "url": "https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=1920&q=80"}, "aspect_ratio": "16:9", "resolution": "1080p", }).json()["data"] video_id = video["video_id"] ``` ```javascript JavaScript theme={null} const API = "https://api.heygen.com"; const HEADERS = { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }; const { data: video } = await (await fetch(`${API}/v3/videos`, { method: "POST", headers: HEADERS, body: JSON.stringify({ type: "avatar", title: "Listing - 42 Maple Lane (template source)", avatar_id: "Brandon_Business_Standing_Front_public", voice_id: "513b14b431b64a578c467c480dd0a9c3", script: "Hi, I'm Brandon with Bayview Realty. Take a look at 42 Maple Lane: three bedrooms, a sunny kitchen, and a backyard made for summer. It's listed at $749,000. Book your private tour today.", remove_background: true, background: { type: "image", url: "https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=1920&q=80" }, aspect_ratio: "16:9", resolution: "1080p", }), })).json(); const videoId = video.video_id; ``` ```json Example response theme={null} { "data": { "output_format": "mp4", "status": "waiting", "video_id": "3c8e1f6a9b2d4c7e8a5f0b1d6e9c2a47" } } ``` You don't have to wait for the render. A template copies the video's draft, not its rendered file, so you can create it as soon as you have the `video_id`. ## Step 2: Promote it to a template Pass the video ID and, optionally, a name (it defaults to the video's title). The template is private to your workspace and starts with **no variables**. ```bash cURL theme={null} curl -X POST "https://api.heygen.com/v3/templates" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"video_id": "3c8e1f6a9b2d4c7e8a5f0b1d6e9c2a47", "name": "Listing video - agent intro"}' ``` ```python Python theme={null} template = requests.post(f"{API}/v3/templates", headers=HEADERS, json={ "video_id": video_id, "name": "Listing video - agent intro", }).json()["data"] template_id = template["id"] ``` ```javascript JavaScript theme={null} const { data: template } = await (await fetch(`${API}/v3/templates`, { method: "POST", headers: HEADERS, body: JSON.stringify({ video_id: videoId, name: "Listing video - agent intro" }), })).json(); const templateId = template.id; ``` ```jsonc Example response (201 Created, abbreviated) theme={null} { "data": { "id": "6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64", "name": "Listing video - agent intro", "source_video_id": "3c8e1f6a9b2d4c7e8a5f0b1d6e9c2a47", "edit_version": "0", "variables": {}, "composition": { "scenes": [ { "id": "scene_0_1790737769242_zrnu3qfqm", "background": { "id": "bg_image_0_1790737769242_8e1hrymj6", "type": "image", "url": "https://static.heygen.ai/example-listing-photo.jpg", "bindable_as": ["image"] }, "elements": [ { "id": "avatar_0_1790737769242_ltg7rc9hp", "type": "avatar", "avatar_id": "Brandon_Business_Standing_Front_public", "bindable_as": ["character"] } ], "script": [ { "id": "script_0_1790737769242_2bae1yu2m", "type": "text", "text": "Hi, I'm Brandon with Bayview Realty. Take a look at 42 Maple Lane: three bedrooms, a sunny kitchen, and a backyard made for summer. It's listed at $749,000. Book your private tour today.", "voice_id": "513b14b431b64a578c467c480dd0a9c3", "bindable_as": ["voice", "text"] } ] } ], "background_audio": [] }, // also returned: aspect_ratio, created_at, updated_at, thumbnail_url when one exists, and the older scenes and scene_ids } } ``` The response is the full template. Note its `id`: every call after this one uses it. Two other fields matter for the next steps: `composition`, which lists every part of the video, and `edit_version`, which starts at `"0"`. ## Step 3: Find the parts you can swap `composition.scenes` describes each scene. Every part that can take a variable has an `id` and a `bindable_as` list, which says what kind of variable it accepts. The source frame with the background outlined and labeled image, and the avatar outlined and labeled character In this template there are three parts: | Part | Where it is in the response | `id` | `bindable_as` | | - | - | - | - | | The listing photo | `composition.scenes[0].background` | `bg_image_0_1790737769242_8e1hrymj6` | `image` | | The agent on screen | `composition.scenes[0].elements[0]` | `avatar_0_1790737769242_ltg7rc9hp` | `character` | | The spoken line | `composition.scenes[0].script[0]` | `script_0_1790737769242_2bae1yu2m` | `voice`, `text` | Your template will have different IDs. Take them from the `composition` in your own response. A template can also contain on-screen text, image and video elements, and `composition.background_audio` tracks for music and sound effects that bind as `audio`. Call `GET /v3/templates/{template_id}` at any time to read the same structure again. The response also still includes the older `scenes` and `scene_ids` fields. They are kept for existing integrations. Use `composition` for anything new. ## Step 4: Declare the variables A variable is a name plus the part it controls. There are two ways to create one, and most templates use both. For `character`, `voice`, `image`, `video` and `audio` variables. Point at a part from Step 3. Each part takes at most one of these. For `text` variables. Give the exact, case-sensitive text. Every occurrence in the script and on-screen text becomes `{{name}}`, and the matched text becomes the default. Add `element_ids` to limit it to specific parts. `PUT` replaces the template's complete list of variables. On a template that already has variables, read it first and send the full list with its current `edit_version`. [More on this below](#the-put-replaces-the-whole-list). We bind the agent, their voice and the listing photo, and turn four phrases into text variables: ```bash cURL theme={null} curl -X PUT "https://api.heygen.com/v3/templates/6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64/variables" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d @- <<'JSON' { "expected_edit_version": "0", "variables": { "agent": { "type": "character", "element_id": "avatar_0_1790737769242_ltg7rc9hp" }, "agent_voice": { "type": "voice", "element_id": "script_0_1790737769242_2bae1yu2m" }, "listing_photo": { "type": "image", "element_id": "bg_image_0_1790737769242_8e1hrymj6" }, "agent_name": { "type": "text", "match": "Brandon" }, "brokerage": { "type": "text", "match": "Bayview Realty" }, "address": { "type": "text", "match": "42 Maple Lane" }, "price": { "type": "text", "match": "$749,000" } } } JSON ``` ```python Python theme={null} scene = template["composition"]["scenes"][0] avatar = next(e for e in scene["elements"] if "character" in e["bindable_as"]) template = requests.put(f"{API}/v3/templates/{template_id}/variables", headers=HEADERS, json={ "expected_edit_version": template["edit_version"], "variables": { "agent": {"type": "character", "element_id": avatar["id"]}, "agent_voice": {"type": "voice", "element_id": scene["script"][0]["id"]}, "listing_photo": {"type": "image", "element_id": scene["background"]["id"]}, "agent_name": {"type": "text", "match": "Brandon"}, "brokerage": {"type": "text", "match": "Bayview Realty"}, "address": {"type": "text", "match": "42 Maple Lane"}, "price": {"type": "text", "match": "$749,000"}, }, }).json()["data"] ``` ```javascript JavaScript theme={null} const scene = template.composition.scenes[0]; const avatar = scene.elements.find((e) => e.bindable_as.includes("character")); const { data: updated } = await (await fetch(`${API}/v3/templates/${templateId}/variables`, { method: "PUT", headers: HEADERS, body: JSON.stringify({ expected_edit_version: template.edit_version, variables: { agent: { type: "character", element_id: avatar.id }, agent_voice: { type: "voice", element_id: scene.script[0].id }, listing_photo: { type: "image", element_id: scene.background.id }, agent_name: { type: "text", match: "Brandon" }, brokerage: { type: "text", match: "Bayview Realty" }, address: { type: "text", match: "42 Maple Lane" }, price: { type: "text", match: "$749,000" }, }, }), })).json(); ``` ```jsonc Example response (200 OK, abbreviated) theme={null} { "data": { "id": "6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64", "edit_version": "1", "composition": { "scenes": [ { "background": { "id": "bg_image_0_1790737769242_8e1hrymj6", "variable": "listing_photo" }, "elements": [{ "id": "avatar_0_1790737769242_ltg7rc9hp", "variable": "agent" }], "script": [ { "id": "script_0_1790737769242_2bae1yu2m", "text": "Hi, I'm {{agent_name}} with {{brokerage}}. Take a look at {{address}}: three bedrooms, a sunny kitchen, and a backyard made for summer. It's listed at {{price}}. Book your private tour today.", "text_variables": ["agent_name", "brokerage", "address", "price"], "variable": "agent_voice" } ] } ] }, "variables": { "address": { "content": "42 Maple Lane", "type": "text" }, "agent": { "type": "character", "avatar_id": "Brandon_Business_Standing_Front_public" }, "agent_name": { "content": "Brandon", "type": "text" }, "agent_voice": { "type": "voice", "voice_id": "513b14b431b64a578c467c480dd0a9c3" }, "brokerage": { "content": "Bayview Realty", "type": "text" }, "listing_photo": { "asset": { "type": "url", "url": "https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=1920&q=80" }, "fit": "contain", "type": "image" }, "price": { "content": "$749,000", "type": "text" } }, // also returned: the rest of the template, as in Step 2 } } ``` Look at `composition.scenes[0].script[0].text` in the response. The four phrases are now placeholders: ```text theme={null} Before: Hi, I'm Brandon with Bayview Realty. Take a look at 42 Maple Lane: ... It's listed at $749,000. ... After: Hi, I'm {{agent_name}} with {{brokerage}}. Take a look at {{address}}: ... It's listed at {{price}}. ... ``` Each bound part now carries a `variable` field with its variable's name, `edit_version` went from `"0"` to `"1"`, and `variables` lists each variable with its default value. ### The PUT replaces the whole list **The body you send becomes the complete list of variables.** Anything you leave out is deleted, and its text goes back to the default. To add a variable, send the same list again plus the new one. Re-sending a `match` that is already a placeholder is fine. ```jsonc Wrong: only the new variable theme={null} { "variables": { "bedrooms": { "type": "text", "match": "three bedrooms" } } } // Result: 1 variable. The other 7 are deleted and the script is back to // "Hi, I'm Brandon with Bayview Realty. Take a look at 42 Maple Lane: {{bedrooms}}, ..." ``` ```jsonc Right: the full list plus the new one theme={null} { "expected_edit_version": "1", "variables": { "agent": { "type": "character", "element_id": "avatar_0_1790737769242_ltg7rc9hp" }, "agent_voice": { "type": "voice", "element_id": "script_0_1790737769242_2bae1yu2m" }, "listing_photo": { "type": "image", "element_id": "bg_image_0_1790737769242_8e1hrymj6" }, "agent_name": { "type": "text", "match": "Brandon" }, "brokerage": { "type": "text", "match": "Bayview Realty" }, "address": { "type": "text", "match": "42 Maple Lane" }, "price": { "type": "text", "match": "$749,000" }, "bedrooms": { "type": "text", "match": "three bedrooms" } } } // Result: 8 variables, edit_version "2", and the script reads // "Hi, I'm {{agent_name}} with {{brokerage}}. Take a look at {{address}}: {{bedrooms}}, ..." ``` ### Protect against overwrites with `expected_edit_version` Every change to the variables increases `edit_version`. Send the version you read as `expected_edit_version`, and the API applies your change only if nobody changed the template since. It prevents this: It is at `edit_version` `"1"` with seven variables. They add `bedrooms`. The template is now at `edit_version` `"2"`. Your PUT carries the seven variables you read. Because the PUT replaces the whole list, it would silently delete your teammate's `bedrooms`. With `"expected_edit_version": "1"`, the API sees the template is at `"2"`, **rejects the request, and changes nothing**. Read the template, which now includes `bedrooms`, add your change, and send it with `"expected_edit_version": "2"`. This is the rejection, from sending `"0"` after the template had moved to `"1"`: ```json Example response (409 Conflict) theme={null} { "error": { "code": "stale_edit_version", "doc_url": "https://developers.heygen.com/docs/error-codes#stale-edit-version", "message": "Template 6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64 is at edit_version 1, not 0. Re-read it with GET /v3/templates/6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64 and send the change again with the current edit_version.", "param": "expected_edit_version" } } ``` ## Step 5: Generate a video per agent, per listing Send the values for this video, keyed by variable name. Character, voice and image variables you leave out keep the template's value. This call swaps in a different agent, Caroline, with her own voice, and a new listing: ```bash cURL theme={null} curl -X POST "https://api.heygen.com/v3/templates/6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d @- <<'JSON' { "title": "Listing - 7 Birch Court (Caroline)", "variables": { "agent": { "type": "character", "avatar_id": "Caroline_Business_Standing_Front_public" }, "agent_voice": { "type": "voice", "voice_id": "f8a2cbd411e84454afc4aba8895d0a3b" }, "agent_name": { "type": "text", "content": "Caroline" }, "brokerage": { "type": "text", "content": "Summit Homes" }, "address": { "type": "text", "content": "7 Birch Court" }, "price": { "type": "text", "content": "$615,000" }, "listing_photo": { "type": "image", "asset": { "type": "url", "url": "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=1920&q=80" } } } } JSON ``` ```python Python theme={null} listing = { "agent_name": "Caroline", "brokerage": "Summit Homes", "address": "7 Birch Court", "price": "$615,000", "photo": "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=1920&q=80", } variables = {k: {"type": "text", "content": listing[k]} for k in ("agent_name", "brokerage", "address", "price")} variables["agent"] = {"type": "character", "avatar_id": "Caroline_Business_Standing_Front_public"} variables["agent_voice"] = {"type": "voice", "voice_id": "f8a2cbd411e84454afc4aba8895d0a3b"} variables["listing_photo"] = {"type": "image", "asset": {"type": "url", "url": listing["photo"]}} job = requests.post(f"{API}/v3/templates/{template_id}", headers=HEADERS, json={ "title": "Listing - 7 Birch Court (Caroline)", "variables": variables, }).json()["data"] new_video_id = job["id"] ``` ```javascript JavaScript theme={null} const listing = { agent_name: "Caroline", brokerage: "Summit Homes", address: "7 Birch Court", price: "$615,000", photo: "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=1920&q=80", }; const variables = Object.fromEntries( ["agent_name", "brokerage", "address", "price"].map((k) => [k, { type: "text", content: listing[k] }]), ); variables.agent = { type: "character", avatar_id: "Caroline_Business_Standing_Front_public" }; variables.agent_voice = { type: "voice", voice_id: "f8a2cbd411e84454afc4aba8895d0a3b" }; variables.listing_photo = { type: "image", asset: { type: "url", url: listing.photo } }; const { data: job } = await (await fetch(`${API}/v3/templates/${templateId}`, { method: "POST", headers: HEADERS, body: JSON.stringify({ title: "Listing - 7 Birch Court (Caroline)", variables }), })).json(); const newVideoId = job.id; ``` ```json Example response (200 OK) theme={null} { "data": { "created_at": 1790737782, "id": "9d4b7e2a5c1f4a8e9b3d6c0f2e7a1b58", "status": "pending", "title": "Listing - 7 Birch Court (Caroline)" } } ``` The response comes back right away with the new video's `id`. Both videos below came from the same template: Only the text and photo were sent. Brandon and his voice came from the template. The request above: Caroline, her own voice, and new text and photo. **Always send every text variable.** A text variable you leave out is not filled with its default. HeyGen reads the placeholder name out loud instead. This video was generated with only `address` sent, and it says *"Hi, I'm Agent Name with Brokerage... It's listed at price."* Read the list of variables from `GET /v3/templates/{template_id}` and fill all the text ones, so a variable added to the template later never slips through. ### Swapped media keeps the source video's framing When you omit `fit`, a template created with `POST /v3/templates` places the new image or video the way the source video placed the original: a background that filled the frame still fills it, and a photo placed inside a box still fits inside that box. Send `fit` only to change that, for example `"fit": "contain"` to show the whole photo with bars, or `"fit": "cover"` to fill a box that used to show a whole image. The variable list from `GET /v3/templates/{template_id}` echoes `"fit": "contain"` for image and video variables. That is the default for templates built in the HeyGen editor. On a template created from a video, omitting `fit` keeps the source framing, shown on the left. Swapped listing photo filling the whole frame Swapped listing photo with white bars on both sides ## Step 6: Wait for the video Poll [`GET /v3/videos/{video_id}`](/reference/get-video) until `status` is `completed` (or `failed`), or pass `callback_url` on the generate call to get a webhook instead. When you generate many videos, use webhooks: polling each one adds up against your rate limit. ```bash cURL theme={null} curl "https://api.heygen.com/v3/videos/9d4b7e2a5c1f4a8e9b3d6c0f2e7a1b58" \ -H "x-api-key: YOUR_API_KEY" ``` ```json Example response (200 OK, completed) theme={null} { "data": { "completed_at": 1790738122, "created_at": 1790737782, "duration": 13.5576, "gif_url": "https://resource2.heygen.ai/example-preview.gif", "id": "9d4b7e2a5c1f4a8e9b3d6c0f2e7a1b58", "status": "completed", "thumbnail_url": "https://files2.heygen.ai/example-thumbnail.jpg?Expires=...&Signature=...", "title": "Listing - 7 Birch Court (Caroline)", "video_page_url": "https://app.heygen.com/videos/9d4b7e2a5c1f4a8e9b3d6c0f2e7a1b58", "video_url": "https://files2.heygen.ai/example-video.mp4?Expires=...&Signature=..." } } ``` `video_url` and `thumbnail_url` are signed links that expire. Call this endpoint again whenever you need fresh ones. ## The whole flow in one script Creates the source video, promotes it, declares variables from the composition, generates one video per listing, and waits for each. ```python Python theme={null} import time import requests API = "https://api.heygen.com" HEADERS = {"x-api-key": "YOUR_API_KEY"} def call(method, path, **kwargs): response = requests.request(method, API + path, headers=HEADERS, **kwargs) response.raise_for_status() return response.json()["data"] # 1. Source video, written with real values video = call("POST", "/v3/videos", json={ "type": "avatar", "title": "Listing - 42 Maple Lane (template source)", "avatar_id": "Brandon_Business_Standing_Front_public", "voice_id": "513b14b431b64a578c467c480dd0a9c3", "script": ( "Hi, I'm Brandon with Bayview Realty. Take a look at 42 Maple Lane: three bedrooms, " "a sunny kitchen, and a backyard made for summer. It's listed at $749,000. " "Book your private tour today." ), "remove_background": True, "background": {"type": "image", "url": "https://images.unsplash.com/photo-1568605114967-8130f3a36994?w=1920&q=80"}, "aspect_ratio": "16:9", "resolution": "1080p", }) # 2. Promote it (no need to wait for the render) template = call("POST", "/v3/templates", json={"video_id": video["video_id"], "name": "Listing video - agent intro"}) # 3. Find the parts to bind scene = template["composition"]["scenes"][0] avatar = next(e for e in scene["elements"] if "character" in e["bindable_as"]) # 4. Declare the full variable list template = call("PUT", f"/v3/templates/{template['id']}/variables", json={ "expected_edit_version": template["edit_version"], "variables": { "agent": {"type": "character", "element_id": avatar["id"]}, "agent_voice": {"type": "voice", "element_id": scene["script"][0]["id"]}, "listing_photo": {"type": "image", "element_id": scene["background"]["id"]}, "agent_name": {"type": "text", "match": "Brandon"}, "brokerage": {"type": "text", "match": "Bayview Realty"}, "address": {"type": "text", "match": "42 Maple Lane"}, "price": {"type": "text", "match": "$749,000"}, }, }) text_variables = [name for name, v in template["variables"].items() if v["type"] == "text"] # 5. One video per listing listings = [ {"agent_name": "Brandon", "brokerage": "Bayview Realty", "address": "18 Harbor View Drive", "price": "$1,125,000", "photo": "https://images.unsplash.com/photo-1570129477492-45c003edd2be?w=1920&q=80"}, {"agent_name": "Caroline", "brokerage": "Summit Homes", "address": "7 Birch Court", "price": "$615,000", "photo": "https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?w=1920&q=80", "avatar_id": "Caroline_Business_Standing_Front_public", "voice_id": "f8a2cbd411e84454afc4aba8895d0a3b"}, ] video_ids = [] for listing in listings: variables = {name: {"type": "text", "content": listing[name]} for name in text_variables} # every text variable variables["listing_photo"] = {"type": "image", "asset": {"type": "url", "url": listing["photo"]}} if "avatar_id" in listing: # otherwise the template's agent and voice are used variables["agent"] = {"type": "character", "avatar_id": listing["avatar_id"]} variables["agent_voice"] = {"type": "voice", "voice_id": listing["voice_id"]} job = call("POST", f"/v3/templates/{template['id']}", json={"title": listing["address"], "variables": variables}) video_ids.append(job["id"]) # 6. Wait for each (at scale, pass callback_url instead of polling) for video_id in video_ids: while (status := call("GET", f"/v3/videos/{video_id}"))["status"] not in ("completed", "failed"): time.sleep(15) print(video_id, status["status"], status.get("video_url")) ``` ## Rename and delete ### Rename `PATCH` renames the template and returns the full template. Renaming does not change `edit_version`. Other changes go through `PUT /v3/templates/{template_id}/variables` or the HeyGen editor. ```bash cURL theme={null} curl -X PATCH "https://api.heygen.com/v3/templates/6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Listing video - agent intro (v2)"}' ``` ```jsonc Example response (200 OK, abbreviated) theme={null} { "data": { "id": "6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64", "name": "Listing video - agent intro (v2)", "edit_version": "1", "updated_at": 1790737790, // also returned: the rest of the template, unchanged } } ``` ### Delete `DELETE` removes the template from your list so it can no longer generate videos. Videos you already generated from it are not affected. There is no request body. ```bash cURL theme={null} curl -X DELETE "https://api.heygen.com/v3/templates/6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64" \ -H "x-api-key: YOUR_API_KEY" ``` ```json Example response (200 OK) theme={null} { "data": { "deleted": true, "id": "6f2a9c4e1b7d4f3a8e5c2b9d0a1f7e64" } } ``` Reading a deleted template afterwards returns a 404: ```json Example response (404 Not Found) theme={null} { "error": { "code": "template_not_found", "doc_url": "https://developers.heygen.com/docs/error-codes#template-not-found", "message": "Template not found" } } ``` ## Errors | Status and code | When it happens | What to do | | - | - | - | | `400` `template_source_unsupported` | The source video has no current-format draft (URL-to-Video, older editor videos). | Recreate it with `POST /v3/videos`, or save it in the HeyGen editor. | | `400` `variable_match_not_found` | A `match` text doesn't occur in the template's script or on-screen text. Matching is exact and case-sensitive. | Check the current text in `composition`. If it's already a `{{name}}` placeholder, declare the variable without `match`. | | `400` `variable_match_spans_styles` | The `match` text runs across differently styled parts of rich text, such as half bold and half regular. | Match a shorter piece inside one style, or make the styling uniform in the editor. | | `400` `template_limit_reached` | The workspace already has the maximum number of templates (2000). | Delete templates you no longer need, then retry. | | `403` `insufficient_api_key_scope` | The API key is missing a scope the endpoint needs. | Check the key's permissions against the endpoints table above. | | `403` `resource_access_denied` | Your role is below Creator, or template creation is turned off in the workspace settings. | Ask a workspace admin. | | `403` `forbidden` | The template authoring endpoints aren't enabled for your workspace yet. | Contact support. | | `404` `video_not_found` | The `video_id` isn't in this workspace. | Check the ID, and that the API key belongs to the same workspace. | | `404` `template_not_found` | The template ID is wrong, in another workspace, or deleted. | List your templates with [`GET /v3/templates`](/reference/list-templates). | | `409` `stale_edit_version` | The template changed after you read it. | Read it again, apply your change to the new list, and send it with the new `edit_version`. | ## FAQ Any avatar look you can use, from [`GET /v3/avatars/looks`](/reference/list-avatar-looks), including your own digital twin or photo avatar. Pair a `character` variable with a `voice` variable so the voice matches the person. Declare the text variable without `match`, for example `"first_name": { "type": "text" }`. Placeholders that already exist are kept. Yes. Pass `scene_ids` on the generate call to pick, reorder or repeat scenes that exist in the template. ## Related Every variable type, asset input and generate option. The same flow, with the template built in the HeyGen editor. # API Key Source: https://heygen-1fa696a7.mintlify.app/docs/api-key Generate your HeyGen API key in under a minute. Authenticate requests for avatar video generation, translation, and streaming. ## Getting Your API Key 1. Go to the [API keys page](https://platform.heygen.com/api-keys). 2. Click to generate your API key. ## Configuring Your API Key ### Environment variable (recommended) ```bash bash theme={null} export HEYGEN_API_KEY="your-api-key-here" ``` ### `.env` file If your project uses a `.env` file (common with Node.js, Python, or frameworks like Next.js): ```text theme={null} HEYGEN_API_KEY=your-api-key-here ``` ### Claude Code If you're using Claude Code or any terminal-based workflow, set the key in your shell before starting: ```bash bash theme={null} export HEYGEN_API_KEY="your-api-key-here" claude # or whatever command starts your session ``` Alternatively, add it to your shell profile (`~/.bashrc`, `~/.zshrc`) so it persists across sessions: ```bash bash theme={null} echo 'export HEYGEN_API_KEY="your-api-key-here"' >> ~/.zshrc source ~/.zshrc ``` ### HeyGen Skills (in Claude) When using HeyGen through the Skills integration in Claude's computer environment, the API key is read from the environment. Make sure `HEYGEN_API_KEY` is set before the skill executes any API calls. ## Using the Key in Requests All HeyGen API requests authenticate via the `X-Api-Key` header. The base URL for all endpoints is `https://api.heygen.com`. When auth fails, the API returns [`unauthorized` (401)](/docs/error-codes#unauthorized). ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/avatars" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ```javascript Node.js theme={null} const response = await fetch("https://api.heygen.com/v3/avatars", { headers: { "X-Api-Key": process.env.HEYGEN_API_KEY }, }); ``` ```python Python theme={null} import os, requests response = requests.get( "https://api.heygen.com/v3/avatars", headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]} ) ``` ### Quick verification You can verify your key is working by fetching your account info. Full schema: [`GET /v3/users/me`](/reference/get-current-user). ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/users/me" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ```json Response theme={null} { "data": { "username": "jane_doe", "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe", "billing_type": "wallet", "wallet": { "currency": "usd", "remaining_balance": 42.50, "auto_reload": { "enabled": false } } } } ``` A `200` response with your account details confirms your key is valid. The `billing_type` and corresponding billing field (`wallet`, `subscription`, or `usage_based`) show your current balance and billing model. ## Restricting Key Permissions Use permission scopes to limit a key to only the resources and actions it needs. See [API Key Permission Scopes](/docs/api-key-permissions) for the complete, automatically updated mapping of scopes to endpoints. To check which scopes a key holds, call [`GET /v3/api_keys/self`](/reference/get-current-api-key) with that key. It returns the key's name, `scope_mode` (`full`, `read_only`, or `custom`), `scopes`, and expiration, and works with any key regardless of its scopes. ## Security Best Practices * **Never commit your API key to version control.** Add `.env` to your `.gitignore`. * **Never expose the key in client-side / browser code.** Always call the API from a backend or server environment. * **Rotate your key periodically** via the API dashboard. * **Monitor usage** on the [Usage page](https://platform.heygen.com/usage). # API Key Permission Scopes Source: https://heygen-1fa696a7.mintlify.app/docs/api-key-permissions Find the API key permission scopes required by each public HeyGen API endpoint. Permission scopes let you restrict an API key to the HeyGen resources and actions it needs. Each scope uses the form `resource:read` or `resource:write`. ## How permissions work * An endpoint can require one scope or several scopes. When several are listed, the key must satisfy all of them. * A `resource:write` grant also satisfies `resource:read` for the same resource. A read grant never satisfies a write requirement. * Full-access keys use `*:*`. Read-only keys use `*:read`, which satisfies every current and future read requirement. * A key without the required permissions receives a `403` [`insufficient_api_key_scope`](/docs/error-codes#insufficient-api-key-scope) response. Create and manage keys in the [HeyGen API dashboard](https://app.heygen.com/developers/api). ## Scope-to-endpoint mapping This reference is generated from the permission metadata in the published OpenAPI specification. Only public, documented endpoints are included. An endpoint with multiple requirements appears under every required scope. ### `account:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/users/me`](/reference/get-current-user) | Get Current User | `account:read` | ### `ai_clipping:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/ai-clipping`](/reference/list-ai-clipping) | List AI Clipping | `ai_clipping:read` | | [`GET /v3/ai-clipping/{job_id}`](/reference/get-ai-clipping) | Get AI Clipping | `ai_clipping:read` | ### `ai_clipping:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/ai-clipping`](/reference/create-ai-clipping) | Create AI Clipping | `ai_clipping:write` | | [`DELETE /v3/ai-clipping/{job_id}`](/reference/delete-ai-clipping) | Delete AI Clipping | `ai_clipping:write` | ### `assets:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/assets/batches/{batch_id}`](/reference/get-asset-batch) | Get Asset Batch | `assets:read` | | [`GET /v3/assets/statuses`](/reference/bulk-asset-statuses) | Bulk Asset Statuses | `assets:read` | | [`GET /v3/assets/{asset_id}`](/reference/get-asset) | Get Asset | `assets:read` | | [`GET /v3/audio/sounds`](/reference/search-audio-music-or-sound-effects) | Search audio (music or sound effects) | `assets:read` | ### `assets:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/assets`](/reference/upload-asset) | Upload Asset | `assets:write` | | [`POST /v3/assets/complete/batches`](/reference/complete-asset-upload-batch) | Complete Asset Upload Batch | `assets:write` | | [`POST /v3/assets/direct-uploads`](/reference/create-asset-upload) | Create Asset Upload | `assets:write` | | [`POST /v3/assets/direct-uploads/batches`](/reference/create-asset-upload-batch) | Create Asset Upload Batch | `assets:write` | | [`DELETE /v3/assets/{asset_id}`](/reference/delete-asset) | Delete Asset | `assets:write` | | [`POST /v3/assets/{asset_id}/complete`](/reference/complete-asset-upload) | Complete Asset Upload | `assets:write` | ### `avatars:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/avatars`](/reference/list-avatar-groups) | List Avatar Groups | `avatars:read` | | [`GET /v3/avatars/look-templates`](/reference/list-look-templates) | List Look Templates | `avatars:read` | | [`GET /v3/avatars/looks`](/reference/list-avatar-looks) | List Avatar Looks | `avatars:read` | | [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look) | Get Avatar Look | `avatars:read` | | [`GET /v3/avatars/{group_id}`](/reference/get-avatar-group) | Get Avatar Group | `avatars:read` | ### `avatars:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/avatars`](/reference/create-avatar) | Create Avatar | `avatars:write` | | [`POST /v3/avatars/looks`](/reference/create-avatar-looks) | Create Avatar Looks | `avatars:write` | | [`PATCH /v3/avatars/looks/{look_id}`](/reference/update-avatar-look) | Update Avatar Look | `avatars:write` | | [`DELETE /v3/avatars/looks/{look_id}`](/reference/delete-avatar-look) | Delete Avatar Look | `avatars:write` | | [`PATCH /v3/avatars/{group_id}`](/reference/update-avatar-group) | Update Avatar Group | `avatars:write` | | [`DELETE /v3/avatars/{group_id}`](/reference/delete-avatar-group) | Delete Avatar Group | `avatars:write` | | [`POST /v3/avatars/{group_id}/consent`](/reference/create-avatar-consent) | Create Avatar Consent | `avatars:write` | ### `brand:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/brand-glossaries`](/reference/list-brand-glossaries) | List Brand Glossaries | `brand:read` | | [`GET /v3/brand-glossaries/{brand_glossary_id}`](/reference/get-brand-glossary) | Get Brand Glossary | `brand:read` | | [`GET /v3/brand-kits`](/reference/list-brand-kits) | List Brand Kits | `brand:read` | | [`GET /v3/brand-kits/{brand_kit_id}`](/reference/get-brand-kit) | Get Brand Kit | `brand:read` | ### `brand:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/brand-glossaries`](/reference/create-brand-glossary) | Create Brand Glossary | `brand:write` | | [`PATCH /v3/brand-glossaries/{brand_glossary_id}`](/reference/update-brand-glossary) | Update Brand Glossary | `brand:write` | | [`DELETE /v3/brand-glossaries/{brand_glossary_id}`](/reference/delete-brand-glossary) | Delete Brand Glossary | `brand:write` | | [`POST /v3/brand-kits`](/reference/create-brand-kit) | Create Brand Kit | `brand:write` | | [`PATCH /v3/brand-kits/{brand_kit_id}`](/reference/update-brand-kit) | Update Brand Kit | `brand:write` | | [`DELETE /v3/brand-kits/{brand_kit_id}`](/reference/delete-brand-kit) | Delete Brand Kit | `brand:write` | ### `filler_removal:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/filler-word-removals/{filler_word_removal_id}`](/reference/get-filler-word-removal) | Get Filler Word Removal | `filler_removal:read` | ### `filler_removal:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/filler-word-removals`](/reference/create-filler-word-removal) | Create Filler Word Removal | `filler_removal:write` | ### `hyperframes:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/hyperframes/renders`](/reference/list-hyperframes-renders) | List HyperFrames Renders | `hyperframes:read` | | [`GET /v3/hyperframes/renders/{render_id}`](/reference/get-hyperframes-render) | Get HyperFrames Render | `hyperframes:read` | ### `hyperframes:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/hyperframes/renders`](/reference/create-hyperframes-render) | Create HyperFrames Render | `hyperframes:write` | | [`DELETE /v3/hyperframes/renders/{render_id}`](/reference/delete-hyperframes-render) | Delete HyperFrames Render | `hyperframes:write` | ### `images:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/models/images/{generation_id}`](/reference/get-heygen-image) | Get HeyGen Image | `images:read` | ### `images:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/models/images`](/reference/create-heygen-image) | Create HeyGen Image | `images:write` | ### `lipsync:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/lipsyncs`](/reference/list-lipsyncs) | List Lipsyncs | `lipsync:read` | | [`GET /v3/lipsyncs/batches/{batch_id}`](/reference/get-lipsync-batch) | Get Lipsync Batch | `lipsync:read` | | [`GET /v3/lipsyncs/statuses`](/reference/bulk-lipsync-statuses) | Bulk Lipsync Statuses | `lipsync:read` | | [`GET /v3/lipsyncs/{lipsync_id}`](/reference/get-lipsync) | Get Lipsync | `lipsync:read` | ### `lipsync:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/lipsyncs`](/reference/create-lipsync) | Create Lipsync | `lipsync:write` | | [`POST /v3/lipsyncs/batches`](/reference/create-lipsync-batch) | Create Lipsync Batch | `lipsync:write` | | [`PATCH /v3/lipsyncs/{lipsync_id}`](/reference/update-lipsync) | Update Lipsync | `lipsync:write` | | [`DELETE /v3/lipsyncs/{lipsync_id}`](/reference/delete-lipsync) | Delete Lipsync | `lipsync:write` | ### `templates:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/templates`](/reference/list-templates) | List Templates | `templates:read` | | [`GET /v3/templates/{template_id}`](/reference/get-template) | Get Template | `templates:read` | | [`POST /v3/templates/{template_id}`](/reference/generate-video-from-template) | Generate Video from Template | `templates:read`, `videos:write` | ### `templates:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/templates`](/reference/create-template-from-video) | Create Template from Video | `templates:write`, `videos:read` | | [`PATCH /v3/templates/{template_id}`](/reference/update-template) | Update Template | `templates:write` | | [`DELETE /v3/templates/{template_id}`](/reference/delete-template) | Delete Template | `templates:write` | | [`PUT /v3/templates/{template_id}/variables`](/reference/set-template-variables) | Set Template Variables | `templates:write` | ### `translations:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/video-translations`](/reference/list-video-translations) | List Video Translations | `translations:read` | | [`GET /v3/video-translations/batches/{batch_id}`](/reference/get-video-translation-batch) | Get Video Translation Batch | `translations:read` | | [`GET /v3/video-translations/languages`](/reference/list-supported-translation-languages) | List Supported Translation Languages | `translations:read` | | [`GET /v3/video-translations/proofreads`](/reference/list-proofread-sessions) | List Proofread Sessions | `translations:read` | | [`GET /v3/video-translations/proofreads/{proofread_id}`](/reference/get-proofread-session) | Get Proofread Session | `translations:read` | | [`GET /v3/video-translations/proofreads/{proofread_id}/srt`](/reference/download-proofread-srt) | Download Proofread SRT | `translations:read` | | [`GET /v3/video-translations/statuses`](/reference/bulk-video-translation-statuses) | Bulk Video Translation Statuses | `translations:read` | | [`GET /v3/video-translations/{video_translation_id}`](/reference/get-video-translation) | Get Video Translation | `translations:read` | ### `translations:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/video-translations`](/reference/create-video-translation) | Create Video Translation | `translations:write` | | [`POST /v3/video-translations/batches`](/reference/create-video-translation-batch) | Create Video Translation Batch | `translations:write` | | [`POST /v3/video-translations/proofreads`](/reference/create-proofread-session) | Create Proofread Session | `translations:write` | | [`DELETE /v3/video-translations/proofreads/{proofread_id}`](/reference/delete-proofread-session) | Delete Proofread Session | `translations:write` | | [`POST /v3/video-translations/proofreads/{proofread_id}/generate`](/reference/generate-video-from-proofread) | Generate Video from Proofread | `translations:write` | | [`PUT /v3/video-translations/proofreads/{proofread_id}/srt`](/reference/upload-proofread-srt) | Upload Proofread SRT | `translations:write` | | [`PATCH /v3/video-translations/{video_translation_id}`](/reference/update-video-translation) | Update Video Translation | `translations:write` | | [`DELETE /v3/video-translations/{video_translation_id}`](/reference/delete-video-translation) | Delete Video Translation | `translations:write` | ### `video_agent:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/video-agents`](/reference/list-video-agent-sessions) | List Video Agent Sessions | `video_agent:read` | | [`GET /v3/video-agents/styles`](/reference/list-video-agent-styles) | List Video Agent Styles | `video_agent:read` | | [`GET /v3/video-agents/{session_id}`](/reference/get-video-agent-session) | Get Video Agent Session | `video_agent:read` | | [`GET /v3/video-agents/{session_id}/resources/{resource_id}`](/reference/get-session-resource) | Get Session Resource | `video_agent:read` | | [`GET /v3/video-agents/{session_id}/videos`](/reference/list-session-videos) | List Session Videos | `video_agent:read` | ### `video_agent:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/video-agents`](/reference/create-video-agent-session) | Create Video Agent Session | `video_agent:write` | | [`POST /v3/video-agents/{session_id}`](/reference/send-message-or-request-revision) | Send Video Agent Message | `video_agent:write` | | [`POST /v3/video-agents/{session_id}/stop`](/reference/stop-video-agent-session) | Stop Video Agent Session | `video_agent:write` | ### `videos:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/folders/{folder_id}`](/reference/get-folder) | Get Folder | `videos:read` | | [`GET /v3/models/videos/{video_id}`](/reference/get-heygen-video) | Get HeyGen Video | `videos:read` | | [`POST /v3/templates`](/reference/create-template-from-video) | Create Template from Video | `templates:write`, `videos:read` | | [`GET /v3/videos`](/reference/list-videos) | List Videos | `videos:read` | | [`GET /v3/videos/batches/{batch_id}`](/reference/get-video-batch) | Get Video Batch | `videos:read` | | [`GET /v3/videos/statuses`](/reference/bulk-video-statuses) | Bulk Video Statuses | `videos:read` | | [`GET /v3/videos/{video_id}`](/reference/get-video) | Get Video | `videos:read` | | [`GET /v3/videos/{video_id}/scenes`](/reference/get-video-scenes) | Get Video Scenes | `videos:read` | ### `videos:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/folders`](/reference/create-folder) | Create Folder | `videos:write` | | [`POST /v3/models/videos`](/reference/create-heygen-video) | Create HeyGen Video | `videos:write` | | [`POST /v3/templates/{template_id}`](/reference/generate-video-from-template) | Generate Video from Template | `templates:read`, `videos:write` | | [`POST /v3/videos`](/reference/create-video) | Create Video | `videos:write` | | [`POST /v3/videos/batches`](/reference/create-video-batch) | Create Video Batch | `videos:write` | | [`DELETE /v3/videos/{video_id}`](/reference/delete-video) | Delete Video | `videos:write` | ### `voices:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/models/audio/voices`](/reference/list-audio-voices) | List Audio Voices | `voices:read` | | [`GET /v3/models/audio/voices/{voice_id}`](/reference/get-an-audio-voice) | Get an Audio Voice | `voices:read` | | [`GET /v3/voices`](/reference/list-voices) | List Voices | `voices:read` | | [`GET /v3/voices/{voice_id}`](/reference/get-voice) | Get Voice | `voices:read` | ### `voices:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/models/audio/tts`](/reference/generate-model-speech) | Generate Speech | `voices:write` | | [`POST /v3/models/audio/tts/stream`](/reference/stream-speech) | Stream Speech | `voices:write` | | [`POST /v3/models/audio/voices`](/reference/create-or-retrain-an-audio-voice) | Create or Retrain an Audio Voice | `voices:write` | | [`DELETE /v3/models/audio/voices/{voice_id}`](/reference/delete-an-audio-voice) | Delete an Audio Voice | `voices:write` | | [`POST /v3/voices`](/reference/design-a-voice) | Design a Voice | `voices:write` | | [`POST /v3/voices/clone`](/reference/clone-a-voice) | Clone a Voice | `voices:write` | | [`POST /v3/voices/speech`](/reference/generate-speech) | Generate Speech | `voices:write` | | [`DELETE /v3/voices/{voice_id}`](/reference/delete-a-voice) | Delete a Voice | `voices:write` | ### `webhooks:read` | Endpoint | Description | Required scopes | | - | - | - | | [`GET /v3/webhooks/endpoints`](/reference/list-webhook-endpoints) | List Webhook Endpoints | `webhooks:read` | | [`GET /v3/webhooks/event-types`](/reference/list-webhook-event-types) | List Webhook Event Types | `webhooks:read` | | [`GET /v3/webhooks/events`](/reference/list-webhook-events) | List Webhook Events | `webhooks:read` | ### `webhooks:write` | Endpoint | Description | Required scopes | | - | - | - | | [`POST /v3/webhooks/endpoints`](/reference/create-webhook-endpoint) | Create Webhook Endpoint | `webhooks:write` | | [`PATCH /v3/webhooks/endpoints/{endpoint_id}`](/reference/update-webhook-endpoint) | Update Webhook Endpoint | `webhooks:write` | | [`DELETE /v3/webhooks/endpoints/{endpoint_id}`](/reference/delete-webhook-endpoint) | Delete Webhook Endpoint | `webhooks:write` | | [`POST /v3/webhooks/endpoints/{endpoint_id}/rotate-secret`](/reference/rotate-webhook-signing-secret) | Rotate Webhook Signing Secret | `webhooks:write` | # Avatar Consent Source: https://heygen-1fa696a7.mintlify.app/docs/avatar-consent How HeyGen verifies that the person in a digital twin agreed to be cloned — the three levels of consent access, and the API flow for collecting it. Before you can generate video with a **digital twin** ([`type: "digital_twin"`](/docs/avatar-from-video)), HeyGen needs proof that the person depicted agreed to be cloned. This protects the avatar subject, your account, and HeyGen — and it's what lets you use a real person's likeness responsibly at scale. The consent API step applies only to **digital twin** avatars. A group created from a photo (`type: "photo"`) or prompt (`type: "prompt"`) has no consent request to make and reports `consent_status` of `null`. If you add a photo or prompt look to an existing group, it retains that group's consent status. A photo avatar is still built from a real person's image. Having no API step to complete is not permission to use someone's likeness. HeyGen runs no consent check and keeps no consent record for a photo avatar, so getting the subject's agreement before you create one, and keeping your own record of it, is your responsibility. Your account team can answer questions about your contract terms. ## The three levels of access Consent is offered as **three increasing levels of access**. Each level removes friction from the flow, and each is unlocked for a progressively narrower set of accounts. Higher levels are more powerful and correspondingly more restricted. | Level | Flow | Who it's for | Availability | | - | - | - | - | | **1. Record via webcam** | The avatar subject records a short consent statement on camera through HeyGen's hosted consent page. | All customers | Available today | | **2. Upload a consent video** | You supply a pre-recorded consent video instead of recording live — more flexible, but consent is still explicitly captured. | Enterprise only | Available today — [contact sales](https://www.heygen.com/contact-us/sales) | | **3. Skip the consent flow** | Consent collection is waived entirely for accounts that have signed an indemnity agreement. | Enterprise only | [Contact sales](https://www.heygen.com/contact-us/sales) | ## Collect consent via API One endpoint drives both **Level 1** and **Level 2**: [`POST /v3/avatars/{group_id}/consent`](/reference/create-avatar-consent). By default it starts the webcam flow and returns a URL for the avatar subject — the link is valid for **24 hours**; enterprise accounts can pass a pre-recorded [`consent_video`](#upload-a-consent-video-level-2) instead. ### Record via webcam (Level 1) ```bash theme={null} curl -X POST "https://api.heygen.com/v3/avatars/group_xyz789/consent" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reroute_url": "https://heygen.com/consent-done" }' ``` | Parameter | Type | Required | Description | | - | - | - | - | | `reroute_url` | string | No | Redirect URL after the subject completes consent. Defaults to HeyGen's own completion page. Applies to the webcam flow only. | #### Response ```json theme={null} { "data": { "avatar_group": { "id": "group_xyz789", "name": "My Digital Twin", "consent_status": "pending", "looks_count": 1, "created_at": 1717000000 }, "url": "https://heygen.com/consent/abc123..." } } ``` | Field | Type | Description | | - | - | - | | `url` | string | Consent page URL. Send this to the avatar subject to complete approval. The link expires after **24 hours** — call the endpoint again to issue a fresh one. Returned for the webcam flow; when you upload a `consent_video`, the group is all you need, so the field is omitted. | | `avatar_group.consent_status` | string | Current consent status of the group: `pending` (awaiting the subject or under review), `accepted` (consent cleared), or `rejected` (resubmit). `null` for photo and prompt groups. | ### Upload a consent video (Level 2) Enterprise accounts can submit a pre-recorded consent statement directly — same endpoint, with `consent_video` in place of the flow parameters: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/avatars/group_xyz789/consent" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "consent_video": { "type": "url", "url": "https://example.com/consent.mp4" } }' ``` | Parameter | Type | Required | Description | | - | - | - | - | | `consent_video` | object | Yes, for this flow | The pre-recorded consent video, as `{"type": "url", "url": "https://..."}` or `{"type": "asset_id", "asset_id": "..."}` (see [Upload Assets](/docs/upload-assets) for uploading). | The video is submitted immediately for review — the subject never visits a consent page, so the response contains the `avatar_group` alone and no `url`. Because there is no hosted page in this flow, send `consent_video` on its own, without the webcam-flow parameters. This flow is enterprise-only — please [reach out to sales](https://www.heygen.com/contact-us/sales) or your account team to request access. #### What the consent video must contain Because the upload flow has no hosted page prompting the subject, you provide the statement yourself. Consent is validated **semantically**, so minor wording differences are fine, and **the statement can be in any language**. Translate it however fits your subject. The current recommended statement: > "Hey there! I'm speaking with LOTS of energy, while staying natural and confident. This helps HeyGen capture my voice, my expressions, and my motion, so my avatar can behave JUST like me in ANY video!" | Requirement | Detail | | - | - | | Consent statement | The subject clearly reads the statement above, in any language (translate as needed). | | Same person | Must be the same person as the training footage (identity-matched). | | One visible face | A single, clearly visible face on camera. | | Audio | Clearly audible. | | Length | Under 35 seconds. A short clip is enough, and longer clips are rejected. | | Format | A standard video file (for example MP4), supplied as a public HTTPS URL or an uploaded `asset_id`. | Record the consent clip separately from the training footage. Training footage works best at about 2 minutes of natural talking (see [Footage that trains well](/docs/avatar-from-video#footage-that-trains-well)), while the consent clip must be under 35 seconds. No passcode is required for the upload path — a spoken code applies only to the interactive webcam flow (Level 1). Check `consent_status` on the avatar group via [`GET /v3/avatars/{group_id}`](/reference/get-avatar-group) to know when consent is complete. It is `null` for a group created from a photo or a prompt, which has no consent step. The status belongs to the group, not the individual look. ## Track the result There is no webhook for consent. Poll [`GET /v3/avatars/{group_id}`](/reference/get-avatar-group) after submitting; review usually completes within a few minutes. The [`instant_avatar.success` and `instant_avatar.fail`](/docs/webhook-events) webhook events report twin training, not consent. When `consent_status` is `rejected`, the group's `status` is `failed` and `error.message` explains why in plain language you can show the subject: ```json theme={null} { "data": { "id": "group_xyz789", "name": "My Digital Twin", "consent_status": "rejected", "status": "failed", "error": { "code": "moderation_failed", "message": "Please make sure your consent video is under 35s." }, "looks_count": 1, "created_at": 1717000000 } } ``` Fix the issue and call the consent endpoint again. ### Resubmit after a rejection You can call [`POST /v3/avatars/{group_id}/consent`](/reference/create-avatar-consent) again for a group whose `consent_status` is `rejected`, with a new webcam link or a new `consent_video`. While a submitted consent video is still under review, the endpoint returns a `400` asking you to wait, and once consent is `accepted` it returns a `400` because consent is already complete. Send the `Idempotency-Key` header so a network retry does not create a second submission. ## Where consent fits Consent is one step in the digital twin flow. To create the avatar itself, see [Create Avatar](/docs/create-avatar); to browse the resulting characters and looks, see [Avatars](/docs/avatars) and [Avatar Looks](/docs/avatar-looks). # Photo to Avatar Source: https://heygen-1fa696a7.mintlify.app/docs/avatar-from-photo Turn one still image into a HeyGen avatar with POST /v3/avatars. The fastest creation path: no footage, no consent step, and the photo becomes the reference for every later look. A **photo avatar** is built from a single still image. It is the fastest way into the API: no recording session, and no consent step, since a group created from a photo reports `consent_status` of `null`. The image is still of a real person. HeyGen does not collect a consent record for photo avatars, so getting the subject's permission and keeping your own record of it is your responsibility, as [Avatar Consent](/docs/avatar-consent) explains. Questions about your contract terms go to your account team. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/avatars" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "photo", "name": "Sarah", "file": { "type": "url", "url": "https://example.com/sarah-headshot.png" } }' ``` `file` also takes `{ "type": "asset_id", "asset_id": "..." }` from [`POST /v3/assets`](/docs/upload-assets), or base64 content. Add `avatar_group_id` to add this photo as another look on a character you already have; `consent_status` is a property of the group, so the response reports whatever that group already carries. Save `avatar_item.id` from the response. That is the `avatar_id` for video creation. Poll [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look) until `status` leaves `processing`. ```bash theme={null} curl -X GET "https://api.heygen.com/v3/avatars/looks/$LOOK_ID" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` A `completed` look returns `preview_image_url` and the `supported_api_engines` it renders on. A `failed` one carries `error.code` of `training_failed` or `moderation_failed`, with the reason in `error.message`. Photo avatars accept `motion_prompt` and `expressiveness` on [`POST /v3/videos`](/reference/create-video), which direct how much the subject moves while speaking. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "avatar_id": "look_abc123", "voice_id": "voice_xyz789", "script": "Here is what changed in this release." }' ``` Pick a voice with [`GET /v3/voices`](/docs/voices/search-voices), or set one as the character default through [`PATCH /v3/avatars/{group_id}`](/reference/update-avatar-group) and leave `voice_id` out. The [Photo Avatar](/photo-avatar) recipe covers the render end to end. A clear, front-facing portrait in even lighting works best: whole head in frame, one person, as much resolution as you have. The image becomes the visual reference the character is remembered by, so choose the shot you want every later look to resemble. ## Give the character more looks The photo is the identity reference for everything that follows. Pass its look id as `avatar_id` to [Prompt to Avatar](/docs/avatar-from-prompt) to put the same person in a new outfit or setting, or apply a [Look Pack](/docs/avatar-look-packs) for a coordinated set in one call. New looks join the same character, and [`GET /v3/avatars/looks?group_id=...`](/reference/list-avatar-looks) lists them all. # Prompt to Avatar Source: https://heygen-1fa696a7.mintlify.app/docs/avatar-from-prompt Generate a synthetic avatar from a text description with POST /v3/avatars, or put an avatar you already have into a new outfit and setting by passing it as the visual reference. Describe a character and [`POST /v3/avatars`](/reference/create-avatar) with `type: "prompt"` generates it. The same call does double duty: pass an existing look as `avatar_id` and the prompt becomes a new outfit, setting, or style for that character instead of a new person. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/avatars" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "prompt", "name": "Space Commander", "prompt": "Woman in her early 30s, confident expression, short silver hair, dark blue flight suit with mission patches, spacecraft bridge with holographic displays, cinematic lighting", "aspect_ratio": "16:9" }' ``` `prompt` holds up to 1000 characters. `aspect_ratio` takes `16:9`, `9:16`, `1:1`, `4:5`, `5:4`, or `auto`, which lets the model choose and matches a reference image's ratio when one is given. Omitting it is the same as `auto`. The character is fully synthetic, so no real person is depicted and no consent step applies. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/avatars" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "prompt", "name": "Sarah, navy blazer", "prompt": "Wearing a navy blazer, modern office with plants behind her, warm natural light", "avatar_id": "look_abc123" }' ``` The referenced look's image conditions the generation, so the person stays recognisable while the prompt changes everything around them. The result saves to that character automatically. This works from any starting point: a [digital twin](/docs/avatar-from-video), a [photo avatar](/docs/avatar-from-photo), or an earlier prompt avatar. Any of the resulting looks can be the reference for the next one. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/avatars" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "prompt", "name": "Sarah, studio portrait", "prompt": "Wearing the coat from the reference, plain studio backdrop, soft key light", "avatar_id": "look_abc123", "reference_images": [ { "type": "url", "url": "https://example.com/coat-reference.png" } ] }' ``` Up to three `reference_images` add wardrobe, style, or setting guidance, each given as a url, an `asset_id` from [`POST /v3/assets`](/docs/upload-assets), or base64 content. Use them alone to steer a brand-new character, or layer them on an `avatar_id` reference. A crop showing only the garment keeps the reference about wardrobe and fabric, leaving the face to the `avatar_id` reference. ## Reference and destination Two fields decide where the look comes from and where it lands: | `avatar_id` | `avatar_group_id` | Visual reference | Saved to | | - | - | - | - | | absent | absent | the prompt alone | a new character | | absent | set | the prompt alone | that character | | set | absent | the referenced look's image | the referenced look's character | | set | set | the referenced look's image | `avatar_group_id`, which the look must belong to | **Changed behavior (June 2026):** `avatar_group_id` used to condition the generation on one of the group's looks. It now only controls where the result is saved. Pass the base look's id as `avatar_id` for character consistency. See the [changelog](/changelog). ## Writing the prompt Name the age, expression, wardrobe, setting, and light. Camera direction is obeyed when it is explicit, as in "turned three-quarters toward camera left, with clean space on the left of the frame". Texture words such as "natural matte skin, visible pores, no retouching" pull toward realism, and naming a lens or aperture pushes further. Details you leave out get invented plausibly, including text on badges and props, so read the generated image before writing a script around it. [Writing Effective Video Prompts](/writing-effective-video-prompts) goes deeper. ## Wait for it, then render Poll [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look) until `status` leaves `processing`. A `completed` look returns `preview_image_url` and its `supported_api_engines`. Then pass the look id as `avatar_id` to [`POST /v3/videos`](/reference/create-video) with a `voice_id` from [`GET /v3/voices`](/docs/voices/search-voices). Prompt avatars render on [Avatar V, IV, and III](/models). Check `supported_api_engines` before requesting a specific engine. A referenced `avatar_id` that does not exist returns `404` with `error.code` of `avatar_not_found`. One that has no usable image, or that is not in the `avatar_group_id` you gave, returns `400` with `invalid_parameter`. # Video to Avatar Source: https://heygen-1fa696a7.mintlify.app/docs/avatar-from-video Train a digital twin from video footage of a person with POST /v3/avatars. Covers footage that trains well, the voice cloned alongside it, consent, and polling to completed. A **digital twin** is trained from footage of a real person, and clones a voice from the same recording so the twin speaks in the subject's own voice. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/avatars" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "digital_twin", "name": "Sarah", "file": { "type": "url", "url": "https://example.com/sarah-footage.mp4" } }' ``` `file` also takes `{ "type": "asset_id", "asset_id": "..." }` from [`POST /v3/assets`](/docs/upload-assets), or base64 content. `avatar_group_id` decides which character the twin belongs to: * **Pass an existing `avatar_group_id`** to add the twin as another look of a character you already have. Only do this for the same person, since consent is tracked per group. * **Leave it out** to create a new avatar group (a new character) for this person. The new group id is returned as `avatar_group.id`. A `url` input is fetched when you call the endpoint and is capped at 32 MB, the same as [`POST /v3/assets`](/docs/upload-assets). Signed or presigned HTTPS URLs work as long as they are reachable at that moment, but they do not raise the cap. Base64 input is capped at 16 MB after decoding. For recommended footage (about 2 minutes at 1080p), use [direct upload](/docs/upload-assets#upload-large-files-direct-upload), then pass `{ "type": "asset_id", "asset_id": "asset_abc123" }`. An `asset_id` stays usable until you delete it, so reuse the same one if you retry creation. Keep two values from the response: `avatar_item.id` is the `avatar_id` you render with, and `avatar_group.id` is what the consent flow acts on. Store the group id per person, since it is also the `avatar_group_id` you pass to add more looks of them later. Poll [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look) until `status` leaves `processing`; training usually completes within a few minutes. ```bash theme={null} curl -X GET "https://api.heygen.com/v3/avatars/looks/$LOOK_ID" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` To be notified instead of polling, subscribe a [webhook endpoint](/docs/webhooks) to the `instant_avatar.success` and `instant_avatar.fail` [events](/docs/webhook-events). A `failed` look carries an `error` object: `training_failed` when the footage could not be trained, `moderation_failed` when it did not pass content review. The reason is in `error.message`. [`POST /v3/avatars/{group_id}/consent`](/reference/create-avatar-consent) returns a link, valid for 24 hours, that the subject opens to record a short statement on camera. Enterprise workspaces can upload a pre-recorded consent video or have the step waived. An uploaded consent video must be under 35 seconds and show the same person as the footage. Record it as its own short clip, separate from the training footage. See [Avatar Consent](/docs/avatar-consent#what-the-consent-video-must-contain) for what it must contain. [Avatar Consent](/docs/avatar-consent) has the three levels of access and the full flow. Render once the look reports `status: "completed"` and the group has cleared consent. No other publish step is needed. Pass the look id as `avatar_id`. Omitting `voice_id` falls back to the character's default voice, which is the group's linked voice when the twin joined an existing group. Pass `avatar_item.default_voice_id` from the create response to speak in the voice cloned from this footage. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "avatar_id": "look_abc123", "voice_id": "voice_cloned123", "script": "Thanks for joining today. Here is what shipped this week." }' ``` The [Digital Twin](/generate-avatar-video) recipe walks the render end to end, and [Models](/models) compares the engines a twin supports. ## Footage that trains well | Attribute | Requirement | Recommended | | - | - | - | | Duration | 15 to 600 seconds | About 2 minutes of natural talking | | Resolution | Long edge at least 640 px, at most 4096 px (4K) | 1080p | | Format | mp4 or webm | mp4 | | People in frame | One, facing camera for the whole recording | | | Audio | Clear, audible speech throughout | Quiet room, no background music | | Framing | The face stays in frame from first frame to last | | Longer footage with varied natural speech gives the model more to learn from. About 2 minutes at 1080p gives the best twin quality. A clip like that is usually larger than 32 MB, so upload it with [direct upload](/docs/upload-assets#upload-large-files-direct-upload), which accepts video files up to 200 MB, and pass the `asset_id`. If your recording is over 200 MB, export it at a lower bitrate before uploading. ## The cloned voice Creating a digital twin clones one voice from the training footage automatically. There is no separate voice request and no opt-in step. The id is returned immediately as `avatar_item.default_voice_id`, while the avatar and the voice finish processing. The cloned voice is its own resource. Deleting the twin's look or group does not delete it; remove it with [`DELETE /v3/voices/{voice_id}`](/reference/delete-a-voice). Two voice ids travel with a twin and they can differ. `avatar_item.default_voice_id` is the voice cloned from this request's footage. `avatar_group.default_voice_id` is the effective voice for the character: a linked group voice takes precedence, falling back to the new look's clone. Set the character's voice deliberately by sending `default_voice_id` to [`PATCH /v3/avatars/{group_id}`](/reference/update-avatar-group). Any voice in your workspace works, including [cloned voices](/docs/voices/heygen-voice-instant-clone). To give the same person more outfits and settings, generate looks from [a prompt](/docs/avatar-from-prompt) or a [Look Pack](/docs/avatar-look-packs). # Look Packs and Templates Source: https://heygen-1fa696a7.mintlify.app/docs/avatar-look-packs Generate a coordinated set of looks for an avatar you already own with POST /v3/avatars/looks. Apply a Look Pack for a full set, or a single look template to restyle one look. [`POST /v3/avatars/looks`](/reference/create-avatar-looks) dresses an avatar you already own in curated looks. Give it one existing look as the identity reference and a curated `template_id`, and the new looks join that look's character. Where prompting gives you [one look at a time in your own words](/docs/avatar-from-prompt), a pack gives you a whole coordinated set in one call. | `type` | What it applies | How many looks | | - | - | - | | `look_pack` | A Look Pack: a curated set built around one theme. | The pack decides. Every pack in the gallery today produces five. | | `template` | A single look template, restyling the reference look into it. | Two, in the template's aspect ratio. | Eight packs from the gallery. **Style packs**, the first four, keep the setting neutral and move the wardrobe and palette. **Role packs**, the last four, put your avatar into a job with the wardrobe and setting that go with it. Template ids are 32-character hex strings, and every pack ships as a female and a male variant, so pick the one matching your avatar. | Look Pack | Female | Male | | - | - | - | | Modern Corporate | `5a8e6be843ee4321a014946bf62aa322` | `5e659e76755d47fd9160a2d7dedce124` | | Stylish Business-Casual | `e1650a9029d04ea1a350421c163295fd` | `a5f8e0c0cb654c1a83c695629b173af5` | | Scandinavian Minimal | `e7b000e2cb0d44f1a2d2f291a38b395a` | `9600f78f27bb49d792e1c56ce624b929` | | Cool Tech / Slate | `d7ad40ec97d4459a92b33398b57caf4e` | `fbd03fc3603a4e3caff594f5743a1733` | | Healthcare | `14f4070bd5a04eb6b1f82ae087cd0f30` | `ce706bfa8556401e8c4401765102a455` | | Keynote Power Presenter | `b0cae4fbae384ab9ab9a8e73ba504be8` | `71e84769162d4f4186563ad9e0fc45b8` | | Fitness | `935ea0b7cdde4e6eb5a198a3d666f677` | `daeb8b15c92a4acf9ffb437ba5534458` | | Automotive Dealership | `6f183d726933457c9035fc0aa4e9c0c3` | `8595c78b466c4e51802731229c59b6e7` | The full gallery is browsable in the [HeyGen app](https://app.heygen.com), covering real estate, legal, teaching, construction and more. Enterprise workspaces can have custom packs built and assigned to them, which then work here exactly like the rest. To have one assigned, [contact sales](https://www.heygen.com/contact-us/sales). ```bash theme={null} # Reuse this key when retrying this exact request in the same shell. # For a different Look Pack request, unset LOOK_PACK_KEY first. LOOK_PACK_KEY="${LOOK_PACK_KEY:-$(uuidgen)}" curl -X POST "https://api.heygen.com/v3/avatars/looks" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $LOOK_PACK_KEY" \ -d '{ "type": "look_pack", "reference_look_id": "look_abc123", "template_id": "5a8e6be843ee4321a014946bf62aa322", "aspect_ratio": "16:9" }' ``` Packs generate at `16:9`. Pass `aspect_ratio: "9:16"` and the delivered frames are center-cropped to portrait as the final step, which is worth knowing when you plan the framing of the reference look. ```bash theme={null} # Reuse this key when retrying this exact request in the same shell. # For a different single-template request, unset SINGLE_TEMPLATE_KEY first. SINGLE_TEMPLATE_KEY="${SINGLE_TEMPLATE_KEY:-$(uuidgen)}" curl -X POST "https://api.heygen.com/v3/avatars/looks" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $SINGLE_TEMPLATE_KEY" \ -d '{ "type": "template", "reference_look_id": "look_abc123", "template_id": "b30c83d99cec48e09c8f5b07ed8aa38c" }' ``` A single template returns two looks in the template's own aspect ratio, so `aspect_ratio` does not apply here. Single templates are one card each rather than a themed set. The id above is Business Casual for a female avatar. The male one is `2dc326030e2e4471babcc9eda56131b6`, and Podcast is `00c49daa4eb94386a0c12508043bab17` for female and `0e52ceafe4d5456fb83c1e39899ff76a` for male. `reference_look_id` is any look you own, from [a digital twin](/docs/avatar-from-video), [a photo](/docs/avatar-from-photo), or [a prompt](/docs/avatar-from-prompt). Its character is where the results land, so there is no group field to set. The response names the character the looks joined and gives one id per look being generated: ```json theme={null} { "data": { "group_id": "group_xyz789", "looks": [ { "id": "look_def456" }, { "id": "look_ghi789" }, { "id": "look_jkl012" }, { "id": "look_mno345" }, { "id": "look_pqr678" } ] } } ``` Each id starts at `processing`. Poll [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look) until it reports `completed`, at which point the preview is available and the id is usable as `avatar_id` for [`POST /v3/videos`](/reference/create-video), or `failed`, where `error.message` gives the reason. To see every look on the character at once, including the originals, call [`GET /v3/avatars/looks?group_id=group_xyz789`](/reference/list-avatar-looks). Send an `Idempotency-Key` header so a retry after a network timeout cannot start a second generation. The examples preserve their keys when rerun in the same shell; in an application, persist the key with the request so a restart can retry with the same key and body. Use a new key for each different request. While the original request is still in flight, a retry returns `409` with `error.code` of `request_in_progress`: wait, then retry. Once the original finishes, a retry within 24 hours replays its response. ## Template access A template id your workspace has access to resolves normally. One that belongs to another workspace returns `403`, and an id that matches no template returns `400`. Sending a pack id with `type: "template"`, or a single template id with `type: "look_pack"`, returns a `400` naming the method that would have taken it. # Avatar Looks Source: https://heygen-1fa696a7.mintlify.app/docs/avatar-looks Browse and select avatar looks (outfits, poses, hair styles) for any HeyGen avatar group via the Avatar Looks API. A **look** is one outfit/pose/style for a character ([avatar group](/docs/avatars)). It's the value you pass as `avatar_id` to video creation. List via [`GET /v3/avatars/looks`](/reference/list-avatar-looks), fetch one via [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look), or [Update](/reference/update-avatar-look) / [Delete](/reference/delete-avatar-look) your own. To create a new look, see [Create Avatar](/docs/create-avatar). ## Quick Example ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/avatars/looks?avatar_type=photo_avatar&ownership=public&limit=5" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ```json Response theme={null} { "data": [ { "id": "look_def456", "name": "Monica - Business Casual", "avatar_type": "photo_avatar", "group_id": "group_abc123", "gender": "female", "preview_image_url": "https://files.heygen.ai/look/preview_def456.jpg", "preview_video_url": "https://files.heygen.ai/look/preview_def456.mp4", "default_voice_id": "voice_xyz789", "tags": ["business", "casual", "female"], "supported_api_engines": ["avatar_iv", "avatar_v"], "status": "completed" } ], "has_more": true, "next_token": "eyJsYXN0X2lkIjoiNDU2In0" } ``` ## Query Parameters | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `group_id` | string | No | — | Filter looks to a specific avatar group. Returns only looks belonging to this character. | | `avatar_type` | string | No | all | `"studio_avatar"`, `"digital_twin"`, or `"photo_avatar"`. | | `ownership` | string | No | all | `"public"` for HeyGen presets or `"private"` for your own. Omit for both. | | `limit` | integer | No | `20` | Results per page (1–50). | | `token` | string | No | — | Opaque cursor token for the next page. | ## Response Fields Each look in the `data` array contains: | Field | Type | Description | | - | - | - | | `id` | string | Unique look identifier. **This is the value to pass as `avatar_id`** to [`POST /v3/videos`](/reference/create-video) or [`POST /v3/video-agents`](/reference/create-video-agent-session). | | `name` | string | Display name of the look. | | `avatar_type` | string | One of `"studio_avatar"`, `"digital_twin"`, or `"photo_avatar"`. Determines engine and parameter compatibility. | | `group_id` | string or null | ID of the avatar group (character) this look belongs to. | | `gender` | string or null | Gender of the avatar. | | `preview_image_url` | string or null | URL to a preview image. | | `preview_video_url` | string or null | URL to a preview video. | | `default_voice_id` | string or null | Default voice ID for this look — see [Browse Voices](/docs/voices/search-voices). | | `tags` | array | Tags associated with the look (e.g. `["business", "casual"]`). | | `supported_api_engines` | array | Engine values this look accepts for [`POST /v3/videos`](/reference/create-video): `"avatar_iii"`, `"avatar_iv"`, and/or `"avatar_v"`. Check this array before requesting a specific `engine` — requesting an engine that isn't listed returns `invalid_parameter`. | | `status` | string or null | Training status: `"processing"`, `"completed"`, or `"failed"`. Only present for private avatars. | ## Avatar Types The `avatar_type` field determines what features and parameters are available when creating a video: | Type | Description | | - | - | | `studio_avatar` | Pre-built HeyGen studio avatars with fixed poses and backgrounds. | | `digital_twin` | Avatars created from video footage. Support background removal by default on new twins. | | `photo_avatar` | Avatars generated from a single photo. Support `motion_prompt` and `expressiveness` in video creation. | ## Get a Single Look Full schema: [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look). To rename or retag, use [`PATCH /v3/avatars/looks/{look_id}`](/reference/update-avatar-look); to remove, [`DELETE`](/reference/delete-avatar-look). ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/avatars/looks/look_def456" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ```json Response theme={null} { "data": { "id": "look_def456", "name": "Monica - Business Casual", "avatar_type": "photo_avatar", "group_id": "group_abc123", "gender": "female", "preview_image_url": "https://files.heygen.ai/look/preview_def456.jpg", "preview_video_url": "https://files.heygen.ai/look/preview_def456.mp4", "default_voice_id": "voice_xyz789", "tags": ["business", "casual", "female"], "supported_api_engines": ["avatar_iv", "avatar_v"], "status": "completed" } } ``` ## Filtering by Group To see all outfits and styles for a specific character, pass its `group_id`: ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/avatars/looks?group_id=group_abc123" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ## Pagination If `has_more` is `true`, pass the `next_token` value as the `token` query parameter to fetch the next page. ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/avatars/looks?token=eyJsYXN0X2lkIjoiNDU2In0" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ## Using a Look in Video Creation Once you have a look `id`, pass it as `avatar_id`. When you request a specific rendering engine, pick one from the look's `supported_api_engines` array — for example, pass `"engine": {"type": "avatar_v"}` when the array includes `"avatar_v"`. Omitting `engine` uses Avatar IV. See [Models](/models) for how the engines compare. ```bash "Video Agent" theme={null} curl -X POST "https://api.heygen.com/v3/video-agents" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "A product demo for our new app", "avatar_id": "look_def456" }' ``` ```bash "Avatar Video" theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "avatar_id": "look_def456", "voice_id": "voice_xyz789", "script": "Welcome to our product walkthrough." }' ``` # Avatar Groups Source: https://heygen-1fa696a7.mintlify.app/docs/avatars Generate AI avatar videos via the HeyGen API. Choose from 500+ stock avatars or train custom digital twins. Includes avatar looks, voices, and instant. Avatars come in two layers: a **group** is a character (e.g. "Monica"); each group has one or more **looks** (outfits, poses, styles). This page is about listing and inspecting groups via [`GET /v3/avatars`](/reference/list-avatar-groups) and [`GET /v3/avatars/{group_id}`](/reference/get-avatar-group). To pick a specific look to render with, see [Avatar Looks](/docs/avatar-looks). To train your own avatar, see [Create Avatar](/docs/create-avatar). ## Quick Example ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/avatars?ownership=public&limit=5" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ```json Response theme={null} { "data": [ { "id": "group_abc123", "name": "Monica", "gender": "female", "preview_image_url": "https://files.heygen.ai/avatar/preview_abc123.jpg", "preview_video_url": "https://files.heygen.ai/avatar/preview_abc123.mp4", "looks_count": 3, "default_voice_id": "voice_xyz789", "consent_status": null, "status": "completed", "created_at": 1711382400 } ], "has_more": true, "next_token": "eyJsYXN0X2lkIjoiMTIzIn0" } ``` ## Query Parameters | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `ownership` | string | No | all | `"public"` for HeyGen's preset avatars or `"private"` for your own. Omit for both. | | `limit` | integer | No | `20` | Results per page (1–50). | | `token` | string | No | — | Opaque cursor token for the next page. | ## Response Fields Each avatar group in the `data` array contains: | Field | Type | Description | | - | - | - | | `id` | string | Unique group identifier. Pass to [`GET /v3/avatars/{group_id}`](/reference/get-avatar-group) for details, or use as `group_id` in [`GET /v3/avatars/looks`](/reference/list-avatar-looks) to filter. | | `name` | string | Display name of the avatar character. | | `gender` | string or null | Gender of the avatar. | | `preview_image_url` | string or null | URL to a preview image. | | `preview_video_url` | string or null | URL to a preview video. | | `looks_count` | integer | Number of looks (outfits/styles) available for this character. | | `default_voice_id` | string or null | Default voice ID for this avatar — pair with [Browse Voices](/docs/voices/search-voices) or [`GET /v3/voices`](/reference/list-voices). | | `consent_status` | string or null | Consent status for the group: `"pending"`, `"accepted"`, or `"rejected"`. `null` means consent is not required, as for photo and prompt groups, which have no consent step. | | `status` | string or null | Group status, driven by consent and review: `"pending_consent"`, `"completed"`, or `"failed"` (consent rejected, reason in `error.message`). Training progress is reported on each look's `status`, see [Avatar Looks](/docs/avatar-looks). Only present for private avatars. | | `created_at` | integer | Unix timestamp of creation. | ## Get a Single Avatar Group Full schema: [`GET /v3/avatars/{group_id}`](/reference/get-avatar-group). To change the default voice of an avatar you own, send `default_voice_id` to [`PATCH /v3/avatars/{group_id}`](/reference/update-avatar-group); any voice available to your workspace works, including imported clones. Public avatars cannot be updated. To remove a character you own and every look in it, call [`DELETE /v3/avatars/{group_id}`](/reference/delete-avatar-group). Source assets and any cloned voice are kept; see [Delete an avatar](/docs/create-avatar#delete-an-avatar). ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/avatars/group_abc123" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ```json Response theme={null} { "data": { "id": "group_abc123", "name": "Monica", "gender": "female", "preview_image_url": "https://files.heygen.ai/avatar/preview_abc123.jpg", "preview_video_url": "https://files.heygen.ai/avatar/preview_abc123.mp4", "looks_count": 3, "default_voice_id": "voice_xyz789", "consent_status": null, "status": "completed", "created_at": 1711382400 } } ``` ## Pagination If `has_more` is `true`, pass the `next_token` value as the `token` query parameter to fetch the next page. ```bash curl theme={null} curl -X GET "https://api.heygen.com/v3/avatars?token=eyJsYXN0X2lkIjoiMTIzIn0" \ -H "X-Api-Key: $HEYGEN_API_KEY" ``` ## Avatars vs. Looks Avatar **groups** represent characters (e.g. "Monica"). Each group has one or more **looks** — different outfits, poses, or styles for that character. When creating a video, you pass a **look ID** (not a group ID) as the `avatar_id`. See [Avatar Looks](/docs/avatar-looks) for how to browse and select looks. # Brand Glossary Source: https://heygen-1fa696a7.mintlify.app/docs/brand-glossary Control how HeyGen says and translates your custom terms. Create a brand glossary over the API with pronunciations, do-not-translate terms, and forced translations, then pass its brand_glossary_id on videos and translations. A brand glossary holds your terms and how HeyGen should treat them: how the voice pronounces them, and whether translation keeps or replaces them. Pass its `brand_glossary_id` on a video or translation request to apply it. Glossaries can be authored in the HeyGen app under **Brand Kit** or over the API; the API is how you give each of your own customers their own vocabulary. **API reference:** [Create Brand Glossary](/reference/create-brand-glossary) · [Update Brand Glossary](/reference/update-brand-glossary) · [Get Brand Glossary](/reference/get-brand-glossary) · [List Brand Glossaries](/reference/list-brand-glossaries) · [Delete Brand Glossary](/reference/delete-brand-glossary) ## Three lists | List | Applies in | Effect | | - | - | - | | `terms` | Generation: [Studio](/studio-videos), [Templates](/templates), `avatar` and `image` videos, [Video Agent](/docs/video-agent) | The voice says `pronunciation` instead of `term`. Captions keep the original spelling. | | `do_not_translate_terms` | Translation: [video translation](/docs/video-translate) over the API, and a Studio or Video Agent draft translated in the HeyGen app | `term` stays untranslated in every target language. | | `forced_translations` | Translation, as above | `term` becomes its exact `translation` in every target language. | Translation rules take effect only through a translation feature; plain video generation and text-to-speech use `terms` alone. Matching is case-insensitive. A `translation` is inserted verbatim with no inflection, so pick a form that reads correctly on its own. If a term is in both `do_not_translate_terms` and `forced_translations`, it stays untranslated. ## Create [`POST /v3/brand-glossaries`](/reference/create-brand-glossary) takes a `name` (1–64 characters, unique per workspace) and any of the three lists. The `201` response is the full glossary, usable immediately. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/brand-glossaries" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "name": "Acme Product Names", "terms": [ { "term": "HeyGen", "pronunciation": "hey-jen" }, { "term": "2024", "pronunciation": "twenty twenty-four" } ], "do_not_translate_terms": [ { "term": "HeyGen" } ], "forced_translations": [ { "term": "CEO", "translation": "Geschäftsführer" } ] }' ``` A pronunciation is a respelling, written the way you would coach someone to say it: `hey-jen`, `hyper frames`, `twenty twenty-four`. Add a term once you have heard it come out wrong. ## Language-specific rules A pronunciation or forced translation can apply to one language. Add `pronunciation_language` to a `terms` entry or `target_language` to a `forced_translations` entry: ```json theme={null} { "terms": [ { "term": "Acme", "pronunciation": "ack-mee" }, { "term": "Acme", "pronunciation": "ak-may", "pronunciation_language": { "scope": "specific", "code": "fr" } } ], "forced_translations": [ { "term": "Hello", "translation": "Bonjour", "target_language": { "scope": "specific", "code": "fr" } } ] } ``` Here French videos say `ak-may` and every other language says `ack-mee`. * Omit the field, or send `{ "scope": "all" }`, for a rule that applies to all languages. * `code` is a base language code: `fr`, `es`, `de`. * A language-specific rule takes precedence over an all-languages rule for that language. * The same term can appear once per language in each list. * `do_not_translate_terms` always applies to all languages. Responses include the language field only on language-specific rules, so all-languages entries keep their existing shape. ## Update [`PATCH /v3/brand-glossaries/{brand_glossary_id}`](/reference/update-brand-glossary) replaces each field independently. Omit a field to leave it alone, send a list to replace it in full, send `[]` to clear it. Send at least one field. A list replaces that kind of rule across every language, so send language-specific entries back with their language fields. A term repeated for the same language within one list returns `409 conflict`. To add a single entry, read the glossary, append, and send the whole list back: ```python theme={null} import requests base = "https://api.heygen.com/v3/brand-glossaries/8c2d5e91a47b4f3c8d1e6a9b2f5c7d40" headers = {"X-Api-Key": HEYGEN_API_KEY} current = requests.get(base, headers=headers).json()["data"]["terms"] current.append({"term": "Reformer", "pronunciation": "ree-form-er"}) requests.patch(base, headers=headers, json={"terms": current}) ``` Tone settings are managed in the HeyGen app under **Brand Kit** and are untouched by `PATCH`. ## Read [`GET /v3/brand-glossaries`](/reference/list-brand-glossaries) lists names, IDs, and timestamps, paged with `limit` and `token`. [`GET /v3/brand-glossaries/{brand_glossary_id}`](/reference/get-brand-glossary) returns the full glossary; `do_not_translate_terms` and `forced_translations` appear whenever the glossary has rules of that kind, including rules set in the app. Read it when a video pronounces or translates a term unexpectedly. ## Delete [`DELETE /v3/brand-glossaries/{brand_glossary_id}`](/reference/delete-brand-glossary) removes a glossary. Videos and translations already created with it are unaffected. This cannot be undone. ## Apply Pass `brand_glossary_id` at the top level of the request, one glossary per call: ```bash theme={null} curl -X POST "https://api.heygen.com/v3/videos" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "avatar", "brand_glossary_id": "8c2d5e91a47b4f3c8d1e6a9b2f5c7d40", "avatar_id": "YOUR_AVATAR_ID", "script": "HeyGen shipped this in 2024.", "voice_id": "YOUR_VOICE_ID" }' ``` [On Brand](/docs/on-brand) lists every endpoint that accepts the field. On [`POST /v3/video-agents`](/reference/create-video-agent-session) it is set once per session. In a [translation batch](/batch-video-translations) or [video batch](/batch-videos) it is set per item. Scenes with their own `audio_url` or `audio_asset_id` have no synthesis step, so `terms` leaves them unchanged. An unknown `brand_glossary_id` fails the request with `400` before rendering, so no credits are spent. Glossaries are workspace-scoped: an ID visible in the app is rejected if the API key belongs to another workspace. ## Errors | Status | Code | Meaning | | - | - | - | | `400` | `invalid_parameter` | A field is out of range. `param` names the field. | | `404` | `resource_not_found` | No glossary with that ID in this workspace. | | `409` | `conflict` | A glossary with this name already exists, a term is repeated for the same language within one list, or the glossary changed during the update. Read it again and retry. | | `409` | `request_in_progress` | A request with this `Idempotency-Key` is still running. Retry shortly. | | `429` | `rate_limit_exceeded` | Retry after `Retry-After`. | | `503` | `service_unavailable` | Temporary. Retry shortly. | ## Migrating from v1 brand voice `brand_voice_id` is the deprecated former name of `brand_glossary_id`. It is still accepted on [`POST /v3/templates/{template_id}`](/reference/generate-video-from-template), [`POST /v3/video-translations`](/reference/create-video-translation), and [`POST /v3/video-translations/proofreads`](/reference/create-proofread-session). [`POST /v3/videos`](/reference/create-video) accepts `brand_glossary_id` only. The legacy `POST /v1/brand_voice/{brand_voice_id}` lists map to the v3 glossary as follows, ahead of the [October 31, 2026 v1/v2 sunset](/endpoint-version-comparison): | v1 field | v1 shape | v3 field | v3 shape | | - | - | - | - | | `vocabulary` | `[["HeyGen", "hey-jen"]]` | `terms` | `[{ "term": "HeyGen", "pronunciation": "hey-jen" }]` | | `blacklist` | `["HeyGen"]` | `do_not_translate_terms` | `[{ "term": "HeyGen" }]` | | `whitelist` | `[["CEO", "Geschäftsführer"]]` | `forced_translations` | `[{ "term": "CEO", "translation": "Geschäftsführer" }]` | | `tones`, `tone` | — | — | Managed in the HeyGen app under **Brand Kit**. | # Brand Kits Source: https://heygen-1fa696a7.mintlify.app/docs/brand-kits Create a brand kit over the API by importing a website's colors, fonts, and logos, then pass its brand_kit_id to Video Agent so every generated video ships on-brand. A brand kit is a saved set of brand colors, fonts, and logos. Pass its `brand_kit_id` to the [Video Agent](/docs/video-agent) and the agent applies it to everything it builds — scene backgrounds, text treatments, chart palettes, logo placement — so output ships on-brand without describing your brand in the prompt. Author a kit in the HeyGen app under **Brand Kit**, or import one from a public website over the API. Either way you get a `brand_kit_id`, and a kit created over the API stays editable in the app. A brand kit governs how a video **looks**. To control how your terms are **spoken**, pair it with a [brand glossary](/docs/brand-glossary) — see [On Brand](/docs/on-brand). **API reference:** [Create Brand Kit](/reference/create-brand-kit) · [Get Brand Kit](/reference/get-brand-kit) · [List Brand Kits](/reference/list-brand-kits) · [Update Brand Kit](/reference/update-brand-kit) · [Delete Brand Kit](/reference/delete-brand-kit) · [Create Session](/reference/create-video-agent-session) · [Send Message](/reference/send-message-or-request-revision) ## Import a kit from a website [`POST /v3/brand-kits`](/reference/create-brand-kit) visits a public URL, imports the logos, colors, and font files it finds, and assembles them into a kit. This is the path to on-brand output for a workspace that has never opened the HeyGen app — onboard a customer by their own domain and generate their first branded video in the same run. By calling this endpoint you confirm you have the rights and licenses necessary to upload, store, and use the imported assets in HeyGen. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/brand-kits" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "url": "https://www.acme.com", "name": "Acme Corp — Primary" }' ``` `201` returns `{ "brand_kit_id": "…", "status": "loading" }` as soon as the record exists, before any asset has been imported. The ID is usable immediately, including on a Video Agent request. Send an `Idempotency-Key` so a retry is free: a repeat within 24 hours replays the original response instead of importing the same site twice. A retry that arrives while the first is still in flight gets a `409` with `request_in_progress`. The key is matched on its own rather than against the body, so reusing one with a different `url` replays the first kit instead of importing the new site. ## Read a kit and wait for the import [`GET /v3/brand-kits/{brand_kit_id}`](/reference/get-brand-kit) returns the kit's `colors`, `logos`, and `fonts` alongside the `color_roles`, `logo_roles`, and `font_roles` that say which of them plays which part. Poll every 2 to 5 seconds until `status` is `completed`, and read the palette only then. A website import usually settles in under two minutes; fonts are typically last to land, so a kit that already has colors and a logo may still be `loading`. Both the collections and the roles are provisional until then. Two habits keep a kit reader robust once a human starts editing the kit in the app: * **A role can name a color outside `colors`.** The palette lists the brand's core swatches; a role such as `tertiary` may point at a supporting surface color that isn't one of them. Read `color_roles` for roles and `colors` for the palette rather than deriving one from the other. * **A role can name an asset that no longer exists.** Deleting a logo or font leaves the role that referenced it in place, so look the `logo_id` or `font_id` up in `logos` or `fonts` and handle the miss. `status` is `error` when the import failed. The kit still exists and keeps whatever was assembled first, so it remains usable — check the collections to see what made it through, and fill the gaps in the app. ## List your brand kits [`GET /v3/brand-kits`](/reference/list-brand-kits) returns the kits in the authenticated workspace — the endpoint to reach for when you need the ID of a kit somebody else created in the app. Page with `limit` and `token`; the response carries `data`, `has_more`, and `next_token` at the top level. The listing is a summary: it carries `logo_url` for the primary logo and the color list, but not fonts or role assignments. Read a kit by ID for those. `logo_url` is `null` when a kit defines colors and fonts but no logo, which is common. ## Correct a kit's roles Creation assigns roles automatically, with no human in the loop, so [`PATCH /v3/brand-kits/{brand_kit_id}`](/reference/update-brand-kit) is how a caller fixes a slot the importer read differently than you would. It sets the kit's **name** and its **role assignments** — `color_roles` (`primary`, `secondary`, `tertiary`, `accent`), `logo_roles` (`primary`), and `font_roles` (`title_text` and `body_text`, each with a `font_id` and `weight`). The colors, logos, and font files themselves are curated in the HeyGen app. ```bash theme={null} curl -X PATCH "https://api.heygen.com/v3/brand-kits/3f9a1c47b8e24d5fa0c61e8b7d24f6a1" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "color_roles": { "primary": "#FF5733", "secondary": "#1A1A1A" } }' ``` An omitted field is left unchanged, so send only what you are changing. A role object you do send is **replaced in full rather than merged** — a role left out of it is cleared, which is why the example above clears `tertiary` and `accent` — and `{}` clears a group outright and stays cleared. Clearing is `{}`, never `null` — a `null` on any field is rejected. Generated clients serialize with `exclude_none` and drop a `null` before it reaches the wire, so honoring it would clear the role over raw HTTP and silently do nothing through an SDK. Role edits need an assembled kit and return `409` while `status` is `loading` or `error`. Renaming is allowed at any time, so a kit can be titled the moment its import starts. ## Delete a kit [`DELETE /v3/brand-kits/{brand_kit_id}`](/reference/delete-brand-kit) removes a kit along with the colors, logos, and fonts it holds, and cannot be undone — the API has no way to restore one. A kit that is still assembling can be deleted at any time. Videos already generated with the kit keep their branding, since the colors were applied at render time. A Video Agent session still configured with the deleted `brand_kit_id` reports it as invalid on its next use. ## Apply a brand kit to a video Pass `brand_kit_id` when you create the session. It works in both `generate` and `chat` modes. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/video-agents" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": "Create a 45-second explainer about our Q3 product launch.", "brand_kit_id": "3f9a1c47b8e24d5fa0c61e8b7d24f6a1", "orientation": "landscape" }' ``` In an [interactive session](/docs/interactive-sessions), `brand_kit_id` is also accepted on each follow-up message, so you can re-brand without starting over — useful when producing the same video for several brands or sub-brands. `brand_kit_id` and `style_id` are independent and compose: a [style](/docs/styles-and-references) picks the look, the brand kit makes that look yours. ## Errors | Status | Code | Meaning | | - | - | - | | `400` | `invalid_parameter` | On an import, the URL could not be read — `"URL is not supported"`. Check that it is a public page reachable without a login. On an update, `"Provide at least one field to update."` means the body arrived empty. | | `403` | `forbidden` | Your workspace settings do not allow creating brand kits. | | `403` | `resource_access_denied` | A kit shared into your workspace by another workspace is readable, and the workspace that owns it holds updates and deletion. | | `404` | `resource_not_found` | No brand kit with that ID in the authenticated workspace. | | `409` | `request_in_progress` | A request with this `Idempotency-Key` is still running. Retry shortly. | | `409` | `brand_kit_not_ready` | A role edit reached a kit that is not assembled — `status` is still `loading`, or the import ended in `error`. Renaming is unaffected. | | `429` | `rate_limit_exceeded` | Retry after the duration in the `Retry-After` header. | An unknown or inaccessible `brand_kit_id` on a Video Agent request is rejected with `400` `invalid_parameter` when the request is made, rather than failing partway through the render — so a bad ID costs you nothing. Brand kits are workspace-scoped: if you get this with an ID that exists, confirm the API key belongs to the workspace that owns the kit. # Burst Concurrency Source: https://heygen-1fa696a7.mintlify.app/docs/burst Run above your included concurrency on demand. Enterprise workspaces opt in to burst, set an extra cap of up to 50 workflows, and pay 1.5× the contract rate only on the jobs that run above the included slots. Burst is available on **Enterprise** plans. It is enabled per workspace by your account team; [contact sales](https://www.heygen.com/contact-us/sales) to turn it on. Every Enterprise workspace includes a fixed number of concurrent workflow slots (20 by default). Burst lets the workspace run **above** that number when a batch lands, without buying a higher permanent cap. You set one extra cap for the workspace, and it applies to each burst-eligible workflow type separately: with a +30 cap, up to 30 extra avatar renders **and** up to 30 extra video translations can run at the same time. Only the workflows that actually run above the included slots are billed at the burst rate. Burst is shared across the web app and the API. ## Turn on burst 1. Open [Usage](https://app.heygen.com/developers/usage) in the API dashboard. Under **Capacity**, the **Concurrency** tile shows your included slots. 2. Click **Set up burst**, choose the extra cap (**+5 to +50** workflows), and accept the rate notice. 3. Save. The tile now reads `20 + 30` with a **Burst** chip, and **Manage burst** lets you change the cap or turn burst off at any time. Workspace admins can also see included and burst capacity under **Settings → Usage & History**. ## How burst is billed | | Included slots | Burst slots | | :- | :- | :- | | Rate | Your contract rate | **1.5×** your contract rate | | Example, Avatar IV | 0.1 credits / sec | 0.15 credits / sec | | Applies to | All workflows | [Avatar video](/reference/create-video) renders and [video translations](/docs/video-translate), each with its own pool of extra slots | A workflow takes a slot when it is admitted and holds it until it completes. If every included slot for its type is busy at admission, the workflow runs in a burst slot and its whole render is billed at the burst rate: the duration is billed to the second as usual, then the 1.5× is applied and rounded up to the next whole quota unit. Workflows admitted into an included slot bill at the normal rate even while others are bursting. You only pay the premium for workflows that actually burst. Burst covers full avatar renders and video translations. Segment renders and free regenerations run in the included slots at the normal rate, and [Video Agent](/docs/video-agent) sessions keep their own concurrency limit. ## See what burst cost you Every burst workflow is tagged **Burst** in the [Activity tab](https://app.heygen.com/developers/usage?tab=activity), next to its credits. While burst is on, the Usage sidebar shows a **Burst** panel with the premium credits and the number of burst workflows for the selected period. ## API behavior With burst on, [`POST /v3/videos`](/reference/create-video) and [`POST /v3/video-translations`](/reference/create-video-translation) each accept requests up to *included + extra* concurrent workflows of their own type, so a +30 cap allows up to 50 avatar renders and up to 50 translations at once. Requests beyond that total return `429 Too Many Requests` with a `Retry-After` header, the same as any [concurrency limit](/docs/usage-limits#concurrency-limits). Batch endpoints such as [`POST /v3/videos/batch`](/reference/create-video-batch) queue their items through the same slots. # Create Avatar Source: https://heygen-1fa696a7.mintlify.app/docs/create-avatar The four ways to create a HeyGen avatar over the API: from video footage, from one photo, from a text prompt, or by applying a Look Pack to an avatar you already own. Every avatar you create is a **look** on a **character**. The look id is what you pass as `avatar_id` when you create a video. The character, called an avatar group, is the identity that holds all of that person's looks together. Browse them with [Avatar Groups](/docs/avatars) and [Avatar Looks](/docs/avatar-looks). ## Pick a starting point | You have | You get | Guide | | - | - | - | | Video footage of a person | A digital twin of that person, plus a voice cloned from the same recording | [Video to Avatar](/docs/avatar-from-video) | | One photo of a person | An avatar from that image, ready in minutes | [Photo to Avatar](/docs/avatar-from-photo) | | A text description | A synthetic character, no real person depicted | [Prompt to Avatar](/docs/avatar-from-prompt) | | An avatar you already own | A new outfit, setting, or style for that same character | [Prompt to Avatar](/docs/avatar-from-prompt) or [Look Packs](/docs/avatar-look-packs) | The first three go through [`POST /v3/avatars`](/reference/create-avatar), distinguished by the `type` field. The fourth generates looks for an existing character: one at a time in your own words through the same endpoint, or a curated set in one call through [`POST /v3/avatars/looks`](/reference/create-avatar-looks). ## What creation returns `POST /v3/avatars` answers the same shape for all three types: the new look in `avatar_item`, the character it belongs to in `avatar_group`. ```json theme={null} { "data": { "avatar_item": { "id": "look_abc123", "name": "Sarah", "avatar_type": "digital_twin", "group_id": "group_xyz789", "preview_image_url": "https://files.heygen.ai/...", "supported_api_engines": ["avatar_iv", "avatar_v"], "default_voice_id": "voice_cloned123", "status": "processing" }, "avatar_group": { "id": "group_xyz789", "name": "Sarah", "looks_count": 1, "default_voice_id": "voice_cloned123", "status": "pending_consent", "consent_status": "pending", "created_at": 1717000000 } } } ``` Creation is asynchronous, so a new look starts at `status: "processing"`. Poll [`GET /v3/avatars/looks/{look_id}`](/reference/get-avatar-look) until it reports `completed`. For digital twins you can also subscribe to the `instant_avatar.success` and `instant_avatar.fail` [webhook events](/docs/webhook-events), then read the look to confirm its status. Before requesting a specific engine, check the look's `supported_api_engines` against [Models](/models). The character carries a status of its own, covering consent and moderation rather than training, so `avatar_group.status` and `avatar_item.status` move independently. `avatar_group.status` follows `consent_status`: `pending_consent` while consent is outstanding, `failed` if it was rejected, and `completed` otherwise. A photo or prompt avatar renders as soon as its look reports `completed`. A digital twin renders once the look is `completed` and its group has cleared consent, so wait for both. ## Consent A [digital twin](/docs/avatar-from-video) generates video once the person depicted has agreed to be cloned, which is what `consent_status` tracks. A group created from a photo or a prompt has no consent step, so it reports `consent_status` of `null`. A photo still shows a real person: you are responsible for having their permission, as [Photo to Avatar](/docs/avatar-from-photo) explains. The field belongs to the group, so a photo or prompt look added to an existing twin's group carries that group's status. [Avatar Consent](/docs/avatar-consent) covers the [`POST /v3/avatars/{group_id}/consent`](/reference/create-avatar-consent) flow and the three levels of access to it. ## One character, many looks Adding looks to a character you already have keeps one identity across every video: pass `avatar_group_id` when you create, hand an existing look to [a prompt](/docs/avatar-from-prompt), or apply [a Look Pack](/docs/avatar-look-packs). List everything a character owns with [`GET /v3/avatars/looks?group_id=...`](/reference/list-avatar-looks), rename a look with [`PATCH /v3/avatars/looks/{look_id}`](/reference/update-avatar-look), and remove one with [`DELETE`](/reference/delete-avatar-look). Deleting the last look in a character deletes the character with it. ## Delete an avatar [`DELETE /v3/avatars/{group_id}`](/reference/delete-avatar-group) deletes the character and every look in it. To remove a single look instead, use [`DELETE /v3/avatars/looks/{look_id}`](/reference/delete-avatar-look) as above. Neither can be undone through the API. Deleting an avatar does not delete the files you created it from, or anything else made with it: | Resource | Removed with the avatar? | How to remove it | | - | - | - | | Training footage, consent video, or photo you supplied | No, each is a separate asset | [`DELETE /v3/assets/{asset_id}`](/reference/delete-asset) | | Voice cloned from twin footage | No, it stays in your voice library | [`DELETE /v3/voices/{voice_id}`](/reference/delete-a-voice) | | Videos already generated | No | [`DELETE /v3/videos/{video_id}`](/reference/delete-video) | A `file` or `consent_video` passed as a `url` or base64 is also saved as an asset in your workspace. To keep the `asset_id` of every source file so you can delete it later, upload with [`POST /v3/assets`](/docs/upload-assets) first and pass `asset_id`. To remove an avatar and everything you supplied for it, delete the avatar group, then each source asset and the cloned voice. Delete source assets only after the look reports `completed` or `failed` and consent has been accepted or rejected. # Discord Source: https://heygen-1fa696a7.mintlify.app/docs/discord Join the HeyGen developer Discord for API help, feature announcements, and community recipes. Talk to the API team and other developers building on HeyGen. Engineers and PMs are active in the community every day. Got a question the docs didn't fully answer? Stuck on an integration? Want to see what others are building? Our [Discord](https://discord.gg/mGKRsCtSC3) is the fastest way to get real help — from the engineers and PMs who build the product, and from developers who've probably hit the same wall you're hitting. Ask questions, get direct access to engineers and PMs, and connect with developers building with HeyGen. Free to join, no waitlist. No ticket queues. Post in the right channel and get answers from engineers who built the feature — often within hours. Share feedback, report rough edges, and request features. PMs actively monitor the community and feed insights back to the team. Browse what others are building, share your own projects, and find patterns and workarounds not in the docs yet. | Channel | What it's for | | :- | :- | | `#general` | General questions about the API and docs | | `#heygen-api` | Questions, integrations, and troubleshooting for the HeyGen API | | `#hyperframes` | Showcase and discuss HyperFrames projects | | `#heygen-cli-and-mcp` | Support and discussions for the HeyGen CLI and MCP tools | | `#hackathon` | project ideas, submissions, and hackathon updates | | `#announcements` | Release notes and major updates |
# Edit Scenes Source: https://heygen-1fa696a7.mintlify.app/docs/edit-scenes Revise two scenes of a Video Agent video without touching the rest. Read the scene list, send an edit_plan with one instruction per scene, and poll the new draft. **API reference:** [Get Video Scenes](/reference/get-video-scenes) · [Send Message or Request Revision](/reference/send-message-or-request-revision) · [Get Video](/reference/get-video) · [List Session Videos](/reference/list-session-videos) A scene edit changes only the scenes you name. Everything else in the video keeps its script, visuals, and pacing, so a small fix stays small: one call, a few minutes of rendering, and no re-prompting of the whole video. It works on any Video Agent session, whether you created it with `"mode": "generate"` or `"mode": "chat"` (see [Interactive Sessions](/docs/interactive-sessions)).
## Three calls [`GET /v3/videos/{video_id}/scenes`](/reference/get-video-scenes) on a completed video from the session. The response lists every scene in order with its `id`, `script`, `background`, and `elements`, plus one `edit_version` for the whole document. Wait for the video to reach `completed` first: while it is still rendering this call returns `409 resource_not_ready`. [`POST /v3/video-agents/{session_id}`](/reference/send-message-or-request-revision) with an `edit_plan` array. Each item names one `scene_id`, the change in plain language, the `scene_snapshot_video_id` you read the scene from, and that snapshot's `edit_version`. No `message` is needed. The response carries the `video_id` of the new working draft. [`GET /v3/videos/{video_id}`](/reference/get-video) with the returned `video_id` until `status` is `completed`. The session's own `video_id` stays `null` while the edit runs, so poll the draft directly. [`GET /v3/video-agents/{session_id}/videos`](/reference/list-session-videos) lists every cut the session has produced. ## Example The plan below produced the "after" video above. Two items, two scenes, one request. ```bash theme={null} curl -X POST "https://api.heygen.com/v3/video-agents/$SESSION_ID" \ -H "X-Api-Key: $HEYGEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "edit_plan": [ { "scene_id": "ff943f68-159d-463b-84ac-b784a61c8bca", "text": "Change the on-screen headline to Evening mode and make the background a much darker, warmer night-time room with a single lit lamp. Keep the narration the same.", "scene_snapshot_video_id": "7c852479652e4e09b2cc6b12382b31f6", "edit_version": "e6eb1e63074cbfc694e4e6b64d3dc1b9cc004da54074d073631baaa825ce0e53" }, { "scene_id": "e89c22a1-7619-4f26-bdb5-c5ec5555da4e", "text": "Replace the closing line with: Lumen. Light that knows you. Available now. Drop the pre-order URL from narration and on-screen text.", "scene_snapshot_video_id": "7c852479652e4e09b2cc6b12382b31f6", "edit_version": "e6eb1e63074cbfc694e4e6b64d3dc1b9cc004da54074d073631baaa825ce0e53" } ] }' ``` ```json Response theme={null} { "data": { "session_id": "6cbd1fbc886b434a920acfed05fa931b", "run_id": "c3817fcd60b543da8480b6bc7d03026f", "title": "Lumen Explainer Video", "video_id": "1af65ebabfdc48ed84fc2f4ec2abd20f" } } ``` With the [CLI](/cli), the same turn is `heygen video scenes get ` followed by `heygen video-agent send -d edit_plan.json`. ## What to expect | Situation | What happens | | - | - | | Narration | Say "keep the narration" and the scene keeps its script while the visuals change. Give new copy and both change. | | Untouched scenes | Same script and visuals as before. The new draft is a new document, so scene and element ids are reissued; compare by content, not by id. | | Several edits in a row | Each accepted turn returns the current working draft in `video_id`. Read that draft's scenes for the next `edit_version`. | | `edit_version` no longer current | `409 stale_edit_version`. Read the scenes again and resend with the new version. | | A `scene_id` outside the snapshot | `400 invalid_parameter`. The whole plan is rejected before anything is submitted, so a typo never produces a half-applied edit. | | Reading scenes | A heavy read with a lower rate limit than `GET /v3/videos/{video_id}`. Poll the video for status and read scenes once it completes. | | Plan size | Up to 50 items per request, each up to 10,000 characters. Items may point at different snapshots from the same session, including earlier videos. | ## Edit with an agent Ask a coding agent to make the change for you: it reads the scenes, picks the ones that match your description, and sends the plan. ```text Prompt for your agent wrap theme={null} Read https://developers.heygen.com/docs/edit-scenes.md and https://developers.heygen.com/docs/for-ai-agents.md first. My HeyGen API key is in the HEYGEN_API_KEY environment variable; never ask me to paste it. Video Agent session has a completed video . Call GET /v3/videos//scenes, show me each scene's script in one line, and ask me which scenes to change and how. Then send one POST /v3/video-agents/ with an edit_plan covering only those scenes, using the scene ids and edit_version you read. Poll the returned video_id until completed and give me the video URL. ``` # Error Codes Source: https://heygen-1fa696a7.mintlify.app/docs/error-codes Error codes, HTTP status codes, and troubleshooting for the HeyGen API HeyGen uses conventional HTTP response codes to indicate the success or failure of an API request. Codes in the `2xx` range indicate success. Codes in the `4xx` range indicate an error with the information provided (e.g., a missing parameter, insufficient credits, or a resource not found). Codes in the `5xx` range indicate an error on HeyGen's servers. Every error response includes a machine-readable `code`, a human-readable `message`, a `doc_url` linking to the relevant section below, and a `request_id`. Some errors that relate to a specific request field also include a `param` attribute. The `request_id` is also returned in the `X-HeyGen-Request-Id` response header, including on errors that have no JSON body. Include it when contacting support. The text-to-speech and speech-to-speech endpoints under `/v3/models/audio/` are served separately and do not include it. ## Error response format ```json theme={null} { "error": { "code": "insufficient_credit", "message": "Your account has 5 credits but this video requires 10 credits.", "doc_url": "https://developers.heygen.com/docs/error-codes#insufficient-credit", "request_id": "8c4f0b9e1d2a4f6b9e3c7a1d5f2b8e40" } } ``` | Attribute | Type | Description | | - | - | - | | `code` | string | A short, machine-readable identifier for the error. See the full list below. | | `message` | string | A human-readable description of what went wrong and, where possible, how to fix it. | | `param` | string | The request field that caused the error. Only present for validation errors. | | `doc_url` | string | A link to the documentation for this specific error code. | | `request_id` | string | The identifier of this request, the same value as the `X-HeyGen-Request-Id` response header. Include it when contacting support. | ## HTTP status code summary | Status | Meaning | | - | - | | `200 OK` | Everything worked as expected. | | `400 Bad Request` | The request was malformed or contained invalid parameters. | | `401 Unauthorized` | No valid API key was provided. | | `402 Payment Required` | The request requires additional credits or a plan upgrade. | | `403 Forbidden` | The API key doesn't have permission to perform the request. | | `404 Not Found` | The requested resource doesn't exist. | | `429 Too Many Requests` | Too many requests hit the API too quickly, or a usage quota was exceeded. | | `500 Internal Server Error` | Something went wrong on HeyGen's end. | *** ## Error codes ### `unauthorized` **HTTP status:** `401` The API key provided is invalid, expired, or missing. Verify that you are sending your API key in the `X-Api-Key` header and that the key is active in your [HeyGen account settings](https://app.heygen.com/settings). ### `api_key_owner_workspace_access_revoked` **HTTP status:** `401` The API key's owner no longer has active access to the workspace the key belongs to, or that workspace was deleted. For an active workspace, ask its admin to restore the owner's membership. For a deleted workspace (including a personal workspace), use a key for an active workspace. Restoring active membership in an active workspace lets the same key work again; retrying or refreshing the credential without restoring access will not help. ### `forbidden` **HTTP status:** `403` The request is authenticated but cannot use the current workspace or action. This can happen when the workspace session expires or the caller lacks access. Reauthenticate if the session expired; otherwise use a workspace or role with the required access. It is also returned when a voice provider rejects the API key of a voice provider account you connected, because the key is invalid or the account is inactive. Update the API key in your settings. ### `insufficient_api_key_scope` **HTTP status:** `403` The API key is valid but its permission scopes don't cover this endpoint. Restricted API keys are only authorized for the resources and access levels (read/write) explicitly granted when the key was created. Check the key's configured scopes in your [HeyGen account settings](https://app.heygen.com/settings), or use a key with broader access. The `message` names the scopes the endpoint requires, so it can be compared against the key's grants directly. ### `api_key_required` **HTTP status:** `403` The request was made with an OAuth token whose client can't pay for this endpoint. Either the endpoint charges only API credits, which only a HeyGen API key can spend, or the client has no wallet it can pay from. Send the request with a HeyGen API key in the `X-Api-Key` header. A request that carries both a key and a token is treated as the key. Nothing was created or charged. ### `resource_access_denied` **HTTP status:** `403` The authenticated user does not have access to the specific resource referenced in the request. The resource may belong to a different user or organization. Verify that the resource ID is correct and belongs to your account. ### `encryption_setup_pending` **HTTP status:** `409` The workspace was created with a customer-managed encryption key (BYOK) and that key has not yet been registered, validated and activated. Until it is, the workspace refuses every media write — uploads, generation, renders — so that nothing it ever holds is outside the customer's key. A workspace administrator completes the setup from Settings → Workspace Controls → Encryption, or HeyGen completes it on the customer's behalf. ### `encryption_disabled` **HTTP status:** `403` The workspace's customer-managed encryption cannot be used for this request: it has been stood down, or the workspace is marked for customer-managed encryption and its key record cannot be resolved. Either way the media write is refused rather than falling back to a HeyGen-managed key. Standing it down is deliberate and one-way, and it leaves the workspace unable to store media: its existing objects are under a key it would no longer use, and moving them out is a separate migration that does not exist yet. So this is not a way to return a workspace to ordinary shared storage — a customer who wants customer-managed encryption again is provisioned a new workspace, and support should treat a workspace in this state as one that needs a decision rather than a retry. This is also not the error for a key the customer has disabled in their own AWS account. That needs no change on this side: the operation fails because AWS refuses it, and uploads and playback resume on their own once the key is usable again. ### `encryption_cross_tree_transfer` **HTTP status:** `409` Content cannot be moved or copied between a BYOK space and any other workspace, in either direction. Moves re-point database rows at the same stored object rather than copying bytes, so a cross-BYOK move would leave media outside the customer's key, or put another customer's media under it. Moves within one BYOK space are unaffected. ### `invalid_encryption_key` **HTTP status:** `400` The supplied KMS key reference was rejected. It must be a full key ARN (not an alias or bare key id, which would resolve inside HeyGen's account rather than the customer's), for a symmetric `ENCRYPT_DECRYPT` key in `us-east-2`. ### `encryption_setup_failed` **HTTP status:** `409` Provisioning the workspace's encrypted bucket, or the validation round trip against it, did not complete. The key itself may be fine: this is the bucket refusing to be configured, an AWS error during setup, or a check in the validation report coming back red. The response carries the per-step or per-check report; act on the first failed entry. Nothing was activated. A provisioning failure during key registration means the key was not recorded; during activation the key stays recorded and the BYOK space stays `awaiting_key`. ### `encryption_temporarily_unavailable` **HTTP status:** `503` The service could not tell whether this workspace's media must be stored under a customer-managed key, so the media was not stored. Nothing is wrong with the workspace or its key: the record that answers the question could not be read. Retry with backoff. ### `ai_vendor_access_restricted` **HTTP status:** `403` The workspace has restricted which AI vendor companies may be used. The action or model you requested relies on a vendor that is not allowed under the workspace’s AI vendor access policy. Ask a workspace administrator to update the policy if this vendor should be permitted. ### `brand_kit_locked` **HTTP status:** `403` The workspace's brand policy is locked (an enterprise governance setting): only the workspace default brand kit may be applied, and the request named a different `brand_kit_id`. Pass the default brand kit id (included in the error message) or omit `brand_kit_id` entirely — the workspace default is applied automatically. Ask a workspace administrator to pin a different default or unlock the brand policy if another kit is needed. ### `voice_not_usable` **HTTP status:** `403` The voice referenced in the request cannot currently be used to generate this video. The voice is in a state that blocks generation and will not resolve on retry. Select a different voice. This code is also returned when the upstream voice provider restricts the voice: it is only offered on a higher tier of the provider's own plan, or the provider has suspended access to it. Upgrading your HeyGen plan does not change this. If you connected your own voice provider account, the error message names the provider tier the voice requires; otherwise, choose a different voice. It is also returned when the voice provider has the voice but will not generate speech with it, for example because the voice is disabled, needs verification, has not been fine-tuned or has invalid samples at the provider, or because the provider returned no audio, which usually means the voice does not support the language of the text. Choose a different voice. If the voice is in your own connected ElevenLabs account, the error message says how to fix a disabled, unverified or not fine-tuned voice in that account: enable, verify or regenerate it. ### `phone_verification_required` **HTTP status:** `403` The account associated with this request must complete phone (SMS) verification before this request can proceed. This is an anti-abuse safeguard that can apply to any generation feature (video, voice, avatar, and related operations). This also applies during API-key authentication, including requests that do not consume credits. Sign in to HeyGen and complete phone verification, then retry the request. Older HeyGen app endpoints may return the numeric code `400193` for the same requirement. ### `rate_limit_exceeded` **HTTP status:** `429` You are sending requests too frequently. Back off and retry with exponential backoff. Check the `Retry-After` response header for the number of seconds to wait before retrying. See [Usage Limits](/docs/usage-limits#concurrency-limits) for concurrency and per-endpoint limits; Enterprise workspaces can raise effective concurrency with [burst](/docs/burst). This code is also returned when the upstream voice provider throttles a speech or voice request. That response carries no `Retry-After` header; back off and retry with exponential backoff. ### `api_endpoint_disabled` **HTTP status:** `403` This endpoint is not available for your account. Contact support. ### `quota_exceeded` **HTTP status:** `429` You have exceeded a usage quota (e.g., the free-tier limit for video agent or AI clip requests). Upgrade your plan or wait for your quota to reset. Check your current usage in the [HeyGen dashboard](https://app.heygen.com). ### `insufficient_credit` **HTTP status:** `402` Your account does not have enough credits to complete this request. The error message includes how many credits you have and how many are required. Purchase additional credits or reduce the scope of your request (e.g., shorter video duration, fewer scenes). ### `usage_limit_reached` **HTTP status:** `402` The member reached a workspace usage limit. Ask a workspace administrator to check the limit, or wait for the limit to reset. Buying more credits does not remove this limit. ### `trial_limit_exceeded` **HTTP status:** `402` You have reached the video generation limit for trial accounts. Upgrade to a paid plan to continue creating videos. ### `subscription_required` **HTTP status:** `402` You have used all the credits included in your pay-as-you-go trial, and further top-ups are not available on your account. Choose a plan to continue making API requests — see [pricing](https://www.heygen.com/pricing). This is distinct from `insufficient_credit`: buying more credits will not resolve it, because purchases are no longer available. Subscribing to a plan is the only action that restores API access. ### `hf_subscription_required` **HTTP status:** `402` HyperFrames Desktop asked to pay with the workspace's HyperFrames plan, and the workspace has no HyperFrames plan or usage. Choose a HyperFrames plan in HyperFrames Desktop, or send the request with a HeyGen API key in the `X-Api-Key` header. Nothing was created or charged. ### `hf_usage_exhausted` **HTTP status:** `402` HyperFrames Desktop asked to pay with the workspace's HyperFrames plan, and the plan's usage left does not cover this request. Wait for the plan to renew, change the plan in HyperFrames Desktop, or send the request with a HeyGen API key in the `X-Api-Key` header. Nothing was created or charged. ### `plan_upgrade_required` **HTTP status:** `402` The requested feature or resource requires a higher subscription tier than your current plan. This can occur when: * Using a premium avatar that is not available on your plan. * Accessing an integration that requires a higher tier. * Requesting a resolution or feature gated by plan level. A higher plan is required to access this feature. Learn more about the available plans on [HeyGen](https://www.heygen.com). ### `resolution_not_allowed` **HTTP status:** `400` The requested video resolution exceeds the resolution available on your current plan. Avatar, image, and studio OAuth video creation check this before accepting the generation request. When resolution is omitted, avatar and image videos use 720p, while studio uses 1080p. Choose a lower resolution or a plan that supports the requested resolution. [Continue on HeyGen](https://www.heygen.com) to learn more. Retrying the same request will not resolve the restriction. API-key video creation retains its existing resolution access. ### `video_not_found` **HTTP status:** `404` No video, draft, or video translation was found matching the provided ID. Verify that: * The `video_id` is correct and was not mistyped. * The video has not been deleted. * The video belongs to your account. ### `avatar_not_found` **HTTP status:** `404` No avatar was found matching the provided ID. This applies to all avatar types — standard avatars, photo avatars (photars), instant avatars, and avatar kits. Verify that: * The `avatar_id` is correct. * The avatar has finished training (if recently created). * The avatar belongs to your account or is a public avatar. ### `voice_not_found` **HTTP status:** `404` No voice was found matching the provided ID. Verify that the `voice_id` is correct and that the voice is available in your account. If using a cloned voice, ensure it has finished processing. ### `template_not_found` **HTTP status:** `404` No template was found matching the provided ID. Verify that the `template_id` is correct and that the template is shared with your account or is publicly available. ### `asset_not_found` **HTTP status:** `404` No asset was found matching the provided ID. Assets may have been deleted or may not have finished uploading. Verify that the `asset_id` was returned from a successful [`POST /v3/assets`](/reference/upload-asset) call and that the asset has not been removed. ### `webhook_not_found` **HTTP status:** `404` No webhook endpoint was found matching the provided ID. Verify that the `endpoint_id` is correct and that the webhook has not been deleted. List your existing webhooks with `GET /v3/webhooks/endpoints` to find valid endpoint IDs. ### `batch_not_found` **HTTP status:** `404` No batch was found matching the provided ID. Verify that the `batch_id` is correct and that the batch belongs to your account. ### `resource_not_found` **HTTP status:** `404` The requested resource was not found. This is a generic not-found error for resources that do not have a more specific error code (e.g., streaming sessions, audio records). Verify that the resource ID is correct and belongs to your account. It is also returned when a voice from a voice provider account you connected no longer exists in that account. Restore or re-import the voice in that account, or choose a different voice. ### `voice_not_cloneable` **HTTP status:** `400` The selected voice is provider-owned and cannot be cloned, duplicated, improved, or changed to a different provider. Use the voice with one of its listed `allowed_engines`, or choose a cloneable voice for mutation operations. ### `invalid_parameter` **HTTP status:** `400` One or more request parameters are invalid, missing, or in the wrong format. The `message` field describes which parameter failed validation and why. The `param` field, when present, identifies the specific field. Common causes: * A required field is missing from the request body. * A field value is the wrong type (e.g., string instead of number). * A field value is outside the allowed range or not in the set of accepted values. * The request body is not valid JSON or is not a JSON object. ### `graph_invalid` **HTTP status:** `422` The workflow product graph is structurally well-formed JSON but semantically invalid, so it cannot be saved as submitted or compiled into a version. Distinct from `invalid_parameter` (400), which means the request body itself is malformed; a `graph_invalid` response means the graph parsed but violates the workflow contract. The `errors[]` array lists every problem found in one pass — validation never stops at the first issue. Each entry carries a stable `code` (for example `unknown_node_type`, `node_type_unavailable`, `invalid_config`, `dangling_dependency`, `cycle_detected`), a human-readable `message`, and where applicable the offending `node_id`, `port`, and `path` (a field path such as `config.prompt` or `outputs.result`). Fix every listed issue and resubmit; use `GET /v3/workflows/node-types` to discover the node types and config schemas available for authoring. ### `conflict` **HTTP status:** `409` The request conflicts with existing state. For example, attempting to create a webhook endpoint with a URL that is already registered for your account, or creating a brand glossary whose name another glossary in your workspace already uses (names are compared without regard to case). On brand glossary writes this also covers a `term` that appears more than once within any one of the `terms`, `do_not_translate_terms`, or `forced_translations` lists in the request body (terms are compared without regard to case). Use a different value or delete the existing resource first. A brand glossary update can also conflict with a concurrent edit. The update is rolled back; fetch the latest glossary, reapply your changes and retry. On the workflow authoring endpoints this also covers `Idempotency-Key` reuse: sending a key that was already used with a different request body or target returns this code instead of replaying the original response. Send a new key for a new request. ### `brand_kit_not_ready` **HTTP status:** `409` The brand kit is still being assembled, or its import failed, so its role assignments cannot be edited yet. A brand kit imported from a website gathers its colors, logos and fonts in the background; until `status` is `completed` the assets a role would name may not exist. Poll `GET /v3/brand-kits/{brand_kit_id}` until `status` is `completed`, then retry. Renaming a kit is allowed at any time and never returns this. ### `voice_not_ready` **HTTP status:** `409` The professional voice clone (PVC) is still training and cannot serve inference yet. Training normally completes within minutes; retry the stream request after a short delay. ### `voice_training_failed` **HTTP status:** `409` Training for this professional voice clone (PVC) ended in a terminal failure, so the voice cannot serve inference. Retrying the stream request will not succeed; contact support to retrain or replace the voice. ### `stale_workflow_draft` **HTTP status:** `409` Returned by `PATCH /v3/workflows/{workflow_id}` when the workflow draft moved before your write landed: the `expected_revision` you sent no longer matches. Nothing was written. Re-read the workflow to pick up the current `draft_revision` and reapply your change; retrying the same request unchanged will keep failing. Publishing (`POST .../versions`) no longer surfaces this code — its graph rides the request body, so a concurrent draft edit cannot invalidate it. ### `workflow_archived` **HTTP status:** `409` The workflow definition is archived, so it cannot be published or modified. This is terminal for that definition — retrying will not help. Create a new workflow instead. ### `template_update_in_progress` **HTTP status:** `409` Another write to this template is still being applied. Writes to one template, from the API or from the HeyGen editor, are serialized so two of them cannot interleave. The template row is only written by the writer that holds the lock; the editor keeps its lock alive while it stores element metadata, and a save that lost the lock is refused before it writes the row. Wait a moment and retry; if you use `expected_edit_version`, re-read the template first. ### `workflow_run_admission_refused` **HTTP status:** `409` The workflow run was created, but its coordinator workflow was authoritatively refused by workflow scheduling — its inputs failed scheduling validation. This is terminal for the run: retrying the same request will not help. Check the run's status for details, or contact [HeyGen support](https://help.heygen.com) with the run id. ### `resource_not_ready` **HTTP status:** `409` The requested resource exists but is not yet in a ready state. This can occur when a video translation is still processing, an instant avatar has not finished training, or an existing reference image does not yet have its stored object. Poll the resource or asset status and retry once it reaches a ready state. Also returned when the uploaded object is not yet present in storage (e.g. `POST /v3/assets/{asset_id}/complete` called before the upload PUT landed), or while `/complete` is still checking the uploaded file, which it waits on for a few seconds before answering; safe to retry. Missing or deleted reference images return `asset_not_found` instead. ### `image_processing_failed` **HTTP status:** `422` The referenced image exists but its processing has permanently ended without producing a usable result (for example, image processing failed, or it completed without generating a thumbnail), so the requested operation cannot be performed. Unlike `resource_not_ready`, this is terminal — retrying or polling will not change the outcome. Upload a new image and retry with it. ### `artifact_unavailable` **HTTP status:** `409` The requested artifact exists but permanently failed to resolve to a durable storage location, so no download URL can be issued. Unlike `resource_not_ready`, this is terminal — retrying or polling will not change the outcome. Inspect the artifact's `failure` field for the reason. ### `context_limit_exceeded` **HTTP status:** `409` The Video Agent session's conversation has grown too long for the agent to continue, so the message was not sent. This is terminal for the session: retrying, shortening the message or waiting will not help. `GET /v3/video-agents/{session_id}` reports the same condition as an `error` with this code. Start a new session with `POST /v3/video-agents` and carry over the brief and any assets you still need; videos the session already produced stay available. ### `stale_edit_version` **HTTP status:** `409` The Video Agent draft changed after the caller read its scenes, so the supplied `edit_version` no longer identifies the current scene graph. No edit was submitted. Fetch `GET /v3/videos/{video_id}/scenes` again, choose scene IDs from that response, and retry with its new `edit_version`. On `PUT /v3/templates/{template_id}/variables`, the `expected_edit_version` you sent does not match the template's current `edit_version`, so something changed the template after you last read it. Every write changes it, whether from another API client or from a save in the HeyGen editor, and the write itself is refused if the template moved between your read and your write. Re-read the template with `GET /v3/templates/{template_id}`, rebuild your variable set from the current state, and send it again with the new `edit_version`. ### `request_in_progress` **HTTP status:** `409` A prior request with this `Idempotency-Key` is still in progress. Wait for the original request to complete and retry. Once the original request finishes, subsequent retries with the same key within 24 hours replay the original response. ### `content_policy_violation` **HTTP status:** `400` The request was rejected for violating HeyGen's content policy. This can occur when an instant avatar does not pass the moderation review, when submitted content contains inappropriate content, or when an uploaded image/video asset that was flagged by moderation is used in a request. Create a new resource (or upload a new asset) that complies with our [usage policy](https://www.heygen.com/policy). For `POST /v3/models/videos`, an explicit prompt moderation rejection stops captioning and video generation when enforcement is enabled. Revise the prompt before retrying. With enforcement disabled, violations are recorded and generation proceeds. Moderation errors and timeouts currently allow generation to proceed in either mode. This code is also returned as a look's `error.code` (with `status: failed`) when a look generated from a prompt fails because the image provider's content safety check refused the prompt. Retrying the same prompt fails the same way, so change the prompt. No credits are used for the refused generation. It is also returned when the voice provider refuses a speech request under its own content policy, either for the text or for the selected voice. Retrying the same request fails the same way, so change the text or choose a different voice. ### `avatar_not_usable` **HTTP status:** `400` The requested avatar cannot be used because it did not pass content moderation or its creation failed. Select a different avatar; the error message identifies which condition occurred. ### `unlimited_mode_disabled` **HTTP status:** `400` The avatar does not support unlimited mode. Use a different avatar, or use Avatar IV or Avatar V. ### `scenes_unavailable` **HTTP status:** `400` This video's scenes cannot be returned. Not every video has a scene representation, and there is no fix on the caller's side — re-create the video through the API if you need its scenes. ### `video_not_editable` **HTTP status:** `400` This video cannot be edited through the API. It belongs to a Video Agent session, it is interactive or branching, it is a type the API cannot edit yet, or it uses avatars the workspace can no longer use or voices that no longer exist. The message names the reason, and an alternative where there is one. Nothing was changed. ### `edit_not_supported` **HTTP status:** `400` The request changes a field or uses an action that cannot be edited through the API yet. `param` names the part of the request at fault. Remove it and send the rest. Nothing was changed. ### `template_source_unsupported` **HTTP status:** `400` The video passed to `POST /v3/templates` cannot become a template. Either it has no editable draft in the current editor format (videos rendered before the current editor, or whose draft was never saved), or it was produced by a flow that does not keep an editable draft, such as URL to Video. Create the template from a video generated with `POST /v3/videos` or saved in the HeyGen editor. For an older editor video, open it in the editor and save it again, then retry. ### `variable_match_not_found` **HTTP status:** `400` A text variable in `PUT /v3/templates/{template_id}/variables` was declared with `match`, but that exact text does not occur in the template's script or on-screen text (or not in the `element_ids` you limited it to). Check the current text in `GET /v3/templates/{template_id}` under `elements[].current.text`; matching is literal and case-sensitive. If the template already contains `{{name}}` placeholders for this variable, declare it without `match`. ### `variable_match_spans_styles` **HTTP status:** `400` The `match` text for a text variable occurs in a rich-text element only across differently styled runs (for example, half bold and half regular), so a single placeholder cannot replace it without losing styling. Shorten `match` to text inside one styled run, or make the styling uniform in the HeyGen editor and retry. ### `avatar_consent_required` **HTTP status:** `400` The avatar group used in the request requires consent before it can be used to generate a video (its instant-avatar consent was skipped, rejected, or never completed). Complete the consent flow for the avatar group (e.g. `POST /v3/avatars/{group_id}/consent`), then retry the request. ### `avatar_expired` **HTTP status:** `400` The digital twin used in the request has expired because the workspace exceeded its avatar slot limit — typically after an avatar-slot add-on lapsed or the plan was downgraded. Existing videos are unaffected, but the expired digital twin cannot be used to generate new videos. Renew or purchase avatar slots, or delete other digital twins to bring the workspace back within its limit; the digital twin becomes usable again immediately. ### `voice_expired` **HTTP status:** `400` The professional voice clone used in the request has expired because the workspace exceeded its professional voice clone slot limit — typically after a voice clone add-on lapsed or its quantity was reduced. The voice and its data are untouched, but it cannot be used to generate new speech. Purchase more professional voice clone slots, or delete other professional voice clones to bring the workspace back within its limit; the voice becomes usable again immediately. ### `resource_limit_reached` **HTTP status:** `400` You have reached the maximum number of a resource allowed for your account (e.g., voice clone slots, instant avatar redo attempts, verified avatar group slots). Delete unused resources to free up capacity, wait for limits to reset, or contact [HeyGen support](https://help.heygen.com) to request a higher limit. Also returned when an account reaches the number of voice clones its plan includes. Every plan has a number; higher plans include more. The web and API clone pools are shared, so a clone created on either surface counts toward the same allowance, and voices created as part of avatar creation do not count against a paid plan's allowance. Deleting a clone frees a slot; upgrading raises the limit where a higher plan exists. ### `template_limit_reached` **HTTP status:** `400` The workspace already holds the maximum number of templates (2000), so `POST /v3/templates` cannot create another one. Delete templates you no longer need with the HeyGen editor's template library and retry. The limit is per workspace and shared with templates created in the web app. ### `role_not_allowed` **HTTP status:** `400` The requested space member role is no longer available and cannot be assigned. The viewer role has been retired; assign a different role (for example creator) instead. Distinct from `invalid_parameter`, which means the request was malformed — here the request is well-formed but names a role the workspace no longer offers. ### `voice_unavailable` **HTTP status:** `400` The requested voice exists but is not in a usable state. This occurs when a cloned voice failed processing, expired, or was canceled. Delete the voice and create a new voice clone, or use a different voice. ### `voice_settings_not_supported` **HTTP status:** `400` The voice is valid, but the requested voice-and-engine combination cannot be honored. Unlike `voice_unavailable` (the voice itself is in a broken state) and `invalid_parameter` (the request was malformed), the request is well-formed but this voice cannot run under the selected engine or configuration. Common causes: * The selected engine needs a source clip for the voice and none could be prepared. * A third-party (bring-your-own-key) voice was requested on an engine that only runs on HeyGen's house account. * The engine configuration mapping for the voice is missing and cannot be recreated. Some cases are transient rather than configuration problems: the voice's source clip may still be being prepared, or a newly created voice may still be propagating at the provider. In those cases, a later attempt with the same voice and engine may succeed. Allow a few seconds between retries for source preparation, or up to a minute between retries for provider propagation. The `message` field does not currently distinguish a transient source-clip failure from a permanent one. If the error persists after a few spaced retries, try a different engine or voice, or contact support. ### `script_too_short` **HTTP status:** `400` The script provided is too short to generate a video. HeyGen requires the text-to-speech audio to be at least 1.0 second long. Very short scripts (a single word, a period, or a few characters) will not produce enough audio. Add more content to your script and retry the request. ### `tts_text_invalid` **HTTP status:** `400` The text provided for text-to-speech conversion is invalid or cannot produce speech. Check that the script is not empty and contains speakable words or valid pauses, then retry. ### `video_too_long` **HTTP status:** `400` The input video exceeds the maximum supported duration for this endpoint (for example, filler word removal supports sources up to 2 hours). The error message includes the probed duration and the limit. Trim or split the video and retry. ### `no_audio_track` **HTTP status:** `400` The input video has no audio track. Speech-based processing (such as filler word removal) needs speech audio to transcribe. Provide a video that contains an audio stream. ### `download_failed` **HTTP status:** `400` A URL provided in your request could not be downloaded. This applies to video URLs, image URLs, audio URLs, and any other user-supplied resource link. Common causes: * The URL is not publicly accessible (authentication required, private video, restricted sharing settings). * The URL is malformed or points to a page rather than a direct file. * The remote server refused the connection or returned an error. * For Google Drive links, the file must be shared with "Anyone with the link" access. * For YouTube/Vimeo, the video must be public (unlisted or private videos are not supported). When the failing URL is one you supplied and could be safely re-rendered, the `message` field names it, without any query parameters. Otherwise — the failure involves one of HeyGen's own internal resources, or your URL could not be parsed — `message` is the fixed string `Failed to download a required asset`. If you supplied the URL, check it first; if it recurs on a URL you've confirmed is valid and reachable, contact support and quote that message. ### `youtube_video_download_forbidden` **HTTP status:** `400` A YouTube URL you provided points to a video that cannot be downloaded because YouTube itself forbids it. Unlike `download_failed`, this is not a connectivity or parsing problem — the video is reachable but not permitted for download. Common causes: * The video is members-only, age-restricted, or otherwise gated behind a YouTube sign-in. * The URL points to a live stream, a premiere, or an upcoming event rather than a finished video. * The video is region-locked or blocked in the region HeyGen downloads from. Supply a different, publicly downloadable YouTube video. Check the `message` field for the specific reason. ### `batch_too_large` **HTTP status:** `400` The batch request contains more items than the maximum allowed. Split the request into smaller batches and retry. ### `batch_item_invalid` **HTTP status:** `400` One or more items in the batch request failed validation. Check the `message` field for details about which item failed and why, correct the payload, and retry. ### `too_many_ids` **HTTP status:** `400` The request specified more IDs than the maximum allowed in a single call. Reduce the number of IDs and split the request across multiple calls. ### `video_delete_failed` **HTTP status:** `500` The video could not be deleted due to an internal error. Retry the request. If the error persists, contact [HeyGen support](https://help.heygen.com) with the `video_id`. ### `internal_error` **HTTP status:** `500` An unexpected error occurred on HeyGen's servers. This is not caused by your request. If the error persists, contact [HeyGen support](https://help.heygen.com) and include the full error response for faster debugging. ### `voice_provider_error` **HTTP status:** `502` An upstream voice provider that HeyGen relies on (for text-to-speech, voice cloning, or voice design) failed or closed the connection unexpectedly. This is not caused by your request. Retry after a short delay. If the error persists, contact [HeyGen support](https://help.heygen.com) and include the full error response for faster debugging. ### `bad_gateway` **HTTP status:** `502` The LiveAvatar API behind `/v3/live-avatar/*` could not be reached: the connection was refused, reset, or could not be established in time. This is not caused by your request. Retry after a short delay. If the error persists, contact [HeyGen support](https://help.heygen.com) and include the full error response for faster debugging. ### `not_implemented` **HTTP status:** `501` The requested endpoint exists, but its server-side implementation has not shipped yet. Retrying the same request will not succeed until the feature is implemented. ### `gateway_timeout` **HTTP status:** `504` An upstream dependency did not respond within the time limit. For a resource URL in your request (for example, background audio or an image), verify that the URL is publicly accessible, responds quickly, and is not blocked by firewall or geo-restrictions. For voice generation, retry after a short delay. When the timeout came from a resource URL you supplied, `message` names that URL when it can be rendered safely; otherwise asset-download timeouts use the fixed string `Failed to download a required asset`. Voice-generation timeouts instead explain that the voice service is temporarily unavailable. For `/v3/live-avatar/*`, the LiveAvatar API accepted the connection but did not answer within 30 seconds. Clients should branch on `code`, not `message`. ### `service_unavailable` **HTTP status:** `503` A downstream service needed to fulfill your request (for example, the text-to-speech synthesis backend or video quality comparison judge) is temporarily overloaded or unavailable. This is transient and not caused by your request. Back off and retry after a short delay. Also returned when brand glossary access cannot be determined temporarily. No glossary read or write is performed. Retry the request after a short delay. Also returned when publishing a workflow version loses the version number to a concurrent publish on the same workflow. Same guidance: back off and retry. Also returned when creating or duplicating a workflow without a name could not assign one because concurrent creates in the same space kept taking the next free name. Nothing was created; retry the request. Also returned by `PUT /v3/templates/{template_id}/variables` when the element metadata store cannot be read while binding a character variable. The variable set is left unchanged rather than stored with a guessed avatar type; retry the same request. Also returned when starting a workflow run could not confirm its coordinator workflow was accepted (a timeout, transport error, or an unrecognized response from workflow scheduling). This is safe and expected to retry: repeat the request with the same `Idempotency-Key` to resume the same run. Also returned when canceling a workflow run could not be confirmed as settled (workflow scheduling was unreachable or returned an error). The run is never moved backward by an unconfirmed cancel; retry the cancel request, or fetch the run to see its current status. Also returned when a workflow run's `url` or `base64` asset input could not be resolved yet. The run stays `preparing`; repeat the run-create request with the same `Idempotency-Key` to resume resolution at the first unresolved input. Also returned when professional voice source-audio ingestion is temporarily saturated or a finalized source cannot be read consistently. Back off and retry the create request with the same `Idempotency-Key`. Also returned by `GET /v3/audio/sounds` when the search backend is temporarily rate-limited. Nothing was searched; retry the same request after a short delay. Also returned by `POST /v3/assets/{asset_id}/complete` when the uploaded file cannot be checked right now. Nothing was finalized; retry the same request after a short delay. ### `render_not_started` **HTTP status:** `503` The video's document changed, but its render did not start, so nothing is charged. Read the video's scenes again for the new `edit_version`, then send the edit again with no changes to render it. ### `hyperframes_project_invalid` **HTTP status:** `400` The HyperFrames project zip you supplied isn't a valid composition. Make sure the zip contains an `index.html` (or the `composition` entry file you specified) at the root or in a single top-level directory, and that it opens correctly with the `hyperframes` CLI before submitting. ### `hyperframes_project_too_large` **HTTP status:** `413` The HyperFrames project zip exceeds the maximum allowed size for the ingestion method you used. Use `asset_id` (pre-upload via [`POST /v3/assets`](/reference/upload-asset)) for projects larger than the `url` / `base64` caps. ### `hyperframes_render_not_found` **HTTP status:** `404` No HyperFrames render with that `render_id` exists for your space, or the render has been soft-deleted. Check that the `render_id` is correct and was created under the same API key / space. ### `hyperframes_render_failed` **HTTP status:** `500` The HyperFrames render failed server-side while producing the video. The project was accepted and the render started, so this is not a problem with your request payload. Retry the render; if it fails repeatedly for the same project, verify the composition renders locally with the `hyperframes` CLI before contacting support with the `render_id`. When the cause is in the composition, the render's `failure_message` names it instead of the generic text: | Cause | What `failure_message` says | What to do | | - | - | - | | A font cannot be resolved | Which fonts could not be resolved | Embed the font with `@font-face` or use a Google Fonts family | | A video cannot be loaded | Why: the server refused the download (HTTP 401/403), the URL returned 404, the file is missing from the project, or it is not a playable video | Use a public URL, a signed URL valid for the whole render, or put the file in the project zip | | A media element points at the wrong kind of file | Which element type and file type disagree, e.g. an `