references/assets.md
---
name: assets
description: Uploading images, videos, and audio for use in HeyGen video generation
---
# 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 | Key for creating uploaded photo avatars (images only) |
| `data.folder_id` | string | Folder ID (empty if not in a folder) |
| `data.meta` | string \| null | Asset metadata |
| `data.created_ts` | number | Unix timestamp of creation |
### curl
```bash
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
```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)
```typescript
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
```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:
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
references/dimensions.md
---
name: dimensions
description: Resolution options (720p/1080p) and aspect ratios for HeyGen videos
---
# 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
```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
```bash
# 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
```typescript
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
```typescript
const youtubeConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape
};
```
### TikTok / Instagram Reels / YouTube Shorts
```typescript
const shortFormConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1920 }, // 9:16 portrait
};
```
### Instagram Feed Post
```typescript
const instagramFeedConfig = {
video_inputs: [...],
dimension: { width: 1080, height: 1080 }, // 1:1 square
};
```
### LinkedIn
```typescript
const linkedinConfig = {
video_inputs: [...],
dimension: { width: 1920, height: 1080 }, // 16:9 landscape preferred
};
```
### Twitter/X
```typescript
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:
```typescript
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:
```typescript
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
```typescript
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:
```typescript
// 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
```typescript
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",
});
```
references/prompt-examples.md
---
name: prompt-examples
description: Full production prompt examples and ready-to-use templates for Video Agent
---
# 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-backed
```
### Output 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.
```
references/prompt-optimizer.md
---
name: prompt-optimizer
description: Write production-quality prompts for HeyGen Video Agent — from basic ideas to fully art-directed scene-by-scene scripts
---
# 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](visual-styles.md) | Choosing a visual style (20 styles with full specs) |
| [prompt-examples.md](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](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 voiceover
```
### FORMAT
```
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 handle
```
### MUSIC & 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](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
references/quota.md
---
name: quota
description: Credit system, usage limits, and checking remaining quota for HeyGen
---
# 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
```bash
curl -X GET "https://api.heygen.com/v2/user/remaining_quota" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```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
```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
```json
{
"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:
```typescript
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
```typescript
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
```typescript
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:
```typescript
const videoConfig = {
test: true, // Use test mode during development
video_inputs: [...],
};
// Test videos may have watermarks but don't consume credits
```
## Subscription 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
```typescript
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;
}
```
references/video-agent.md
---
name: video-agent
description: One-shot prompt video generation with HeyGen Video Agent API
---
# 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](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](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](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/generate
```
## Request 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
```json
{
"error": null,
"data": {
"video_id": "abc123"
}
}
```
## curl Example
```bash
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
```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
```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
```typescript
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
```typescript
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
```typescript
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:
```typescript
// 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:
```typescript
// Same polling as standard video generation
const videoUrl = await waitForVideo(videoId);
```
See [video-status.md](video-status.md) for polling implementation.
## Comparison: Video Agent vs Standard API
### Video Agent Request
```typescript
// 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
```typescript
// 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
references/video-status.md
---
name: video-status
description: Polling patterns, status types, and retrieving download URLs for HeyGen videos
---
# 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
```bash
curl -X GET "https://api.heygen.com/v2/videos/YOUR_VIDEO_ID" \
-H "X-Api-Key: $HEYGEN_API_KEY"
```
### TypeScript
```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
```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
```json
{
"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
```json
{
"error": null,
"data": {
"id": "abc123",
"status": "failed",
"failure_code": "script_too_long",
"failure_message": "Script too long for selected avatar"
}
}
```
## Polling Implementation
### Basic Polling
```typescript
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
```typescript
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
```python
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)
```typescript
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)
```python
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:
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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](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
references/visual-styles.md
---
name: visual-styles
description: 20 named visual styles for Video Agent prompts — each with colors, typography, motion, and transitions
---
# 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.
```
references/webhooks.md
---
name: webhooks
description: Registering webhook endpoints and event types for HeyGen
---
# 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
```typescript
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
```python
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
```typescript
interface VideoSuccessEvent {
event_type: "avatar_video.success";
event_data: {
video_id: string;
video_url: string;
thumbnail_url: string;
duration: number;
callback_id?: string;
};
}
```
```json
{
"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
```typescript
interface VideoFailureEvent {
event_type: "avatar_video.fail";
event_data: {
video_id: string;
error: string;
callback_id?: string;
};
}
```
```json
{
"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
```bash
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
```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
```typescript
const videoConfig = {
video_inputs: [...],
callback_id: "order_12345", // Your custom identifier
};
```
### Handle in Webhook
```typescript
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:
```typescript
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
```typescript
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:
```typescript
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
```bash
# Start ngrok tunnel
ngrok http 3000
# Use ngrok URL as webhook endpoint
# https://abc123.ngrok.io/webhook/heygen
```
### Webhook Testing Tool
```typescript
// 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
SKILL.md
---
name: create-video
description: |
Create videos from a text prompt using HeyGen's Video Agent. Use when: (1) Creating a video from a description or idea, (2) Generating explainer, demo, or marketing videos from a prompt, (3) Making a video without specifying exact avatars, voices, or scenes, (4) Quick video prototyping or drafts, (5) One-shot prompt-to-video generation, (6) User says "make me a video" or "create a video about X".
homepage: https://docs.heygen.com/reference/generate-video-agent
allowed-tools: mcp__heygen__*
metadata:
openclaw:
requires:
env:
- HEYGEN_API_KEY
primaryEnv: HEYGEN_API_KEY
---
# 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.
```bash
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](references/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](references/prompt-optimizer.md) → [visual-styles.md](references/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](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md)
2. `POST /v1/video_agent/generate` — see [video-agent.md](references/video-agent.md)
3. `GET /v2/videos/<id>` — see [video-status.md](references/video-status.md)
## Quick Reference
| Task | MCP Tool | Read |
|------|----------|------|
| Generate video from prompt | `mcp__heygen__generate_video_agent` | [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md) → [video-agent.md](references/video-agent.md) |
| Check video status / get download URL | `mcp__heygen__get_video` | [video-status.md](references/video-status.md) |
| Upload reference files for prompt | — | [assets.md](references/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](references/prompt-optimizer.md) - Writing effective prompts (core workflow + rules)
- [references/visual-styles.md](references/visual-styles.md) - 20 named visual styles with full specs
- [references/prompt-examples.md](references/prompt-examples.md) - Full production prompt example + ready-to-use templates
- [references/video-agent.md](references/video-agent.md) - Video Agent API endpoint details
### Foundation
- [references/video-status.md](references/video-status.md) - Polling patterns and download URLs
- [references/webhooks.md](references/webhooks.md) - Webhook endpoints and events
- [references/assets.md](references/assets.md) - Uploading images, videos, audio as references
- [references/dimensions.md](references/dimensions.md) - Resolution and aspect ratios
- [references/quota.md](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