Skill: michaelwang11394/create-video (version 2.23.0)

--- name: create-video description: | Create videos from a text prompt using HeyGen's Video Agent (POST /v3/video-agents). The default for most video requests — AI handles script, avatar, visuals, voiceover, and captions automatically. 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) User says "make me a video" or "create a video about X". For precise control over specific avatars, exact scripts, or per-scene configuration, use the avatar-video skill instead. homepage: https://docs.heygen.com/reference/create-video-with-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/v3/video-agents" \ -H "X-[REDACTED_SECRET]" \ -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 | [REDACTED_SECRET] | `POST /v3/video-agents` | | Check video status / get URL | `mcp__heygen__get_video` | `GET /v3/videos/{video_id}` | | List account videos | `mcp__heygen__list_videos` | `GET /v3/videos` | | Delete a video | `mcp__heygen__delete_video` | `DELETE /v3/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 [REDACTED_SECRET] 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 /v3/video-agents` — see [video-agent.md](references/video-agent.md) 3. `GET /v3/videos/<id>` — see [video-status.md](references/video-status.md) ## Reference Files Read these as needed — they contain endpoint details, request/response schemas, and code examples (curl, TypeScript, Python). **Core workflow:** - [references/prompt-optimizer.md](references/prompt-optimizer.md) — Writing effective prompts (scenes, timing, visual styles) - [references/visual-styles.md](references/visual-styles.md) — 20 named visual styles with full specs - [references/prompt-examples.md](references/prompt-examples.md) — Production prompt examples and templates - [references/video-agent.md](references/video-agent.md) — `POST /v3/video-agents` request fields, response format, file inputs **Foundation:** - [references/video-status.md](references/video-status.md) — `GET /v3/videos/{id}` polling and download - [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** — quality depends entirely on prompt structure. Always use the prompt optimizer 2. **Lock avatar if needed** — pass `avatar_id` for consistency across videos 3. **Specify voice** — pass `voice_id` for a specific narrator voice 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

path: references/assets.md
content: ---
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-[REDACTED_SECRET]" \
  -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 with Video Agent
  console.log("Generating video...");
  const response = await fetch("https://api.heygen.com/v3/video-agents", {
    method: "POST",
    headers: {
      "X-Api-Key": process.env.HEYGEN_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      prompt: "Create a product demo using the uploaded assets",
      files: [
        { type: "asset_id", asset_id: logoAssetId },
        { type: "asset_id", asset_id: bgAssetId },
      ],
    }),
  });

  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


path: references/dimensions.md
content: ---
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/v3/video-agents" \
  -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Create a product demo video.",
    "orientation": "landscape"
  }'
```

## 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",
});
```


path: references/prompt-examples.md
content: ---
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.
```


path: references/prompt-optimizer.md
content: ---
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


path: references/quota.md
content: ---
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-[REDACTED_SECRET]"
```

### 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;
}
```


path: references/video-agent.md
content: ---
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__create_video_agent` instead of direct API calls:

```
Tool: mcp__heygen__create_video_agent
Parameters:
  prompt: "<optimized prompt from prompt-optimizer.md>"
  avatar_id: "avatar_id"       # optional, agent selects if omitted
  voice_id: "voice_id"         # optional, agent selects if omitted
  orientation: "landscape"     # optional, "landscape" or "portrait"
  files:                       # optional, type-discriminated union
    - type: "asset_id"
      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 POST /v3/videos |
| Automated content generation at scale | Video Agent |
| Specific avatar with exact script | Standard POST /v3/videos |
| Prototype or draft video | Video Agent |
| Brand-consistent production video | Standard POST /v3/videos |

## 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/v3/video-agents
```

## Request Fields

All fields are top-level (no nested `config` object).

| Field | Type | Req | Description |
|-------|------|:---:|-------------|
| `prompt` | string | ✓ | Text prompt describing the video you want (1-10000 chars) |
| `avatar_id` | string \| null | | Specific avatar to use (agent selects if omitted) |
| `voice_id` | string \| null | | Specific voice for narration (agent selects if omitted) |
| `orientation` | string \| null | | `"landscape"` or `"portrait"` |
| `files` | array \| null | | Asset files to reference in generation (max 20, see below) |
| `callback_url` | string \| null | | Webhook URL for completion notification |
| `callback_id` | string \| null | | Custom ID for tracking. **Requires `callback_url` to also be set** — omit both if you don't need webhooks |

### Files Array

Each file entry is a type-discriminated union. You must include the `type` field to indicate the format.

| Variant | Fields | Description |
|---------|--------|-------------|
| `AssetUrl` | `{ type: "url", url: string }` | Public URL to a file |
| `AssetId` | `{ type: "asset_id", asset_id: string }` | Asset ID of a previously uploaded file |
| `AssetBase64` | `{ type: "base64", media_type: string, data: string }` | Inline base64-encoded file |

## Response Format

```json
{
  "data": {
    "session_id": "sess_012abc345def678",
    "status": "generating",
    "video_id": "v_abc123def456",
    "created_at": 1711929600
  }
}
```

**Error format:**

```json
{
  "error": {
    "code": "invalid_request",
    "message": "prompt is required"
  }
}
```

## curl Example

```bash
curl -X POST "https://api.heygen.com/v3/video-agents" \
  -H "X-[REDACTED_SECRET]" \
  -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
type AssetUrl = { type: "url"; url: string };
type AssetId = { type: "asset_id"; asset_id: string };
type AssetBase64 = { type: "base64"; media_type: string; data: string };
type VideoAgentFile = AssetUrl | AssetId | AssetBase64;

interface VideoAgentRequest {
  prompt: string;                              // Required, 1-10000 chars
  avatar_id?: string | null;                   // Optional: specific avatar
  voice_id?: string | null;                    // Optional: specific voice
  orientation?: "portrait" | "landscape" | null;
  files?: VideoAgentFile[] | null;             // Max 20 files
  callback_id?: string | null;                 // Requires callback_url if set
  callback_url?: string | null;
}

interface VideoAgentResponse {
  data: {
    session_id: string;
    status: string;
    video_id: string;
    created_at: number;
  };
}

interface VideoAgentError {
  error: {
    code: string;
    message: string;
  };
}

async function generateWithVideoAgent(
  prompt: string,
  options?: {
    avatar_id?: string;
    voice_id?: string;
    orientation?: "portrait" | "landscape";
    files?: VideoAgentFile[];
  }
): Promise<{ session_id: string; video_id: string }> {
  const request: VideoAgentRequest = { prompt, ...options };

  const response = await fetch(
    "https://api.heygen.com/v3/video-agents",
    {
      method: "POST",
      headers: {
        "X-Api-Key": process.env.HEYGEN_API_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(request),
    }
  );

  const json = await response.json();

  if ("error" in json) {
    const err = json as VideoAgentError;
    throw new Error(`Video Agent failed: ${err.error.code} — ${err.error.message}`);
  }

  const result = json as VideoAgentResponse;
  return {
    session_id: result.data.session_id,
    video_id: result.data.video_id,
  };
}
```

## Python

```python
import requests
import os
from typing import Optional

def generate_with_video_agent(
    prompt: str,
    avatar_id: Optional[str] = None,
    voice_id: Optional[str] = None,
    orientation: Optional[str] = None,
    files: Optional[list[dict]] = None,
) -> dict:
    """Returns dict with session_id, status, video_id, created_at."""
    request_body: dict = {"prompt": prompt}

    if avatar_id:
        request_body["avatar_id"] = avatar_id
    if voice_id:
        request_body["voice_id"] = voice_id
    if orientation:
        request_body["orientation"] = orientation
    if files:
        request_body["files"] = files

    response = requests.post(
        "https://api.heygen.com/v3/video-agents",
        headers={
            "X-Api-Key": os.environ["HEYGEN_API_KEY"],
            "Content-Type": "application/json",
        },
        json=request_body,
    )

    data = response.json()
    if "error" in data:
        err = data["error"]
        raise Exception(f"Video Agent failed: {err['code']} — {err['message']}")

    return data["data"]
```

## Examples

### Basic: Prompt Only

```typescript
const { video_id } = await generateWithVideoAgent(
  "Create a 30-second welcome video for new employees at a tech startup. Keep it energetic and modern."
);
```

```python
result = generate_with_video_agent(
    "Create a 30-second welcome video for new employees at a tech startup. Keep it energetic and modern."
)
video_id = result["video_id"]
```

```bash
curl -X POST "https://api.heygen.com/v3/video-agents" \
  -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Create a 30-second welcome video for new employees at a tech startup. Keep it energetic and modern."
  }'
```

### With Orientation

```typescript
const { video_id } = await generateWithVideoAgent(
  "Explain the benefits of cloud computing for small businesses. Use simple language and real-world examples.",
  { orientation: "landscape" }
);
```

```python
result = generate_with_video_agent(
    "Explain the benefits of cloud computing for small businesses. Use simple language and real-world examples.",
    orientation="landscape",
)
```

```bash
curl -X POST "https://api.heygen.com/v3/video-agents" \
  -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Explain the benefits of cloud computing for small businesses. Use simple language and real-world examples.",
    "orientation": "landscape"
  }'
```

### With Specific Avatar and Voice

```typescript
const { video_id } = await generateWithVideoAgent(
  "Present quarterly sales results. Professional tone, data-focused.",
  {
    avatar_id: "josh_lite3_20230714",
    voice_id: "1bd001e7e50f421d891986aad5158bc8",
    orientation: "landscape",
  }
);
```

```python
result = generate_with_video_agent(
    "Present quarterly sales results. Professional tone, data-focused.",
    avatar_id="josh_lite3_20230714",
    voice_id="1bd001e7e50f421d891986aad5158bc8",
    orientation="landscape",
)
```

```bash
curl -X POST "https://api.heygen.com/v3/video-agents" \
  -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Present quarterly sales results. Professional tone, data-focused.",
    "avatar_id": "josh_lite3_20230714",
    "voice_id": "1bd001e7e50f421d891986aad5158bc8",
    "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 { video_id } = await generateWithVideoAgent(
  "Create a product demo video showcasing our new dashboard feature. Use the uploaded screenshots as visual references.",
  {
    orientation: "landscape",
    files: [
      { type: "asset_id", asset_id: logoAssetId },
      { type: "asset_id", asset_id: productImageId },
    ],
  }
);
```

```python
# 1. Upload reference materials (see assets.md)
logo_asset_id = upload_file("./company-logo.png", "image/png")
product_image_id = upload_file("./product-screenshot.png", "image/png")

# 2. Generate video with references
result = generate_with_video_agent(
    "Create a product demo video showcasing our new dashboard feature. Use the uploaded screenshots as visual references.",
    orientation="landscape",
    files=[
        {"type": "asset_id", "asset_id": logo_asset_id},
        {"type": "asset_id", "asset_id": product_image_id},
    ],
)
```

```bash
curl -X POST "https://api.heygen.com/v3/video-agents" \
  -H "X-[REDACTED_SECRET]" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Create a product demo video showcasing our new dashboard feature. Use the uploaded screenshots as visual references.",
    "orientation": "landscape",
    "files": [
      { "type": "asset_id", "asset_id": "uploaded_logo_id" },
      { "type": "url", "url": "https://example.com/product-screenshot.png" }
    ]
  }'
```

## 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:

```
GET https://api.heygen.com/v3/videos/{video_id}
```

```typescript
// Same polling as standard video generation
const videoUrl = await waitForVideo(videoId);
```

```python
import time

def wait_for_video(video_id: str, timeout: int = 600) -> str:
    """Poll until video is ready. Returns the video URL."""
    start = time.time()
    while time.time() - start < timeout:
        resp = requests.get(
            f"https://api.heygen.com/v3/videos/{video_id}",
            headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]},
        )
        data = resp.json()["data"]
        if data["status"] == "completed":
            return data["video_url"]
        if data["status"] == "failed":
            raise Exception(f"Video failed: {data.get('error')}")
        time.sleep(5)
    raise TimeoutError(f"Video {video_id} did not complete within {timeout}s")
```

```bash
curl "https://api.heygen.com/v3/videos/$VIDEO_ID" \
  -H "X-[REDACTED_SECRET]"
```

See [video-status.md](video-status.md) for full polling implementation.

## Comparison: Video Agent vs Standard API

### Video Agent Request
```typescript
// Simple: describe what you want
const { video_id } = 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 via POST /v3/videos
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

- Avatar selection may vary if not specified
- Scene composition is automated
- May not match precise brand guidelines
- Duration is approximate (determined by the agent from the prompt, not directly configurable)

## Best Practices

1. **Be specific in prompts** — More detail = better results
2. **Specify voice if needed** — Use `voice_id` to lock a specific voice for consistency
3. **Lock avatar if needed** — Use `avatar_id` for consistency across videos
4. **Upload reference files** — Help agent understand your brand/product (use `type: "asset_id"` for uploaded assets, `type: "url"` for public URLs)
5. **Iterate on prompts** — Refine based on results
6. **Use for drafts** — Video Agent is great for quick iterations before final production


path: references/video-status.md
content: ---
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, video_page_url, and other metadata in a single call.

## Checking Video Status (Direct API)

### curl

```bash
curl -X GET "https://api.heygen.com/v3/videos/YOUR_VIDEO_ID" \
  -H "X-[REDACTED_SECRET]"
```

### TypeScript

```typescript
interface VideoStatusResponse {
  data: {
    id: string;
    status: "pending" | "processing" | "completed" | "failed";
    video_url?: string;
    thumbnail_url?: string;
    duration?: number;
    title?: string;
    created_at?: number;
    completed_at?: number;
    gif_url?: string;
    captioned_video_url?: string;
    subtitle_url?: string;
    folder_id?: string;
    output_language?: string;
    video_page_url?: string;
    failure_code?: string;
    failure_message?: string;
  };
  error?: {
    code: string;
    message: string;
  };
}

async function getVideoStatus(videoId: string): Promise<VideoStatusResponse["data"]> {
  const response = await fetch(
    `https://api.heygen.com/v3/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.code}: ${json.error.message}`);
  }

  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/v3/videos/{video_id}",
        headers={"X-Api-Key": os.environ["HEYGEN_API_KEY"]}
    )

    data = response.json()
    if data.get("error"):
        err = data["error"]
        raise Exception(f"{err['code']}: {err['message']}")

    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
{
  "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": 1705311000,
    "completed_at": 1705311480,
    "gif_url": "https://files.heygen.ai/gif/abc123.gif",
    "captioned_video_url": null,
    "subtitle_url": null,
    "folder_id": null,
    "output_language": "en",
    "video_page_url": "https://app.heygen.com/videos/abc123"
  }
}
```

### Failed Video

```json
{
  "data": {
    "id": "abc123",
    "status": "failed",
    "failure_code": "script_too_long",
    "failure_message": "Script too long for selected avatar"
  }
}
```

### Error Response

```json
{
  "error": {
    "code": "not_found",
    "message": "Video not found"
  }
}
```

## 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/v3/videos",
    {
      method: "POST",
      headers: {
        "X-Api-Key": process.env.HEYGEN_API_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(config),
    }
  );

  const generateJson = await generateResponse.json();
  if (generateJson.error) {
    throw new Error(`${generateJson.error.code}: ${generateJson.error.message}`);
  }
  const videoId = generateJson.data.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`);
      console.log(`Video page: ${status.video_page_url}`);
      // 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,
        videoPageUrl: status.video_page_url,
      }, 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


path: references/visual-styles.md
content: ---
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.
```


path: references/webhooks.md
content: ---
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-[REDACTED_SECRET]" \
  -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,
  [REDACTED_SECRET]
): 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


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)