
Create Video
- 368 installs
- 42.8k repo stars
- Updated July 24, 2026
- calesthio/openmontage
Use this skill when working with create video.
About
Skill for video creation and processing. Use when you need to generate, edit, or process video files programmatically.
- Specialized for create video
- Integrated with Claude Code
- Streamlines workflow
Create Video by the numbers
- 368 all-time installs (skills.sh)
- Ranked #451 of 1,340 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/calesthio/openmontage --skill create-videoAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 368 |
|---|---|
| repo stars | ★ 42.8k |
| Last updated | July 24, 2026 |
| Repository | calesthio/openmontage ↗ |
What it does
Use this skill when working with create video.
Files
Create Video
Generate complete videos from a text prompt. Describe what you want and the AI handles script writing, avatar selection, visuals, voiceover, pacing, and captions automatically.
Authentication
All requests require the X-Api-Key header. Set the HEYGEN_API_KEY environment variable.
curl -X POST "https://api.heygen.com/v1/video_agent/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": "Create a 60-second product demo video."}'Tool Selection
If HeyGen MCP tools are available (mcp__heygen__*), prefer them over direct HTTP API calls — they handle authentication and request formatting automatically.
| Task | MCP Tool | Fallback (Direct API) |
|---|---|---|
| Generate video from prompt | mcp__heygen__generate_video_agent | POST /v1/video_agent/generate |
| Check video status / get URL | mcp__heygen__get_video | GET /v2/videos/{video_id} |
| List account videos | mcp__heygen__list_videos | GET /v2/videos |
| Delete a video | mcp__heygen__delete_video | DELETE /v2/videos/{video_id} |
If no HeyGen MCP tools are available, use direct HTTP API calls as documented in the reference files.
Default Workflow
Always use prompt-optimizer.md guidelines to structure prompts with scenes, timing, and visual styles.
With MCP tools: 1. Write an optimized prompt using prompt-optimizer.md → visual-styles.md 2. Call mcp__heygen__generate_video_agent with prompt and config (duration_sec, orientation, avatar_id) 3. Call mcp__heygen__get_video with the returned video_id to poll status and get the download URL
Without MCP tools (direct API): 1. Write an optimized prompt using prompt-optimizer.md → visual-styles.md 2. POST /v1/video_agent/generate — see video-agent.md 3. GET /v2/videos/<id> — see video-status.md
Quick Reference
| Task | MCP Tool | Read |
|---|---|---|
| Generate video from prompt | mcp__heygen__generate_video_agent | prompt-optimizer.md → visual-styles.md → video-agent.md |
| Check video status / get download URL | mcp__heygen__get_video | video-status.md |
| Upload reference files for prompt | — | assets.md |
When to Use This Skill vs Avatar Video
This skill is for prompt-based video creation — describe what you want, and the AI handles the rest.
If the user needs precise control over specific avatars, exact scripts, per-scene voice/background configuration, or multi-scene composition, use the avatar-video skill instead.
| User Says | This Skill | Avatar Video Skill |
|---|---|---|
| "Make me a video about X" | ✓ | |
| "Create a product demo" | ✓ | |
| "I want avatar Y to say exactly Z" | ✓ | |
| "Multi-scene video with different backgrounds" | ✓ | |
| "Transparent WebM for compositing" | ✓ |
Reference Files
Core Workflow
- references/prompt-optimizer.md - Writing effective prompts (core workflow + rules)
- references/visual-styles.md - 20 named visual styles with full specs
- references/prompt-examples.md - Full production prompt example + ready-to-use templates
- references/video-agent.md - Video Agent API endpoint details
Foundation
- references/video-status.md - Polling patterns and download URLs
- references/webhooks.md - Webhook endpoints and events
- references/assets.md - Uploading images, videos, audio as references
- references/dimensions.md - Resolution and aspect ratios
- references/quota.md - Credit system and usage limits
Best Practices
1. Optimize your prompt — The difference between mediocre and professional results depends entirely on prompt quality. Always use the prompt optimizer 2. Specify duration — Use config.duration_sec for predictable length 3. Lock avatar if needed — Use config.avatar_id for consistency across videos 4. Upload reference files — Help the agent understand your brand/product 5. Iterate on prompts — Refine based on results; Video Agent is great for quick iterations
Asset Upload and Management
HeyGen allows you to upload custom assets (images, videos, audio) for use in video generation, such as backgrounds, talking photo sources, and custom audio.
Upload Flow
Asset uploads are a single-step process: POST the raw file binary directly to the upload endpoint. The Content-Type header must match the file's MIME type.
Uploading an Asset
Endpoint: POST https://upload.heygen.com/v1/asset
Request
| Header | Required | Description |
|---|---|---|
X-Api-Key | ✓ | Your HeyGen API key |
Content-Type | ✓ | MIME type of the file (e.g. image/jpeg) |
The request body is the raw binary file data. No JSON or form fields are needed.
Response
| Field | Type | Description |
|---|---|---|
code | number | Status code (100 = success) |
data.id | string | Unique asset ID for use in video generation |
data.name | string | Asset name |
data.file_type | string | image, video, or audio |
data.url | string | Accessible URL for the uploaded file |
data.image_key | string \ | null |
data.folder_id | string | Folder ID (empty if not in a folder) |
data.meta | string \ | null |
data.created_ts | number | Unix timestamp of creation |
curl
curl -X POST "https://upload.heygen.com/v1/asset" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary '@./background.jpg'TypeScript
import fs from "fs";
import path from "path";
interface AssetUploadResponse {
code: number;
data: {
id: string;
name: string;
file_type: string;
url: string;
image_key: string | null;
folder_id: string;
meta: string | null;
created_ts: number;
};
msg: string | null;
message: string | null;
}
async function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileBuffer = fs.readFileSync(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: fileBuffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}
// Usage
const asset = await uploadAsset("./background.jpg", "image/jpeg");
console.log(`Uploaded asset: ${asset.id}`);
console.log(`Asset URL: ${asset.url}`);TypeScript (with streams for large files)
import fs from "fs";
import path from "path";
import { stat } from "fs/promises";
async function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse["data"]> {
const resolvedPath = path.resolve(filePath);
const fileStats = await stat(resolvedPath);
const fileStream = fs.createReadStream(resolvedPath);
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
"Content-Length": fileStats.size.toString(),
},
body: fileStream as any,
// @ts-ignore - duplex is needed for streaming
duplex: "half",
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}Python
import requests
import os
def upload_asset(file_path: str, content_type: str) -> dict:
with open(file_path, "rb") as f:
response = requests.post(
"https://upload.heygen.com/v1/asset",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": content_type
},
data=f
)
data = response.json()
if data.get("code") != 100:
raise Exception(data.get("message", "Upload failed"))
return data["data"]
# Usage
asset = upload_asset("./background.jpg", "image/jpeg")
print(f"Uploaded asset: {asset['id']}")
print(f"Asset URL: {asset['url']}")Supported Content Types
| Type | Content-Type | Use Case |
|---|---|---|
| JPEG | image/jpeg | Backgrounds, talking photos |
| PNG | image/png | Backgrounds, overlays |
| MP4 | video/mp4 | Video backgrounds |
| WebM | video/webm | Video backgrounds |
| MP3 | audio/mpeg | Custom audio input |
| WAV | audio/wav | Custom audio input |
Uploading from URL
If your asset is already hosted online:
async function uploadFromUrl(sourceUrl: string, contentType: string): Promise<AssetUploadResponse["data"]> {
// 1. Validate and download the file
const url = new URL(sourceUrl);
if (url.protocol !== "https:") {
throw new Error("Only HTTPS URLs are supported");
}
const sourceResponse = await fetch(sourceUrl);
const buffer = Buffer.from(await sourceResponse.arrayBuffer());
// 2. Upload directly to HeyGen
const response = await fetch("https://upload.heygen.com/v1/asset", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": contentType,
},
body: buffer,
});
const json: AssetUploadResponse = await response.json();
if (json.code !== 100) {
throw new Error(json.message ?? "Upload failed");
}
return json.data;
}Using Uploaded Assets
As Background Image
const videoConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Hello, this is a video with a custom background!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: asset.url, // Use the URL from the upload response
},
},
],
};As Talking Photo Source
const talkingPhotoConfig = {
video_inputs: [
{
character: {
type: "talking_photo",
talking_photo_id: asset.id, // Use the ID from the upload response
},
voice: {
type: "text",
input_text: "Hello from my talking photo!",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
},
],
};As Audio Input
const audioConfig = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "audio",
audio_url: asset.url, // Use the URL from the upload response
},
},
],
};Complete Upload Workflow
async function createVideoWithCustomBackground(
backgroundPath: string,
script: string
): Promise<string> {
// 1. Upload background
console.log("Uploading background...");
const background = await uploadAsset(backgroundPath, "image/jpeg");
// 2. Create video config
const config = {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: script,
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "image",
url: background.url,
},
},
],
dimension: { width: 1920, height: 1080 },
};
// 3. Generate video
console.log("Generating video...");
const response = await fetch("https://api.heygen.com/v2/video/generate", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const { data } = await response.json();
return data.video_id;
}Asset Limitations
- File size: 10MB maximum
- Image dimensions: Recommended to match video dimensions
- Audio duration: Should match expected video length
- Retention: Assets may be deleted after a period of inactivity
Best Practices
1. Optimize images - Resize to match video dimensions before uploading 2. Use appropriate formats - JPEG for photos, PNG for graphics with transparency 3. Validate before upload - Check file type and size locally first 4. Handle upload errors - Implement retry logic for failed uploads 5. Cache asset IDs - Reuse assets across multiple video generations
Video Dimensions and Resolution
HeyGen supports various video dimensions and aspect ratios to fit different platforms and use cases.
Standard Resolutions
Landscape (16:9)
| Resolution | Width | Height | Use Case |
|---|---|---|---|
| 720p | 1280 | 720 | Standard quality, faster processing |
| 1080p | 1920 | 1080 | High quality, most common |
Portrait (9:16)
| Resolution | Width | Height | Use Case |
|---|---|---|---|
| 720p | 720 | 1280 | Mobile-first content |
| 1080p | 1080 | 1920 | High quality vertical |
Square (1:1)
| Resolution | Width | Height | Use Case |
|---|---|---|---|
| 720p | 720 | 720 | Social media posts |
| 1080p | 1080 | 1080 | High quality square |
Setting Dimensions
TypeScript
// Landscape 1080p
const landscapeConfig = {
video_inputs: [...],
dimension: {
width: 1920,
height: 1080
}
};
// Portrait 1080p
const portraitConfig = {
video_inputs: [...],
dimension: {
width: 1080,
height: 1920
}
};
// Square 1080p
const squareConfig = {
video_inputs: [...],
dimension: {
width: 1080,
height: 1080
}
};curl
# Landscape 1080p
curl -X POST "https://api.heygen.com/v2/video/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"video_inputs": [...],
"dimension": {
"width": 1920,
"height": 1080
}
}'Dimension Helper Functions
type AspectRatio = "16:9" | "9:16" | "1:1" | "4:3" | "4:5";
type Quality = "720p" | "1080p";
interface Dimensions {
width: number;
height: number;
}
function getDimensions(aspectRatio: AspectRatio, quality: Quality): Dimensions {
const configs: Record<AspectRatio, Record<Quality, Dimensions>> = {
"16:9": {
"720p": { width: 1280, height: 720 },
"1080p": { width: 1920, height: 1080 },
},
"9:16": {
"720p": { width: 720, height: 1280 },
"1080p": { width: 1080, height: 1920 },
},
"1:1": {
"720p": { width: 720, height: 720 },
"1080p": { width: 1080, height: 1080 },
},
"4:3": {
"720p": { width: 960, height: 720 },
"1080p": { width: 1440, height: 1080 },
},
"4:5": {
"720p": { width: 576, height: 720 },
"1080p": { width: 864, height: 1080 },
},
};
return configs[aspectRatio][quality];
}
// Usage
const youTubeDimensions = getDimensions("16:9", "1080p");
const tikTokDimensions = getDimensions("9:16", "1080p");
const instagramDimensions = getDimensions("1:1", "1080p");Platform-Specific Recommendations
YouTube
const youtubeConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape
};TikTok / Instagram Reels / YouTube Shorts
const shortFormConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1920 }, // 9:16 portrait
};Instagram Feed Post
const instagramFeedConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1080 }, // 1:1 square
};const linkedinConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape preferred
};Twitter/X
const twitterConfig = {
video_inputs: [...],
dimension: { width: 1280, height: 720 }, // 16:9, 720p is common
};Avatar IV Dimensions
For Avatar IV (photo-based avatars), dimensions are set via orientation:
type VideoOrientation = "portrait" | "landscape" | "square";
function getAvatarIVDimensions(orientation: VideoOrientation): Dimensions {
switch (orientation) {
case "portrait":
return { width: 720, height: 1280 };
case "landscape":
return { width: 1280, height: 720 };
case "square":
return { width: 720, height: 720 };
}
}Custom Dimensions
HeyGen supports custom dimensions within limits:
const customConfig = {
video_inputs: [...],
dimension: {
width: 1600,
height: 900 // Custom 16:9 at non-standard resolution
}
};Dimension Constraints
- Minimum: 128px on any side
- Maximum: 4096px on any side
- Must be even numbers: Both width and height must be divisible by 2
function validateDimensions(width: number, height: number): boolean {
if (width < 128 || height < 128) {
throw new Error("Dimensions must be at least 128px");
}
if (width > 4096 || height > 4096) {
throw new Error("Dimensions cannot exceed 4096px");
}
if (width % 2 !== 0 || height % 2 !== 0) {
throw new Error("Dimensions must be even numbers");
}
return true;
}Resolution vs. Credit Cost
Higher resolutions may consume more credits:
| Resolution | Relative Cost |
|---|---|
| 720p | Base rate |
| 1080p | ~1.5x base rate |
Consider using 720p for drafts and testing, then 1080p for final output.
Background Considerations
Match background image/video dimensions to your video dimensions:
// For 1080p landscape video
const config = {
video_inputs: [
{
character: {...},
voice: {...},
background: {
type: "image",
url: "https://example.com/1920x1080-background.jpg" // Match video dimensions
}
}
],
dimension: { width: 1920, height: 1080 }
};Creating a Video Config Factory
interface VideoConfigOptions {
script: string;
avatarId: string;
voiceId: string;
platform: "youtube" | "tiktok" | "instagram_feed" | "instagram_story" | "linkedin";
quality?: "720p" | "1080p";
}
function createVideoConfig(options: VideoConfigOptions) {
const platformDimensions: Record<string, Dimensions> = {
youtube: { width: 1920, height: 1080 },
tiktok: { width: 1080, height: 1920 },
instagram_feed: { width: 1080, height: 1080 },
instagram_story: { width: 1080, height: 1920 },
linkedin: { width: 1920, height: 1080 },
};
const dimension = platformDimensions[options.platform];
// Scale down for 720p if requested
if (options.quality === "720p") {
dimension.width = Math.round((dimension.width * 720) / 1080);
dimension.height = Math.round((dimension.height * 720) / 1080);
}
return {
video_inputs: [
{
character: {
type: "avatar",
avatar_id: options.avatarId,
avatar_style: "normal",
},
voice: {
type: "text",
input_text: options.script,
voice_id: options.voiceId,
},
},
],
dimension,
};
}
// Usage
const tiktokVideo = createVideoConfig({
script: "Hey everyone! Check this out!",
avatarId: "josh_lite3_20230714",
voiceId: "1bd001e7e50f421d891986aad5158bc8",
platform: "tiktok",
quality: "1080p",
});Video Agent Prompt Examples
Full Example: Brief to Production Prompt
Input Brief
Topic: Monthly company report for a SaaS startup
Key data: $141M ARR (up from $54M), 1.85M signups (+28%), 3M paid videos/month
Customer story: Creator built AI character, 2.5M followers, 20 min/video
Challenge: Organic traffic volatile, -16% last week
Duration: ~90 seconds
Tone: Confident CEO, data-backedOutput Prompt
FORMAT: Bloomberg-style company report. 90 seconds. Fast-paced, data-dense.
Record-breaking month. Proud but analytical.
TONE: Confident, direct, data-backed. Highlights hit hard with numbers.
Customer stories are the emotional core. Challenges are honest — no spin.
AVATAR: Man in simple black crew-neck tee, standing in a modern glass-walled
office at golden hour. Behind him, a wall-mounted display shows the company logo
in soft blue glow. Monitor to his right shows a dashboard with upward-trending
charts. Desk beside him: laptop, half-empty flat white, scattered sticky notes.
Warm afternoon light through floor-to-ceiling windows, long shadows on polished
concrete. Minimal, focused startup HQ.
STYLE — SWISS PULSE (Müller-Brockmann): Grid-locked compositions. Black (#1a1a1a),
white, electric blue (#0066FF), warm amber (#FF9500) for records. Helvetica Bold
headlines, Regular labels. Numbers LARGE. Animated counters count up from 0.
Diagonal compositions on accent moments. Grid wipe transitions. No dissolves.
CRITICAL ON-SCREEN TEXT (display literally):
- "1.85M SIGNUPS — +28% MoM"
- "$2.12M NEW SUBSCRIPTION REVENUE"
- "$54M → $141M ARR"
- "2.5M FOLLOWERS" and "20 MIN / VIDEO"
- Quote: "Use technology to serve the message, not distract from it."
- "ORGANIC: 65% OF SUBS — VOLATILE"
MUSIC: Upbeat electronic with a driving beat. Tycho meets Bloomberg opening theme.
Builds through highlights, warms for customer story, softens for challenges, peaks
on close.
---
SCENE 1 — A-ROLL (8s)
[Avatar center-frame, energetic, leaning slightly forward]
VOICEOVER: "January was a record month. New highs across acquisition, revenue,
and product velocity. Here's the full picture."
Lower-third SLIDES in: "COMPANY NAME | JANUARY 2026" white on blue bar.
Grid wipe.
SCENE 2 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "One-point-eight-five million signups — twenty-eight percent month
over month. Two-point-one-two million in new subscription revenue. Both all-time
highs."
LAYER 1: Dark #1a1a1a background with thin grid lines pulsing at 8% opacity.
LAYER 2: "1.85M" SLAMS in from left, white Bold 140pt. "SIGNUPS" types on
in electric blue 32pt uppercase. "+28% MoM" appears in amber.
LAYER 3: Three stat cards CASCADE from top-right, staggered 0.3s:
"$2.12M New Revenue" — "$3.4M Business ARR" — "$3M Pro ARR."
Each number COUNTS UP from 0.
LAYER 4: Bottom ticker scrolls: "Non-brand search +36% • Brand impressions 9.2M
• Weekly subs +20.5%"
LAYER 5: Grid lines RIPPLE outward on "1.85M" slam. Diagonal amber bar behind
stat cards.
Hard cut.
SCENE 3 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "Zoom out. Twelve months ago — fifty-four million ARR. Today —
one hundred forty-one million. Nearly three X in a single year."
LAYER 1: Dark background, subtle grid scrolling upward.
LAYER 2: Animated line chart DRAWS ITSELF left to right. Y-axis: $50M to $150M.
Final point "$140.84M" glows amber and pulses.
LAYER 3: Milestone annotations float in at key data points.
LAYER 4: Second smaller chart below — "Paid Videos" 0.91M to 2.97M, same style.
LAYER 5: Thin grid lines converge toward final data point. Scan line sweeps.
Grid wipe.
SCENE 4 — A-ROLL (8s)
[Avatar center-frame, warm tone, genuine smile]
VOICEOVER: "But the numbers only tell half the story. The other half is the
people building on the platform."
Lower-third: "Customer Spotlight"
SCENE 5 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — warm palette]
VOICEOVER: "An AI character built entirely on the platform. Twenty minutes
per video. Two-point-five million Instagram followers. The creator's principle:
use technology to serve the message, not distract from it."
LAYER 1: Dark background with warm amber grid lines at low opacity.
LAYER 2: "CHARACTER NAME" in large white, center-top, 80pt.
LAYER 3: Stats cascade from right: "2.5M Followers" COUNTS UP in amber —
"20 min/video" — "7x Faster." Each a glowing node.
LAYER 4: Quote card SLIDES UP: "Use technology to serve the message, not
distract from it." Types on word by word.
LAYER 5: Warm light bloom. Grid lines soften into curved arcs.
Grid wipe.
SCENE 6 — A-ROLL (10s)
[Avatar center-frame, serious/candid]
VOICEOVER: "Now the honest part. Organic drives sixty-five percent of
subscriptions and it's volatile. Non-brand traffic dropped sixteen percent
last week. We've rebuilt attribution and we're investing in SEO."
Lower-third: "Challenges"
SCENE 7 — A-ROLL (7s)
[Avatar center-frame, energy lifts, direct eye contact]
VOICEOVER: "Fifty-four million to one-forty-one in twelve months. Three million
paid videos a month. January set the bar — now we raise it."
End card: Logo centered, blue glow fade-in. Grid lines converge. Music peaks.
---
NARRATION STYLE: CEO energy — conviction backed by data. Fast on highlights.
Warm on customer stories. Candid on challenges. Close with forward momentum.Ready-to-Use Templates
Tech News Briefing
FORMAT: 75-second high-energy tech briefing. Think: Bloomberg meets Vice.
AVATAR: [Presenter in tech-casual at a multi-monitor station.
Describe clothing, monitor content, desk items, lighting.]
STYLE — DECONSTRUCTED (Brody): Dark grey #1a1a1a, rust orange #D4501E.
Type at angles, overlapping. Gritty textures. Smash cut transitions.
CRITICAL ON-SCREEN TEXT:
- [List every stat, quote, handle that must appear]
SCENE 1 — A-ROLL (8s): Hook with energy. State what's happening.
SCENE 2 — B-ROLL (12s): First story with layered visuals (L1-L5).
SCENE 3 — A-ROLL + OVERLAY (10s): Second story, split frame.
SCENE 4 — B-ROLL (10s): Third story or dramatic data point.
SCENE 5 — A-ROLL (8s): Wrap-up and forward look.Product Comparison
FORMAT: 60-second comparison. [Product A] vs [Product B]. Data-driven.
AVATAR: [Presenter in review studio. Desk with both products visible.]
STYLE — DIGITAL GRID (Crouwel): Dark #0a0a0a, cyan #00D4FF and amber #FFB800.
Two-color coding: cyan = Product A, amber = Product B. Monospaced type.
CRITICAL ON-SCREEN TEXT:
- [Key stats for each product]
- [Pricing, features, differentiators]
Use SPLIT FRAME B-roll: Product A left, Product B right.Strategy Presentation
FORMAT: 90-second strategy briefing. Bloomberg meets board meeting.
AVATAR: [Executive in blazer over tee. Conference room with whiteboard frameworks.]
STYLE — SWISS PULSE (Müller-Brockmann): Black/white + blue #0066FF.
Grid-locked. Helvetica. Animated counters. Grid wipe transitions.
CRITICAL ON-SCREEN TEXT:
- [Framework labels, quadrant labels, key quotes]
Build frameworks visually: draw axes, plot positions, animate labels.Social Ad (30 seconds)
FORMAT: 30-second social ad. Maximum energy. Portrait 9:16.
AVATAR: [Creator-style presenter. Ring light, colorful background.]
STYLE — CARNIVAL SURGE (Lins): Hot pink, yellow, teal. Collage layering.
Text MASSIVE at angles. Confetti. Smash cuts.
Three scenes: Hook (8s) → Value prop (12s) → CTA (10s).
Text fills 50-80% of every frame. Numbers SLAM.Premium Report
FORMAT: 120-second investor-grade report. Understated authority.
AVATAR: [Tailored merino sweater. Architectural room, diffused natural light.]
STYLE — VELVET STANDARD (Vignelli): Black, white, gold #c9a84c.
Thin ALL CAPS, wide spacing. Generous negative space.
Slow cross-dissolves. Numbers fade in with weight.Video Agent Prompt Optimizer
Write effective prompts for the HeyGen Video Agent API. Based on patterns from 40+ produced videos.
The core insight: Video Agent is an HTML interpreter. It renders layouts, typography, and structured content natively. Describe B-roll as layered text motion graphics with action verbs ("slams in," "types on," "counts up") — not layout specs ("upper-left, 48pt").
Reference Files
| File | Load when... |
|---|---|
| visual-styles.md | Choosing a visual style (20 styles with full specs) |
| prompt-examples.md | Writing a prompt from scratch (full production example + templates) |
Workflow: Brief to Prompt
1. Pull data — Research the topic: web search, APIs, internal docs. Gather real quotes, stats, handles 2. Synthesize a thesis — Not a list. A story. "X is happening because Y — here's the proof." Group into 3-5 themes with a narrative arc 3. Choose a style — Match mood first, content second. Ask: "What should the viewer FEEL?" See visual-styles.md 4. Write the avatar — Thematic wardrobe matching content's emotional context. Brand logos and content-specific props in the set (see Avatar Guide below) 5. Extract critical text — List every number, quote, handle, and label that must appear literally 6. Break into scenes — One concept per scene. Rotate scene types. Never 3+ of same type in a row. At least 2 pure B-roll scenes 7. Write voiceover — Spell out numbers in VO ("one-point-eight-five million"), use figures on screen ("1.85M"). Narration on EVERY scene including B-roll 8. Layer each B-roll scene — L1 background, L2 hero, L3 supporting, L4 info bar, L5 effects. Every element must MOVE 9. Add music direction — Reference artists, describe energy arc 10. Add narration style — How to deliver: fast/slow, where to pause, emotional register per section
Prompt Anatomy
Every production-quality prompt follows this structure:
FORMAT: What kind of video, how long, what energy
TONE: Emotional register, references
AVATAR: Detailed physical + environment description (60-100 words)
STYLE: Named aesthetic with colors, typography, motion rules, transitions
CRITICAL ON-SCREEN TEXT: Exact strings that must appear
SCENE-BY-SCENE: Individual scene breakdowns with VO and layered visuals
MUSIC: Genre, reference artists, energy arc
NARRATION STYLE: How to deliver the voiceoverFORMAT
FORMAT: 75-second high-energy tech daily briefing. Think: a creator who just got amazing news.
FORMAT: Bloomberg-style strategy briefing. 100-120 seconds. CEO-delivered.TONE
TONE: Confident, direct, data-backed. Highlights hit hard. Lowlights are honest — no spin.
TONE: Edgy, punk tech commentary. Vice News meets The Face magazine — raw, confrontational.CRITICAL ON-SCREEN TEXT
List every exact string that must appear on screen. Without this, the agent may summarize, round numbers, or rephrase quotes.
CRITICAL ON-SCREEN TEXT (display literally):
- "$141M ARR — All-Time High"
- "1.85M Signups — +28% MoM"
- Quote: "Use technology to serve the message, not distract from it." — Shalev Hani
- "@username" — exact social handleMUSIC & NARRATION
MUSIC: Driving electronic, heavy bass drops on key numbers. Run the Jewels meets
a tech keynote. Builds relentlessly, only softens for customer stories.
NARRATION STYLE: High energy throughout. Let numbers PUNCH — pause before big ones,
then deliver hard. Customer stories get warmth. The close should feel like a mic drop.Avatar Description Guide
The avatar is NOT a fixed headshot — design it for each video like a movie character. Think costume designer + set designer.
Thematic Wardrobe Rule
The avatar's outfit and environment MUST match the content's emotional/cultural context:
| Content Type | Avatar Design | NOT This |
|---|---|---|
| Chinese New Year | Red qipao with gold embroidery, lantern-lit courtyard | "Reporter in a blazer" |
| Breaking tech news | Field reporter, windswept hair, earpiece, city skyline | "Anchor at a desk" |
| Sleep science | Oversized cream knit, cross-legged on bed, warm lamp | "Analyst in a lab" |
| Reddit community | Messy desk, Reddit alien on monitors, upvote arrows on wall | "Researcher in a studio" |
What to Specify
| Element | Weak | Strong |
|---|---|---|
| Clothing | "Business casual" | "Black ribbed merino turtleneck, high collar framing jaw" |
| Environment | "An office" | "Glass-walled conference room. Whiteboard with hand-drawn tier pyramid" |
| Monitor content | "Computer screens" | "Monitor shows scrolling green terminal text and red security alerts" |
| Lighting | "Well lit" | "Cool blue monitor glow from left, warm amber desk lamp from right" |
Template
AVATAR: [Clothing — fabric, color, fit, accessories, posture].
[Setting — specific props, brand logos, what's on the walls].
[Monitors/desk — content visible on screens, items on desk].
[Lighting — direction, color temperature]. [Mood of the space].
60-100 words. 3+ content-specific props. Brand elements visible.Scene Types
| Type | Format | When to Use |
|---|---|---|
| A-ROLL | Avatar speaking to camera | Intros, key insights, CTAs, emotional beats |
| FULL SCREEN B-ROLL | No avatar — motion graphics only | Data visualization, information-dense content |
| A-ROLL + OVERLAY | Split frame: avatar + content | Presenting data while maintaining human connection |
Rotation is mandatory. Never 3+ of the same type in a row. Every prompt needs at least 2 pure B-roll scenes.
Voiceover on EVERY scene. Every B-roll scene MUST include a VOICEOVER: line. Silent B-roll = broken video.
Scene Anatomy
A-ROLL:
SCENE 1 — A-ROLL (10s)
[Avatar center-frame, excited, hands gesturing]
VOICEOVER: "The exact script for this scene."
Lower-third: "TITLE TEXT" white on blue bar.B-ROLL with layers:
SCENE 2 — FULL SCREEN B-ROLL (12s)
[NO AVATAR — motion graphic only]
VOICEOVER: "The exact script for this scene."
LAYER 1: Dark #1a1a1a background with subtle grid lines pulsing.
LAYER 2: "HEADLINE" SLAMS in from left in white Bold 100pt at -5 degrees.
LAYER 3: Three data cards CASCADE from right, staggered 0.3s.
LAYER 4: Bottom ticker SLIDES in: "supporting text scrolling continuously."
LAYER 5: Grid lines RIPPLE outward from impact point.
Hard cut.A-ROLL + OVERLAY:
SCENE 3 — A-ROLL + OVERLAY (10s)
[SPLIT — Avatar LEFT 35%. Content RIGHT 65%. NO overlap.]
Avatar gestures toward content side.
VOICEOVER: "The exact script for this scene."
RIGHT SIDE: "HEADLINE" in cyan 60pt. Three stats COUNT UP below.Alternate which side the avatar appears on between overlay scenes.
The Visual Layer System
Break B-roll into 5 stacked layers. This is the most powerful technique for motion graphics scenes.
| Layer | Purpose | Examples |
|---|---|---|
| L1 | Background | Textured surface, grid, gradient, color field |
| L2 | Hero content | Main headline/number that dominates the frame |
| L3 | Supporting data | Cards, stats, bullet points, secondary information |
| L4 | Information bar | Tickers, labels, source attributions, quotes |
| L5 | Effects | Particles, glitches, grid animations, ambient motion |
Every B-roll: 4+ layers. Every overlay content side: 3+ layers. Every element must MOVE.
Motion Vocabulary
High Energy
| Verb | Example |
|---|---|
| SLAMS | "$95M" SLAMS in from left at -5 degrees |
| CRASHES | Title CRASHES in from right, screen-shake on impact |
| PUNCHES | Quote card PUNCHES up from bottom |
| STAMPS | Data blocks STAMP in staggered 0.4s |
| SHATTERS | Text SHATTERS after 1.5s, revealing number underneath |
Medium Energy
| Verb | Example |
|---|---|
| CASCADE | Three cards CASCADE from top, staggered 0.3s |
| SLIDES | Ticker SLIDES in from right — continuous scroll |
| DROPS | "TIER 1" DROPS in with white flash |
| FILLS | Progress bar FILLS 0 to 90% in orange |
| DRAWS | Chart line DRAWS itself left to right |
Low Energy
| Verb | Example |
|---|---|
| types on | Quote types on word by word in italic white |
| fades in | Logo fades in at center, held for 3 seconds |
| FLOATS | Bokeh orbs FLOAT across frame at different speeds |
| morphs | Number morphs from 17 to 18.9 |
| COUNTS UP | "1.85M" COUNTS UP from 0 in amber 96pt |
Transition Types
| Transition | Energy | Styles It Fits |
|---|---|---|
| Smash cut | Aggressive | Deconstructed, Maximalist, Carnival Surge |
| White flash frame | Punchy | Deconstructed, Maximalist |
| Grid wipe | Systematic | Swiss Pulse, Digital Grid |
| Hard cut | Clean | Swiss Pulse, Shadow Cut |
| Liquid dissolve | Elegant | Data Drift, Dream State |
| Slow cross-dissolve | Refined | Velvet Standard |
| Pop cut / bounce | Fun | Play Mode, Carnival Surge |
| Snap cut | Urgent | Red Wire, Contact Sheet |
| Soft dissolve | Warm | Soft Signal, Warm Grain, Quiet Drama |
| Iris wipe | Nostalgic | Heritage Reel |
Timing Guidelines
| Content Type | Duration |
|---|---|
| Hook/Intro (A-roll) | 6-10 seconds |
| Data-heavy B-roll | 10-15 seconds (NEVER ≤5s — causes black frames) |
| A-roll + Overlay | 8-12 seconds |
| CTA / Close (A-roll) | 6-8 seconds |
Common video lengths: Social clip: 30-45s (5-7 scenes) | Briefing: 60-75s (7-9 scenes) | Deep dive: 90-120s (10-13 scenes)
Speaking pace: ~150 words/minute. Calculate: words / 150 * 60 = seconds
What Doesn't Work
Patterns that consistently produce poor results:
Layout language — Screen coordinates cause empty/black B-roll:
❌ "UPPER-LEFT: headline in 48pt Helvetica"
❌ "CENTER-SCREEN: display at coordinates (400, 300)"
✅ "135K" SLAMS in from left, white Impact 120pt, fills 40% of frame.Named artists without specs — "Ikko Tanaka style" means nothing to Video Agent. Translate to concrete rules:
❌ "Use an Ikko Tanaka style"
✅ "Flat color blocks, maximum 3 colors per frame, 60% negative space, typography as primary element"Style examples injected into prompts — Full example scenes from a style library confuse the agent. Use the style's rules, not example scenes.
Forced short B-roll (≤5 seconds) — Too short for rendering. Every tested video with 5s B-roll had empty/black screens. Use 10-15s.
Content as a list, not a story — "Here are 5 tweets" produces flat videos. Always synthesize: "X is happening because Y — here's the proof."
Production Insights
Style Performance (from 40+ videos)
| Rank | Style | Strength |
|---|---|---|
| 1 | Deconstructed (Brody) | Most reliable across all topics |
| 2 | Swiss Pulse (Müller-Brockmann) | Best for data-heavy content |
| 3 | Digital Grid (Crouwel) | Strong for tech topics |
| 4 | Geometric Bold (Tanaka) | Elegant and versatile |
| 5 | Maximalist Type (Scher) | High energy, use sparingly |
Duration by Approach
| Approach | Avg Duration | Quality |
|---|---|---|
| Natural storyboard + custom avatar | ~106s | Best |
| Natural storyboard, no custom avatar | ~69s | Good |
| Forced short scenes + custom avatar | ~71s | Mixed |
| Layout language prompts | ~48s | Poor |
Quality Checklist
- [ ] Thesis-driven — story, not bullet points
- [ ] Style named with colors, typography, motion, transitions (see visual-styles.md)
- [ ] Avatar has thematic wardrobe + branded environment (60-100 words)
- [ ] Critical text listed — every stat, quote, label
- [ ] Scenes rotate types — never 3+ same type. At least 2 B-roll scenes
- [ ] Every scene has VOICEOVER — including B-roll
- [ ] B-roll scenes have 4+ layers, every element has motion verbs
- [ ] B-roll scenes are 10-15 seconds (never ≤5s)
- [ ] Brand logos appear when discussing companies
- [ ] Every element moves — no static frames
HeyGen Quota and Credits
HeyGen uses a credit-based system for video generation. Understanding quota management helps prevent failed video generation requests.
Checking Remaining Quota
curl
curl -X GET "https://api.heygen.com/v2/user/remaining_quota" \
-H "X-Api-Key: $HEYGEN_API_KEY"TypeScript
interface QuotaResponse {
error: null | string;
data: {
remaining_quota: number;
used_quota: number;
};
}
const response = await fetch("https://api.heygen.com/v2/user/remaining_quota", {
headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! },
});
const { data }: QuotaResponse = await response.json();
console.log(`Remaining credits: ${data.remaining_quota}`);Python
import requests
import os
response = requests.get(
"https://api.heygen.com/v2/user/remaining_quota",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()["data"]
print(f"Remaining credits: {data['remaining_quota']}")Response Format
{
"error": null,
"data": {
"remaining_quota": 450,
"used_quota": 50
}
}Credit Consumption
Different operations consume different amounts of credits:
| Operation | Credit Cost | Notes |
|---|---|---|
| Standard video (1 min) | ~1 credit per minute | Varies by resolution |
| 720p video | Base rate | Standard quality |
| 1080p video | ~1.5x base rate | Higher quality |
| Video translation | Varies | Depends on video length |
| Streaming avatar | Per session | Real-time usage |
Pre-Generation Quota Check
Always verify sufficient quota before generating videos:
async function generateVideoWithQuotaCheck(videoConfig: VideoConfig) {
// Check quota first
const quotaResponse = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data: quota } = await quotaResponse.json();
// Estimate required credits (rough estimate: 1 credit per minute)
const estimatedMinutes = videoConfig.estimatedDuration / 60;
const requiredCredits = Math.ceil(estimatedMinutes);
if (quota.remaining_quota < requiredCredits) {
throw new Error(
`Insufficient credits. Need ${requiredCredits}, have ${quota.remaining_quota}`
);
}
// Proceed with video generation
return generateVideo(videoConfig);
}Quota Management Best Practices
1. Monitor Usage Regularly
async function logQuotaUsage() {
const response = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
console.log({
remaining: data.remaining_quota,
used: data.used_quota,
percentUsed: (
(data.used_quota / (data.remaining_quota + data.used_quota)) *
100
).toFixed(1),
});
}2. Set Up Alerts
const QUOTA_WARNING_THRESHOLD = 50;
async function checkQuotaWithAlert() {
const response = await fetch(
"https://api.heygen.com/v2/user/remaining_quota",
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const { data } = await response.json();
if (data.remaining_quota < QUOTA_WARNING_THRESHOLD) {
// Send alert (email, Slack, etc.)
await sendAlert(`Low HeyGen quota: ${data.remaining_quota} credits remaining`);
}
return data;
}3. Use Test Mode for Development
When available, use test mode to avoid consuming credits during development:
const videoConfig = {
test: true, // Use test mode during development
video_inputs: [...],
};
// Test videos may have watermarks but don't consume creditsSubscription Tiers
Different subscription tiers have different quota allocations and features:
| Tier | Features |
|---|---|
| Free | Limited credits, basic features |
| Creator | More credits, standard avatars |
| Team | Higher limits, team collaboration |
| Enterprise | Custom limits, API access, priority support |
API access typically requires Enterprise tier or higher.
Error Handling for Quota Issues
async function handleQuotaError(error: any) {
if (error.message.includes("quota") || error.message.includes("credit")) {
console.error("Quota exceeded. Consider:");
console.error("1. Upgrading your subscription");
console.error("2. Waiting for quota reset");
console.error("3. Purchasing additional credits");
// Check current quota
const quota = await getQuota();
console.error(`Current remaining: ${quota.remaining_quota}`);
}
throw error;
}Video Agent API
The Video Agent API generates complete videos from a single text prompt. Unlike the standard video generation API which requires detailed scene-by-scene configuration, Video Agent automatically handles script writing, avatar selection, visuals, voiceover, pacing, and captions.
MCP Tool (Preferred)
If the HeyGen MCP server is connected, use mcp__heygen__generate_video_agent instead of direct API calls:
Tool: mcp__heygen__generate_video_agent
Parameters:
prompt: "<optimized prompt from prompt-optimizer.md>"
config:
duration_sec: 90 # optional, 5-300
avatar_id: "avatar_id" # optional, agent selects if omitted
orientation: "landscape" # optional, "landscape" or "portrait"
files: # optional
- asset_id: "uploaded_asset_id"Then check status with mcp__heygen__get_video using the returned video_id.
The prompt quality is still the critical factor — always follow prompt-optimizer.md regardless of whether you use MCP or direct API.
When to Use Video Agent vs Standard API
| Use Case | Recommended API |
|---|---|
| Quick video from idea | Video Agent |
| Precise control over scenes, avatars, timing | Standard v2/video/generate |
| Automated content generation at scale | Video Agent |
| Specific avatar with exact script | Standard v2/video/generate |
| Prototype or draft video | Video Agent |
| Brand-consistent production video | Standard v2/video/generate |
Before You Call This API
Required step: Optimize your prompt using prompt-optimizer.md before generating a video. The difference between mediocre and professional results depends entirely on prompt quality.
Quick checklist: 1. Define visual style (colors, aesthetic) — see visual-styles.md 2. Structure scenes with specific scene types 3. Write VO script at ~150 words/minute 4. Specify media types for each scene (Motion Graphics, Stock, AI-generated)
Direct API Endpoint
POST https://api.heygen.com/v1/video_agent/generateRequest Fields
| Field | Type | Req | Description |
|---|---|---|---|
prompt | string | ✓ | Text prompt describing the video you want |
config | object | Configuration options (see below) | |
files | array | Asset files to reference in generation | |
callback_id | string | Custom ID for tracking. Requires `callback_url` to also be set — omit both if you don't need webhooks | |
callback_url | string | Webhook URL for completion notification |
Config Object
| Field | Type | Description |
|---|---|---|
duration_sec | integer | Approximate duration in seconds (5-300) |
avatar_id | string | Specific avatar to use (optional - agent selects if not provided) |
orientation | string | "portrait" or "landscape" |
Files Array
| Field | Type | Description |
|---|---|---|
asset_id | string | Asset ID of uploaded file to reference |
Response Format
{
"error": null,
"data": {
"video_id": "abc123"
}
}curl Example
curl -X POST "https://api.heygen.com/v1/video_agent/generate" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Create a 60-second product demo video for a new AI-powered calendar app. The tone should be professional but friendly, targeting busy professionals. Highlight the smart scheduling feature and time zone handling."
}'TypeScript
interface VideoAgentConfig {
duration_sec?: number; // 5-300 seconds
avatar_id?: string; // Optional: specific avatar
orientation?: "portrait" | "landscape";
}
interface VideoAgentFile {
asset_id: string;
}
interface VideoAgentRequest {
prompt: string; // Required
config?: VideoAgentConfig;
files?: VideoAgentFile[];
callback_id?: string; // Requires callback_url if set
callback_url?: string;
}
interface VideoAgentResponse {
error: string | null;
data: {
video_id: string;
};
}
async function generateWithVideoAgent(
prompt: string,
config?: VideoAgentConfig
): Promise<string> {
const request: VideoAgentRequest = { prompt };
if (config) {
request.config = config;
}
const response = await fetch(
"https://api.heygen.com/v1/video_agent/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
}
);
const json: VideoAgentResponse = await response.json();
if (json.error) {
throw new Error(`Video Agent failed: ${json.error}`);
}
return json.data.video_id;
}Python
import requests
import os
from typing import Optional
def generate_with_video_agent(
prompt: str,
duration_sec: Optional[int] = None,
avatar_id: Optional[str] = None,
orientation: Optional[str] = None
) -> str:
request_body = {"prompt": prompt}
config = {}
if duration_sec:
config["duration_sec"] = duration_sec
if avatar_id:
config["avatar_id"] = avatar_id
if orientation:
config["orientation"] = orientation
if config:
request_body["config"] = config
response = requests.post(
"https://api.heygen.com/v1/video_agent/generate",
headers={
"X-Api-Key": os.environ["HEYGEN_API_KEY"],
"Content-Type": "application/json"
},
json=request_body
)
data = response.json()
if data.get("error"):
raise Exception(f"Video Agent failed: {data['error']}")
return data["data"]["video_id"]Examples
Basic: Prompt Only
const videoId = await generateWithVideoAgent(
"Create a 30-second welcome video for new employees at a tech startup. Keep it energetic and modern."
);With Duration and Orientation
const videoId = await generateWithVideoAgent(
"Explain the benefits of cloud computing for small businesses. Use simple language and real-world examples.",
{
duration_sec: 90,
orientation: "landscape"
}
);With Specific Avatar
const videoId = await generateWithVideoAgent(
"Present quarterly sales results. Professional tone, data-focused.",
{
duration_sec: 120,
avatar_id: "josh_lite3_20230714",
orientation: "landscape"
}
);With Reference Files
Upload assets first, then reference them:
// 1. Upload reference materials (see assets.md)
const logoAssetId = await uploadFile("./company-logo.png", "image/png");
const productImageId = await uploadFile("./product-screenshot.png", "image/png");
// 2. Generate video with references
const response = await fetch(
"https://api.heygen.com/v1/video_agent/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "Create a product demo video showcasing our new dashboard feature. Use the uploaded screenshots as visual references.",
config: {
duration_sec: 60,
orientation: "landscape"
},
files: [
{ asset_id: logoAssetId },
{ asset_id: productImageId }
]
}),
}
);Writing Effective Prompts
See [prompt-optimizer.md](prompt-optimizer.md) for comprehensive prompt writing guidance.
The prompt optimizer covers:
- Prompt complexity levels (basic → scene-by-scene)
- Visual style taxonomy and color specification
- Media type selection (Motion Graphics vs Stock vs AI-generated)
- Scene structure and timing calculations
- Ready-to-use templates for common video types
Checking Video Status
Video Agent returns a video_id - use the standard status endpoint to check progress:
// Same polling as standard video generation
const videoUrl = await waitForVideo(videoId);See video-status.md for polling implementation.
Comparison: Video Agent vs Standard API
Video Agent Request
// Simple: describe what you want
const videoId = await generateWithVideoAgent(
"Create a 60-second tutorial on setting up two-factor authentication. Professional tone, step-by-step."
);Equivalent Standard API Request
// Complex: specify every detail
const videoId = await generateVideo({
video_inputs: [
{
character: {
type: "avatar",
avatar_id: "josh_lite3_20230714",
avatar_style: "normal",
},
voice: {
type: "text",
input_text: "Welcome to this tutorial on two-factor authentication...",
voice_id: "1bd001e7e50f421d891986aad5158bc8",
},
background: {
type: "color",
value: "#1a1a2e",
},
},
// ... more scenes for each step
],
dimension: { width: 1920, height: 1080 },
});Limitations
- Less control over exact script wording
- Avatar selection may vary if not specified
- Scene composition is automated
- May not match precise brand guidelines
- Duration is approximate, not exact
Best Practices
1. Be specific in prompts - More detail = better results 2. Specify duration - Use config.duration_sec for predictable length 3. Lock avatar if needed - Use config.avatar_id for consistency 4. Upload reference files - Help agent understand your brand/product 5. Iterate on prompts - Refine based on results 6. Use for drafts - Video Agent is great for quick iterations before final production
Video Status and Polling
After generating a video, you need to poll for status until the video is complete. HeyGen processes videos asynchronously.
MCP Tool (Preferred)
If the HeyGen MCP server is connected, use mcp__heygen__get_video with the videoId parameter. It returns status, video_url, thumbnail_url, duration, title, gif_url, captioned_video_url, and other metadata in a single call.
Checking Video Status (Direct API)
curl
curl -X GET "https://api.heygen.com/v2/videos/YOUR_VIDEO_ID" \
-H "X-Api-Key: $HEYGEN_API_KEY"TypeScript
interface VideoStatusResponse {
error: null | string;
data: {
id: string;
status: "pending" | "processing" | "completed" | "failed";
video_url?: string;
thumbnail_url?: string;
duration?: number;
title?: string;
created_at?: string;
completed_at?: string;
gif_url?: string;
captioned_video_url?: string;
subtitle_url?: string;
folder_id?: string;
output_language?: string;
failure_code?: string;
failure_message?: string;
};
}
async function getVideoStatus(videoId: string): Promise<VideoStatusResponse["data"]> {
const response = await fetch(
`https://api.heygen.com/v2/videos/${videoId}`,
{ headers: { "X-Api-Key": process.env.HEYGEN_API_KEY! } }
);
const json: VideoStatusResponse = await response.json();
if (json.error) {
throw new Error(json.error);
}
return json.data;
}Python
import requests
import os
def get_video_status(video_id: str) -> dict:
response = requests.get(
f"https://api.heygen.com/v2/videos/{video_id}",
headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
)
data = response.json()
if data.get("error"):
raise Exception(data["error"])
return data["data"]Video Status Types
| Status | Description |
|---|---|
pending | Video is queued for processing |
processing | Video is being generated |
completed | Video is ready for download |
failed | Video generation failed |
Expected Generation Times
Video generation typically takes 5-15 minutes, but can exceed 20 minutes during peak load or for longer scripts.
| Factor | Impact |
|---|---|
| Script length | Longer scripts = significantly longer processing |
| Resolution | 1080p takes longer than 720p |
| Avatar complexity | Some avatars render faster |
| Queue load | Peak hours may cause 15-20+ minute waits |
| Multiple scenes | Each scene adds processing time |
Recommendations:
- Set timeout to 15-20 minutes (900,000-1,200,000 ms) for safety
- For scripts > 2 minutes of speech, expect 15+ minutes
- Consider async patterns (save video_id, check later) for long videos
Response Format
Completed Video
{
"error": null,
"data": {
"id": "abc123",
"status": "completed",
"video_url": "https://files.heygen.ai/video/abc123.mp4",
"thumbnail_url": "https://files.heygen.ai/thumbnail/abc123.jpg",
"duration": 45.2,
"title": "My Video",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:38:00Z",
"gif_url": "https://files.heygen.ai/gif/abc123.gif",
"captioned_video_url": null,
"subtitle_url": null,
"folder_id": null,
"output_language": "en"
}
}Failed Video
{
"error": null,
"data": {
"id": "abc123",
"status": "failed",
"failure_code": "script_too_long",
"failure_message": "Script too long for selected avatar"
}
}Polling Implementation
Basic Polling
async function waitForVideo(
videoId: string,
maxWaitMs = 600000, // 10 minutes
pollIntervalMs = 5000 // 5 seconds
): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const status = await getVideoStatus(videoId);
switch (status.status) {
case "completed":
return status.video_url!;
case "failed":
throw new Error(status.failure_message || "Video generation failed");
case "pending":
case "processing":
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
break;
}
}
throw new Error("Video generation timed out");
}Polling with Progress Callback
type ProgressCallback = (status: string, elapsed: number) => void;
async function waitForVideoWithProgress(
videoId: string,
onProgress?: ProgressCallback,
maxWaitMs = 600000,
pollIntervalMs = 5000
): Promise<string> {
const startTime = Date.now();
while (Date.now() - startTime < maxWaitMs) {
const elapsed = Date.now() - startTime;
const status = await getVideoStatus(videoId);
onProgress?.(status.status, elapsed);
switch (status.status) {
case "completed":
return status.video_url!;
case "failed":
throw new Error(status.failure_message || "Video generation failed");
default:
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
}
throw new Error("Video generation timed out");
}
// Usage
const videoUrl = await waitForVideoWithProgress(
videoId,
(status, elapsed) => {
console.log(`Status: ${status}, Elapsed: ${Math.round(elapsed / 1000)}s`);
}
);Python Polling
import time
from typing import Optional, Callable
def wait_for_video(
video_id: str,
max_wait_seconds: int = 600,
poll_interval: int = 5,
on_progress: Optional[Callable[[str, int], None]] = None
) -> str:
start_time = time.time()
while time.time() - start_time < max_wait_seconds:
elapsed = int(time.time() - start_time)
status_data = get_video_status(video_id)
status = status_data["status"]
if on_progress:
on_progress(status, elapsed)
if status == "completed":
return status_data["video_url"]
elif status == "failed":
raise Exception(status_data.get("failure_message", "Video generation failed"))
time.sleep(poll_interval)
raise Exception("Video generation timed out")
# Usage
def progress_callback(status: str, elapsed: int):
print(f"Status: {status}, Elapsed: {elapsed}s")
video_url = wait_for_video(video_id, on_progress=progress_callback)Downloading the Video
Once the video is complete, download it. Important: The video URL may not be immediately available after status shows "completed". Use retry logic with backoff.
TypeScript (with retry)
import fs from "fs";
import path from "path";
async function downloadVideoWithRetry(
videoUrl: string,
outputPath = "./output/video.mp4",
maxRetries = 5,
initialDelayMs = 2000
): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
fs.writeFileSync(path.resolve(outputPath), Buffer.from(arrayBuffer));
console.log(`Video downloaded to ${outputPath}`);
return;
} catch (error) {
lastError = error as Error;
const delay = initialDelayMs * Math.pow(2, attempt); // Exponential backoff
console.log(`Download attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error(`Failed to download after ${maxRetries} attempts: ${lastError?.message}`);
}Python (with retry)
import requests
import time
def download_video_with_retry(
video_url: str,
output_path: str,
max_retries: int = 5,
initial_delay: float = 2.0
) -> None:
last_error = None
for attempt in range(max_retries):
try:
response = requests.get(video_url, stream=True, timeout=60)
response.raise_for_status()
with open(output_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Video downloaded to {output_path}")
return
except Exception as e:
last_error = e
delay = initial_delay * (2 ** attempt) # Exponential backoff
print(f"Download attempt {attempt + 1} failed, retrying in {delay}s...")
time.sleep(delay)
raise Exception(f"Failed to download after {max_retries} attempts: {last_error}")Simple Download (no retry)
For quick scripts where you'll retry manually:
async function downloadVideo(videoUrl: string, outputPath = "./output/video.mp4") {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(`Failed to download: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
fs.writeFileSync(path.resolve(outputPath), Buffer.from(arrayBuffer));
}Complete Workflow Example
async function generateAndDownloadVideo(config: VideoConfig): Promise<string> {
// 1. Generate video
const generateResponse = await fetch(
"https://api.heygen.com/v2/video/generate",
{
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
}
);
const { data: generateData } = await generateResponse.json();
const videoId = generateData.video_id;
console.log(`Video ID: ${videoId}`);
// 2. Poll for completion
const videoUrl = await waitForVideoWithProgress(
videoId,
(status, elapsed) => {
console.log(`[${Math.round(elapsed / 1000)}s] Status: ${status}`);
}
);
// 3. Download
const outputPath = `./output/${videoId}.mp4`;
await downloadVideo(videoUrl, outputPath);
return outputPath;
}Resumable Status Checking
For long-running generations, save the video_id and check status later rather than keeping a process waiting.
Save State After Generation
interface PendingVideo {
videoId: string;
createdAt: string;
script: string;
avatarId: string;
voiceId: string;
}
async function startVideoGeneration(config: VideoGenerateRequest): Promise<PendingVideo> {
const videoId = await generateVideo(config);
const pending: PendingVideo = {
videoId,
createdAt: new Date().toISOString(),
script: config.video_inputs[0].voice.input_text!,
avatarId: config.video_inputs[0].character.avatar_id!,
voiceId: config.video_inputs[0].voice.voice_id!,
};
// Save to file for later retrieval
fs.writeFileSync("pending-video.json", JSON.stringify(pending, null, 2));
console.log(`Video generation started. ID: ${videoId}`);
console.log("Check status later with: checkVideoStatus()");
return pending;
}Check Status Later
async function checkVideoStatus(): Promise<void> {
if (!fs.existsSync("pending-video.json")) {
console.log("No pending video found");
return;
}
const pending: PendingVideo = JSON.parse(
fs.readFileSync("pending-video.json", "utf-8")
);
const elapsed = Date.now() - new Date(pending.createdAt).getTime();
console.log(`Checking video ${pending.videoId} (started ${Math.round(elapsed / 60000)} min ago)...`);
const status = await getVideoStatus(pending.videoId);
switch (status.status) {
case "completed":
console.log(`Video ready: ${status.video_url}`);
console.log(`Duration: ${status.duration}s`);
// Clean up pending file
fs.unlinkSync("pending-video.json");
// Save result
fs.writeFileSync("video-result.json", JSON.stringify({
...pending,
videoUrl: status.video_url,
thumbnailUrl: status.thumbnail_url,
duration: status.duration,
title: status.title,
createdAt: status.created_at,
completedAt: status.completed_at,
}, null, 2));
break;
case "failed":
console.error(`Video failed: ${status.failure_message}`);
fs.unlinkSync("pending-video.json");
break;
default:
console.log(`Status: ${status.status} - check again in a few minutes`);
}
}CLI-Friendly Pattern
// generate-video.ts - Start generation and exit
async function main() {
const pending = await startVideoGeneration(config);
console.log(`\nVideo ID saved. Run 'npx tsx check-status.ts' to check progress.`);
process.exit(0); // Exit immediately, don't wait
}
// check-status.ts - Check and optionally wait
async function main() {
const args = process.argv.slice(2);
const shouldWait = args.includes("--wait");
if (shouldWait) {
// Poll until complete (with 20 min timeout)
const result = await waitForVideo(pending.videoId, apiKey, onProgress, 1200000);
console.log(`Done: ${result.video_url}`);
} else {
// Just check once and report
await checkVideoStatus();
}
}Alternative: Using Webhooks
Instead of polling, you can use webhooks to receive notifications when videos complete. See webhooks.md for details. Webhooks are ideal for production systems where you don't want to maintain polling connections.
Best Practices
1. Use exponential backoff - Increase poll intervals for long-running jobs 2. Set reasonable timeouts - Most videos complete within 10 minutes 3. Handle failures gracefully - Check error messages for actionable feedback 4. Consider webhooks - For production systems, webhooks are more efficient than polling 5. Cache video URLs - Downloaded video URLs are valid for a limited time
Visual Style Library — 20 Styles
Named visual styles for Video Agent prompts. Each is inspired by a real graphic designer. Ordered by mood intensity.
Picking a style: Match mood first, content second. Ask: "What should the viewer FEEL?"
Using a style: Copy the style block into your prompt's STYLE section. Use the visual language rules — don't inject the example B-roll scenes (they confuse the agent).
Custom styles: These are examples. Create your own by combining elements, referencing other designers, art movements, or cultural aesthetics. The pattern: named style + designer reference + color palette + typography + motion rules + transitions.
Quick Reference
| # | Style | Artist | Mood | Best For |
|---|---|---|---|---|
| 1 | Soft Signal | Sagmeister | Intimate, warm | Personal stories, wellness |
| 2 | Warm Grain | Eksell | Organic, friendly | Environmental, sustainability |
| 3 | Quiet Drama | Ray | Humanist, contemplative | Profiles, biographical |
| 4 | Heritage Reel | Cassandre | Nostalgic, vintage | History, retrospectives |
| 5 | Silk Route | Abedini | Flowing, mysterious | Global affairs, cross-cultural |
| 6 | Swiss Pulse | Müller-Brockmann | Clinical, precise | Data-heavy, analytical |
| 7 | Geometric Bold | Tanaka | Minimal, elegant | Lifestyle, visual essays |
| 8 | Velvet Standard | Vignelli | Premium, timeless | Luxury, investor updates |
| 9 | Digital Grid | Crouwel | Systematic, technical | Infrastructure, engineering |
| 10 | Contact Sheet | Brodovitch | Editorial, investigative | Journalism, deep dives |
| 11 | Folk Frequency | Terrazas | Cultural, vivid | Festivals, food, heritage |
| 12 | Earth Pulse | Ghariokwu | Grounded, communal | Community, grassroots |
| 13 | Dream State | Tomaszewski | Surreal, poetic | Op-eds, philosophy |
| 14 | Play Mode | Ahn Sang-soo | Playful, irreverent | Entertainment, pop culture |
| 15 | Carnival Surge | Lins | Euphoric, celebratory | Milestones, hype |
| 16 | Shadow Cut | Hillmann | Dark, cinematic | Exposés, investigations |
| 17 | Deconstructed | Brody | Industrial, raw | Tech news, punk energy |
| 18 | Maximalist Type | Scher | Loud, kinetic | Big announcements, launches |
| 19 | Data Drift | Anadol | Futuristic, immersive | AI/tech, innovation |
| 20 | Red Wire | Tartakover | Urgent, immediate | Breaking news, crisis |
Mood-to-Style Guide
| Content feels... | Use... |
|---|---|
| Personal, intimate | Soft Signal, Quiet Drama |
| Natural, earthy | Warm Grain, Earth Pulse |
| Nostalgic, historical | Heritage Reel |
| Data-driven, analytical | Swiss Pulse, Digital Grid |
| Elegant, premium | Velvet Standard, Geometric Bold |
| Cultural, global | Silk Route, Folk Frequency |
| Investigative, serious | Contact Sheet, Shadow Cut |
| Fun, lighthearted | Play Mode, Carnival Surge |
| Philosophical, abstract | Dream State |
| Punk, grassroots, raw | Deconstructed |
| Hype, loud, high-energy | Maximalist Type |
| Tech-forward, futuristic | Data Drift |
| Breaking, urgent | Red Wire |
---
1. Soft Signal — Stefan Sagmeister
Mood: Intimate, warm | Best for: Personal stories, wellness, reflections
- Warm amber and cream with dusty rose, sage green, honey gold accents
- Handwritten-style text overlays — personal, lowercase, delicate
- Close-up framing: hands, faces, textures. Macro lens feel
- Slow drifts and floats, never snaps. Soft dissolves, warm light leaks
STYLE — SOFT SIGNAL (Sagmeister): Warm amber/cream, dusty rose, sage green.
Handwritten-style text. Close-up framing. Slow drifts and floats.
Soft dissolves with warm light leaks.2. Warm Grain — Olle Eksell
Mood: Organic, friendly | Best for: Environmental, sustainability, community
- Earth tones: ochre, forest green, terracotta, cream, soft brown
- Rounded sans-serif type. Organic rounded compositions — nothing angular
- 16mm film grain, slightly desaturated. Natural textures: wood, linen, stone
- Gentle wipes, soft cuts, unhurried
STYLE — WARM GRAIN (Eksell): Earth tones — ochre, forest green, terracotta, cream.
Organic rounded compositions. 16mm film grain. Rounded sans-serif.
Gentle wipes and soft cuts.3. Quiet Drama — Satyajit Ray
Mood: Humanist, contemplative | Best for: Profiles, biographical, cultural
- Muted warm: sepia, deep brown, soft gold, off-white, charcoal
- Clean serif type, positioned with care. Portrait framing
- Strong single-source contrast: window light, single lamp
- Deliberate pacing, longer holds. Slow fades to black
STYLE — QUIET DRAMA (Ray): Muted warm — sepia, deep brown, soft gold.
Portrait framing. Clean serif. Strong single-source contrast.
Slow fades to black.4. Heritage Reel — Cassandre
Mood: Nostalgic, vintage | Best for: History, retrospectives, brand origins
- Faded gold, deep burgundy, navy, cream, sepia wash
- Elegant centered serif like classic film title cards
- Vignetting, softened edges. Film grain, light scratches, gentle jitter
- Iris wipes, film reel flicker
STYLE — HERITAGE REEL (Cassandre): Faded gold, burgundy, navy, sepia wash.
Elegant centered serif. Vignetting and aged film grain.
Iris wipe transitions.5. Silk Route — Reza Abedini
Mood: Flowing, mysterious | Best for: Global affairs, cross-cultural, art/design
- Rich jewel tones: deep teal, burgundy, gold, lapis blue, black
- Elegant spaced type along natural visual lines
- Layered compositions — foreground, midground, background all active
- Flowing dissolves, smooth morphs
STYLE — SILK ROUTE (Abedini): Jewel tones — deep teal, burgundy, gold, lapis blue.
Layered compositions, all depths active. Elegant spaced type.
Flowing dissolves and smooth morphs.6. Swiss Pulse — Josef Müller-Brockmann
Mood: Clinical, precise | Best for: Data-heavy, analytical, financial, metrics
- Black (#1a1a1a), white, ONE accent: electric blue (#0066FF)
- Helvetica Bold headlines, Regular labels. Numbers LARGE (80-120pt)
- Grid-locked compositions. Every element snaps to 12-column grid
- Animated counters COUNT UP from 0. Diagonal compositions on key moments
- Grid wipes, hard cuts. No dissolves
STYLE — SWISS PULSE (Müller-Brockmann): Black/white + electric blue #0066FF.
Grid-locked. Helvetica Bold. Animated counters. Diagonal accents.
Grid wipe transitions.7. Geometric Bold — Ikko Tanaka
Mood: Minimal, elegant | Best for: Clean lifestyle, culture, visual essays, brand profiles
- Maximum 3 flat colors per frame — no gradients
- Bold clean type as primary visual element
- Asymmetric composition, 60% negative space minimum. Single focal point
- Clean cuts on beat, no effects
STYLE — GEOMETRIC BOLD (Tanaka): Max 3 flat colors per frame.
60% negative space. Bold type as primary element.
Single focal point. Clean cuts on beat.8. Velvet Standard — Massimo Vignelli
Mood: Premium, timeless | Best for: Luxury, investor updates, keynotes, product showcases
- Black, white, ONE rich accent: deep navy (#1a237e) or gold (#c9a84c)
- Thin sans-serif, ALL CAPS, letter-spaced wide
- Generous negative space. Symmetrical, centered, architectural precision
- Slow, deliberate. Sequential reveals. Elegant cross-dissolves
STYLE — VELVET STANDARD (Vignelli): Black, white, one accent: gold #c9a84c.
Thin ALL CAPS, wide spacing. Generous negative space.
Slow elegant cross-dissolves.9. Digital Grid — Wim Crouwel
Mood: Systematic, technical | Best for: Infrastructure, engineering, code, tech
- Dark (#0a0a0a) with cyan (#00E5FF), amber (#FFB300), green (#00FF88)
- Monospaced type throughout. Code-terminal aesthetic
- Pixel grid overlays visible. Everything snaps to system
- Grid nodes light up sequentially. Scan-line effects, cursor blinks
- Clean wipe transitions
STYLE — DIGITAL GRID (Crouwel): Monospaced type. Dark #0a0a0a with cyan #00E5FF, amber #FFB300.
Pixel grid overlays. Terminal aesthetic. Clean wipe transitions.10. Contact Sheet — Alexey Brodovitch
Mood: Editorial, investigative | Best for: Journalism, deep dives, research breakdowns
- High contrast B&W with occasional desaturated color accents
- Bold sans-serif captions like editorial annotations
- Photo-editorial framing — multiple images, contact-sheet energy
- Raw grain, imperfect focus. Tight crops on faces and hands
- Hard cuts on beat, snap-zooms
STYLE — CONTACT SHEET (Brodovitch): High contrast B&W, desaturated accents.
Photo-editorial framing. Bold sans-serif annotations. Raw grain.
Hard cuts on beat. Snap-zooms.11. Folk Frequency — Eduardo Terrazas
Mood: Cultural, vivid | Best for: Cultural events, food, tradition, heritage
- Vivid folk: hot pink, bright orange, cobalt blue, sun yellow, emerald
- Bold warm rounded type. Pattern and repetition — folk art rhythms
- Rich textures: woven fabrics, painted surfaces, ceramic, handmade
- Colorful wipes, quick cuts on festive rhythm
STYLE — FOLK FREQUENCY (Terrazas): Vivid folk — hot pink, cobalt blue, sun yellow, emerald.
Bold rounded type. Folk art rhythms. Rich handmade textures.
Colorful wipes on festive rhythm.12. Earth Pulse — Lemi Ghariokwu
Mood: Grounded, communal | Best for: Community, music/culture, grassroots
- Warm saturated: burnt orange, deep green, rich yellow, terracotta
- Bold expressive type, center-frame. Wide community framing
- Rhythmic editing timed to musical beats
- Rhythmic cuts on beat, freeze-frames for emphasis
STYLE — EARTH PULSE (Ghariokwu): Warm saturated — burnt orange, deep green, rich yellow.
Bold expressive type. Wide community framing.
Rhythmic cuts on beat. Freeze-frames.13. Dream State — Henryk Tomaszewski
Mood: Surreal, poetic | Best for: Op-eds, philosophy, think pieces, speculative
- Muted palette with one surreal accent: dusty blues, grey-greens, then shock of red or gold
- Sparse precise text — few words, maximum impact. Thin elegant floating type
- Unusual juxtapositions. Dreamlike quality: soft edges, atmospheric haze
- Slow morph dissolves. NEVER hard cuts
STYLE — DREAM STATE (Tomaszewski): Muted palette + one surreal accent.
Thin elegant floating type. Soft edges, atmospheric haze.
Slow morph dissolves — NEVER hard cuts.14. Play Mode — Ahn Sang-soo
Mood: Playful, irreverent | Best for: Entertainment, pop culture, listicles, fun
- Bright candy: electric blue, hot pink, lime green, yellow, white
- Bouncy oversized tilted text. Asymmetric off-kilter compositions
- Quick cuts (1-3 seconds). Score cards, achievement popups, XP bars
- Bouncy spring physics — text overshoots and settles, screen shakes
- Pop cuts, whip pans, bounce effects
STYLE — PLAY MODE (Ahn Sang-soo): Electric blue, hot pink, lime green.
Bouncy spring physics. Oversized tilted text. Score cards, XP bars.
Pop cuts, bounce effects.15. Carnival Surge — Rico Lins
Mood: Euphoric, celebratory | Best for: Big announcements, milestones, celebrations, hype
- Maximum color: hot pink (#FF1493), electric yellow (#FFE000), teal (#00CED1), orange, violet
- MASSIVE bold text at ANGLES over footage. Collage-style overlapping
- Rapid 1-2 second clips. Confetti, lights, constant energy
- Smash cuts, flash frames, rapid-fire montage
STYLE — CARNIVAL SURGE (Lins): Max color — hot pink #FF1493, yellow #FFE000, teal #00CED1.
Collage layering. Text MASSIVE at ANGLES. Confetti bursts.
Smash cuts, flash frames.16. Shadow Cut — Hans Hillmann
Mood: Dark, cinematic | Best for: Exposés, investigations, controversy, dark deep dives
- Near-monochrome: deep blacks, cold greys, stark white + blood red or toxic green
- Sharp angular text like film noir title cards
- Heavy shadow — faces half-lit, objects emerging from darkness
- Slow creeping push-ins, slow reveals, tension
- Iris to black, slow fade from darkness, hard cuts to silence
STYLE — SHADOW CUT (Hillmann): Deep blacks, cold greys + blood red accent.
Sharp angular text. Heavy shadow. Slow creeping push-ins.
Hard cuts to black. Film noir tension.17. Deconstructed — Neville Brody
Mood: Industrial, raw | Best for: Tech news, security, punk energy, counter-culture
- Dark grey (#1a1a1a), black, rust orange (#D4501E), raw white (#f0f0f0)
- Type at angles, overlapping edges, escaping frames. Bold industrial
- High contrast, gritty textures: scratched metal, peeling paint, scan-line glitch
- Text SLAMS, SHATTERS, PUNCHES. Letters scramble then snap
- Smash cuts, glitch transitions, white flash frames
STYLE — DECONSTRUCTED (Brody): Dark grey #1a1a1a, rust orange #D4501E.
Type at angles, overlapping. Gritty textures, scan-line glitch.
Smash cuts with flash frames.18. Maximalist Type — Paula Scher
Mood: Loud, kinetic | Best for: Big announcements, launches, high-energy recaps
- Bold saturated: red, yellow, black, white — maximum contrast
- Text IS the visual. Overlapping layers at different scales and angles, 50-80% of frame
- Kinetic energy: everything moving, slamming, sliding. 1-2 second rapid cuts
- Text layered OVER footage — never empty backgrounds
- Smash cuts, text slamming from edges, flash frames
STYLE — MAXIMALIST TYPE (Scher): Red, yellow, black, white — max contrast.
Text IS the visual. Overlapping at different scales, 50-80% of frame.
Kinetic everything. Smash cuts, flash frames.19. Data Drift — Refik Anadol
Mood: Futuristic, immersive | Best for: AI/tech, speculative, cutting-edge science
- Iridescent: holographic silver, electric purple (#7c3aed), cyan (#06b6d4), deep black (#0a0a0a)
- Thin futuristic sans-serif — minimal, floating, weightless
- Fluid morphing compositions. Extreme scale shifts: microscopic to cosmic
- Particles coalesce into numbers, light traces data paths
- Liquid dissolves, particles dispersing and reforming
STYLE — DATA DRIFT (Anadol): Iridescent — purple #7c3aed, cyan #06b6d4, deep black.
Fluid morphing compositions. Thin futuristic type.
Liquid dissolves. Particles coalesce into numbers.20. Red Wire — David Tartakover
Mood: Urgent, immediate | Best for: Breaking news, crisis updates, alerts
- High alert: red, black, white, emergency yellow — maximum contrast
- Bold condensed all caps — every word screams urgency
- Split screens, ticker-style text bars, timestamp overlays — max information density
- Multiple text elements simultaneously. Handheld energy
- Snap cuts, flash frames, zero breathing room
STYLE — RED WIRE (Tartakover): Red, black, white, emergency yellow.
Bold condensed all-caps. Split screens, tickers, timestamps.
Snap cuts, flash frames. Zero breathing room.Webhooks
Webhooks allow HeyGen to notify your application when events occur, such as video completion. This is more efficient than polling for status updates.
Overview
Instead of repeatedly checking video status, webhooks push notifications to your server when:
- Video generation completes
- Video generation fails
- Translation completes
- Avatar training completes
- Other async operations finish
Setting Up a Webhook Endpoint
Your webhook endpoint should: 1. Accept POST requests 2. Return 200 status quickly 3. Handle events asynchronously
Express.js Example
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
// Webhook endpoint
app.post("/webhook/heygen", async (req, res) => {
// Acknowledge receipt immediately
res.status(200).send("OK");
// Process event asynchronously
processWebhookEvent(req.body).catch(console.error);
});
async function processWebhookEvent(event: HeyGenWebhookEvent) {
console.log(`Received event: ${event.event_type}`);
switch (event.event_type) {
case "avatar_video.success":
await handleVideoSuccess(event);
break;
case "avatar_video.fail":
await handleVideoFailure(event);
break;
case "video_translate.success":
await handleTranslationSuccess(event);
break;
default:
console.log(`Unknown event type: ${event.event_type}`);
}
}
app.listen(3000, () => {
console.log("Webhook server running on port 3000");
});Python Flask Example
from flask import Flask, request, jsonify
import threading
app = Flask(__name__)
@app.route("/webhook/heygen", methods=["POST"])
def heygen_webhook():
event = request.json
# Acknowledge immediately
response = jsonify({"status": "received"})
# Process asynchronously
thread = threading.Thread(
target=process_webhook_event,
args=(event,)
)
thread.start()
return response, 200
def process_webhook_event(event):
event_type = event.get("event_type")
print(f"Received event: {event_type}")
if event_type == "avatar_video.success":
handle_video_success(event)
elif event_type == "avatar_video.fail":
handle_video_failure(event)
elif event_type == "video_translate.success":
handle_translation_success(event)
if __name__ == "__main__":
app.run(port=3000)Webhook Event Types
| Event Type | Description |
|---|---|
avatar_video.success | Video generation completed |
avatar_video.fail | Video generation failed |
video_translate.success | Translation completed |
video_translate.fail | Translation failed |
instant_avatar.success | Instant avatar created |
instant_avatar.fail | Instant avatar creation failed |
Event Payload Structure
Video Success Event
interface VideoSuccessEvent {
event_type: "avatar_video.success";
event_data: {
video_id: string;
video_url: string;
thumbnail_url: string;
duration: number;
callback_id?: string;
};
}{
"event_type": "avatar_video.success",
"event_data": {
"video_id": "abc123",
"video_url": "https://files.heygen.ai/video/abc123.mp4",
"thumbnail_url": "https://files.heygen.ai/thumbnail/abc123.jpg",
"duration": 45.2,
"callback_id": "your_custom_id"
}
}Video Failure Event
interface VideoFailureEvent {
event_type: "avatar_video.fail";
event_data: {
video_id: string;
error: string;
callback_id?: string;
};
}{
"event_type": "avatar_video.fail",
"event_data": {
"video_id": "abc123",
"error": "Script too long for selected avatar",
"callback_id": "your_custom_id"
}
}Registering a Webhook URL
Configure your webhook URL through the HeyGen dashboard or API:
Request Fields
| Field | Type | Req | Description |
|---|---|---|---|
url | string | ✓ | Your webhook endpoint URL |
events | array | ✓ | Event types to subscribe to |
secret | string | Shared secret for signature verification |
Via API
curl -X POST "https://api.heygen.com/v1/webhook/endpoint.add" \
-H "X-Api-Key: $HEYGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhook/heygen",
"events": ["avatar_video.success", "avatar_video.fail"]
}'TypeScript
interface WebhookConfig {
url: string; // Required
events: string[]; // Required
secret?: string;
}
async function registerWebhook(config: WebhookConfig): Promise<void> {
const response = await fetch("https://api.heygen.com/v1/webhook/endpoint.add", {
method: "POST",
headers: {
"X-Api-Key": process.env.HEYGEN_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify(config),
});
const json = await response.json();
if (json.error) {
throw new Error(json.error);
}
}Using Callback IDs
Track which video triggered a webhook with callback IDs:
Include Callback ID in Video Generation
const videoConfig = {
video_inputs: [...],
callback_id: "order_12345", // Your custom identifier
};Handle in Webhook
async function handleVideoSuccess(event: VideoSuccessEvent) {
const { video_id, video_url, callback_id } = event.event_data;
if (callback_id) {
// Look up your original request
const order = await getOrderByCallbackId(callback_id);
await updateOrderWithVideo(order.id, video_url);
}
}Webhook Security
Verify Webhook Signatures
If HeyGen provides signature verification:
import crypto from "crypto";
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler
app.post("/webhook/heygen", (req, res) => {
const signature = req.headers["x-heygen-signature"] as string;
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Process event...
});Validate Event Origin
function isValidHeygenEvent(event: any): boolean {
// Check required fields
if (!event.event_type || !event.event_data) {
return false;
}
// Check event type is known
const validEventTypes = [
"avatar_video.success",
"avatar_video.fail",
"video_translate.success",
"video_translate.fail",
];
return validEventTypes.includes(event.event_type);
}Handling Webhook Failures
Implement retry logic and error handling:
async function processWebhookEvent(event: HeyGenWebhookEvent) {
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await handleEvent(event);
return;
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt < maxRetries) {
// Exponential backoff
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1000));
}
}
}
// Store failed event for manual review
await storeFailedEvent(event);
}Webhook vs Polling Comparison
| Aspect | Webhook | Polling |
|---|---|---|
| Latency | Immediate | Depends on interval |
| Efficiency | High (push) | Low (repeated requests) |
| Complexity | Requires endpoint | Simpler to implement |
| Reliability | Needs retry handling | Guaranteed delivery |
| Cost | Lower API usage | Higher API usage |
Testing Webhooks
Local Development with ngrok
# Start ngrok tunnel
ngrok http 3000
# Use ngrok URL as webhook endpoint
# https://abc123.ngrok.io/webhook/heygenWebhook Testing Tool
// Test webhook locally
async function simulateWebhook(event: HeyGenWebhookEvent) {
const response = await fetch("http://localhost:3000/webhook/heygen", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
});
console.log(`Response: ${response.status}`);
}
// Simulate success event
await simulateWebhook({
event_type: "avatar_video.success",
event_data: {
video_id: "test_123",
video_url: "https://example.com/test.mp4",
thumbnail_url: "https://example.com/test.jpg",
duration: 30,
callback_id: "test_callback",
},
});Best Practices
1. Respond quickly - Return 200 within 5 seconds, process async 2. Handle duplicates - Same event may be sent multiple times 3. Implement retries - Handle temporary processing failures 4. Log everything - Store webhook payloads for debugging 5. Use callback IDs - Track requests through the system 6. Secure endpoints - Verify signatures, use HTTPS 7. Monitor health - Track webhook success rates 8. Queue processing - Use job queues for heavy processing