{"id":"eb252011-be2a-4b5e-872f-25a1ac1c4a6d","entityType":"agent","slug":"clawhub-michaelwang11394-video-agent","name":"Video Agent","canonicalUrl":"https://www.xpersona.co/agent/clawhub-michaelwang11394-video-agent","canonicalPath":"/agent/clawhub-michaelwang11394-video-agent","generatedAt":"2026-10-09T18:51:23.882Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":null},"description":"HeyGen AI video creation API. Use when: (1) Using Video Agent for one-shot prompt-to-video generation, (2) Generating AI avatar videos with /v2/video/generat...","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 4.1K downloads reported by the source. Last updated 4/15/2026.","installCommand":"clawhub skill install kn7dnc0jepdz3jy0rg589kcxns80dmr5:video-agent","sourceUrl":"https://clawhub.ai/michaelwang11394/video-agent","homepage":"https://clawhub.ai/michaelwang11394/video-agent","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/michaelwang11394/video-agent","kind":"source"}],"safetyScore":84,"overallRank":62,"popularityScore":66,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Video Agent technical dossier on Xpersona with source links, trust signals, and execution metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":"No protocol or capability metadata is available."},"protocols":[],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":0,"capabilityMatrix":{"rows":[],"flattenedTokens":""}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":null},"stars":null,"forks":null,"downloads":4058,"packageName":null,"latestVersion":"2.8.0","tractionLabel":"4.1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-02-28T17:35:06.334Z","emptyReason":null},"lastUpdatedAt":"2026-04-15T00:45:39.800Z","lastCrawledAt":"2026-02-28T17:35:06.334Z","lastIndexedAt":null,"nextCrawlAt":"2026-03-01T17:35:06.334Z","lastVerifiedAt":null,"highlights":[{"version":"2.8.0","createdAt":"2026-02-23T17:23:15.703Z","changelog":"Auto-publish from commit 1817bb7648735737457f1250bfb7513f04576b87","fileCount":24,"zipByteSize":89312},{"version":"2.6.0","createdAt":"2026-02-22T22:01:20.257Z","changelog":"Auto-publish from commit 0456978d9cee307682ac9e2ef78eddfbdf600192","fileCount":22,"zipByteSize":86738},{"version":"2.5.0","createdAt":"2026-02-18T19:54:49.956Z","changelog":"Auto-publish from commit a1f81720f25c4e8c0d9225e2c890b3a7e8d892fe","fileCount":null,"zipByteSize":null},{"version":"2.2.0","createdAt":"2026-02-17T20:17:14.362Z","changelog":"Auto-publish from commit 06389ef6b4c4d9f7108adc45334ff331f8fa9916","fileCount":null,"zipByteSize":null},{"version":"1.2.1","createdAt":"2026-02-09T18:16:40.318Z","changelog":"Reordered description to prioritize Video Agent one-shot prompt-to-video generation","fileCount":null,"zipByteSize":null},{"version":"1.2.0","createdAt":"2026-02-09T18:15:03.186Z","changelog":"Major update: Added comprehensive HeyGen API documentation including avatars, voices, streaming, translation, Remotion integration, and more.","fileCount":null,"zipByteSize":null},{"version":"1.1.0","createdAt":"2026-02-09T18:14:33.101Z","changelog":"Major update: Added comprehensive HeyGen API documentation including avatars, voices, streaming, translation, Remotion integration, and more.","fileCount":null,"zipByteSize":null},{"version":"1.0.1","createdAt":"2026-02-02T18:06:25.082Z","changelog":"Added prompt-optimizer reference guide","fileCount":null,"zipByteSize":null}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install kn7dnc0jepdz3jy0rg589kcxns80dmr5:video-agent","setupComplexity":"low","setupSteps":["Install using `clawhub skill install kn7dnc0jepdz3jy0rg589kcxns80dmr5:video-agent` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/michaelwang11394/video-agent before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":[]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-09T18:51:23.880Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-michaelwang11394-video-agent/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":null},"readme":"Skill: Video Agent\n\nOwner: michaelwang11394\n\nSummary: HeyGen AI video creation API. Use when: (1) Using Video Agent for one-shot prompt-to-video generation, (2) Generating AI avatar videos with /v2/video/generat...\n\nTags: ai-avatar:2.8.0, ai-video:2.8.0, avatar:2.8.0, digital-human:2.8.0, heygen:2.8.0, latest:2.8.0, talking-head:2.8.0, text-to-video:2.8.0, video:2.8.0, video-generation:2.8.0\n\nVersion history:\n\nv2.8.0 | 2026-02-23T17:23:15.703Z | user\n\nAuto-publish from commit 1817bb7648735737457f1250bfb7513f04576b87\n\nv2.6.0 | 2026-02-22T22:01:20.257Z | user\n\nAuto-publish from commit 0456978d9cee307682ac9e2ef78eddfbdf600192\n\nv2.5.0 | 2026-02-18T19:54:49.956Z | user\n\nAuto-publish from commit a1f81720f25c4e8c0d9225e2c890b3a7e8d892fe\n\nv2.2.0 | 2026-02-17T20:17:14.362Z | user\n\nAuto-publish from commit 06389ef6b4c4d9f7108adc45334ff331f8fa9916\n\nv1.2.1 | 2026-02-09T18:16:40.318Z | user\n\nReordered description to prioritize Video Agent one-shot prompt-to-video generation\n\nv1.2.0 | 2026-02-09T18:15:03.186Z | user\n\nMajor update: Added comprehensive HeyGen API documentation including avatars, voices, streaming, translation, Remotion integration, and more.\n\nv1.1.0 | 2026-02-09T18:14:33.101Z | user\n\nMajor update: Added comprehensive HeyGen API documentation including avatars, voices, streaming, translation, Remotion integration, and more.\n\nv1.0.1 | 2026-02-02T18:06:25.082Z | user\n\nAdded prompt-optimizer reference guide\n\nv1.0.0 | 2026-02-02T17:46:22.427Z | user\n\nInitial release of video-agent skill.\n\n- Generate AI  videos from a single prompt using HeyGen's Video Agent API.\n- Command-line tools to submit prompts, poll for video completion, and download results.\n- Simple API key setup via environment variable.\n- Includes usage examples and detailed options for video generation and status checking.\n- Automatically selects avatars and voices based on your prompt.\n\nArchive index:\n\nArchive v2.8.0: 24 files, 89312 bytes\n\nFiles: references/assets.md (8484b), references/authentication.md (4770b), references/avatars.md (16134b), references/backgrounds.md (6696b), references/captions.md (5631b), references/dimensions.md (6975b), references/photo-avatars.md (24366b), references/prompt-examples.md (8064b), references/prompt-optimizer.md (12384b), references/quota.md (4765b), references/remotion-integration.md (18550b), references/scripts.md (10143b), references/templates.md (9990b), references/text-overlays.md (6964b), references/text-to-speech.md (8693b), references/video-agent.md (9037b), references/video-generation.md (22105b), references/video-status.md (12892b), references/video-translation.md (11118b), references/visual-styles.md (14963b), references/voices.md (11892b), references/webhooks.md (9302b), SKILL.md (4603b), _meta.json (130b)\n\nFile v2.8.0:SKILL.md\n\n---\nname: heygen\ndescription: |\n  HeyGen AI video creation API. Use when: (1) Using Video Agent for one-shot prompt-to-video generation, (2) Generating AI avatar videos with /v2/video/generate, (3) Working with HeyGen avatars, voices, backgrounds, or captions, (4) Creating transparent WebM videos for compositing, (5) Polling video status or handling webhooks, (6) Integrating HeyGen with Remotion for programmatic video, (7) Translating or dubbing existing videos, (8) Generating standalone TTS audio with the Starfish model via /v1/audio.\nhomepage: https://docs.heygen.com/reference/generate-video-agent\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - HEYGEN_API_KEY\n    primaryEnv: HEYGEN_API_KEY\n---\n\n# HeyGen API\n\nAI avatar video creation API for generating talking-head videos, explainers, and presentations.\n\n## Default Workflow\n\n**Prefer Video Agent API** (`POST /v1/video_agent/generate`) for most video requests.\nAlways use [prompt-optimizer.md](references/prompt-optimizer.md) guidelines to structure prompts with scenes, timing, and visual styles.\n\nOnly use v2/video/generate when user explicitly needs:\n- Exact script without AI modification\n- Specific voice_id selection\n- Different avatars/backgrounds per scene\n- Precise per-scene timing control\n- Programmatic/batch generation with exact specs\n\n## Quick Reference\n\n| Task | Read |\n|------|------|\n| Generate video from prompt (easy) | [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md) → [video-agent.md](references/video-agent.md) |\n| Generate video with precise control | [video-generation.md](references/video-generation.md), [avatars.md](references/avatars.md), [voices.md](references/voices.md) |\n| Check video status / get download URL | [video-status.md](references/video-status.md) |\n| Add captions or text overlays | [captions.md](references/captions.md), [text-overlays.md](references/text-overlays.md) |\n| Transparent video for compositing | [video-generation.md](references/video-generation.md) (WebM section) |\n| Generate standalone TTS audio | [text-to-speech.md](references/text-to-speech.md) |\n| Translate/dub existing video | [video-translation.md](references/video-translation.md) |\n| Use with Remotion | [remotion-integration.md](references/remotion-integration.md) |\n\n## Reference Files\n\n### Foundation\n- [references/authentication.md](references/authentication.md) - API key setup and X-Api-Key header\n- [references/quota.md](references/quota.md) - Credit system and usage limits\n- [references/video-status.md](references/video-status.md) - Polling patterns and download URLs\n- [references/assets.md](references/assets.md) - Uploading images, videos, audio\n\n### Core Video Creation\n- [references/avatars.md](references/avatars.md) - Listing avatars, styles, avatar_id selection\n- [references/voices.md](references/voices.md) - Listing voices, locales, speed/pitch\n- [references/scripts.md](references/scripts.md) - Writing scripts, pauses, pacing\n- [references/video-generation.md](references/video-generation.md) - POST /v2/video/generate and multi-scene videos\n- [references/video-agent.md](references/video-agent.md) - One-shot prompt video generation\n- [references/prompt-optimizer.md](references/prompt-optimizer.md) - Writing effective Video Agent prompts (core workflow + rules)\n- [references/visual-styles.md](references/visual-styles.md) - 20 named visual styles with full specs\n- [references/prompt-examples.md](references/prompt-examples.md) - Full production prompt example + ready-to-use templates\n- [references/dimensions.md](references/dimensions.md) - Resolution and aspect ratios\n\n### Video Customization\n- [references/backgrounds.md](references/backgrounds.md) - Solid colors, images, video backgrounds\n- [references/text-overlays.md](references/text-overlays.md) - Adding text with fonts and positioning\n- [references/captions.md](references/captions.md) - Auto-generated captions and subtitles\n\n### Advanced Features\n- [references/templates.md](references/templates.md) - Template listing and variable replacement\n- [references/video-translation.md](references/video-translation.md) - Translating videos and dubbing\n- [references/text-to-speech.md](references/text-to-speech.md) - Standalone TTS audio with Starfish model\n- [references/photo-avatars.md](references/photo-avatars.md) - Creating avatars from photos\n- [references/webhooks.md](references/webhooks.md) - Webhook endpoints and events\n\n### Integration\n- [references/remotion-integration.md](references/remotion-integration.md) - Using HeyGen in Remotion compositions\n\nFile v2.8.0:_meta.json\n\n{\n  \"ownerId\": \"kn7dnc0jepdz3jy0rg589kcxns80dmr5\",\n  \"slug\": \"video-agent\",\n  \"version\": \"2.8.0\",\n  \"publishedAt\": 1771867395703\n}\n\nFile v2.8.0:references/assets.md\n\n---\nname: assets\ndescription: Uploading images, videos, and audio for use in HeyGen video generation\n---\n\n# Asset Upload and Management\n\nHeyGen allows you to upload custom assets (images, videos, audio) for use in video generation, such as backgrounds, talking photo sources, and custom audio.\n\n## Upload Flow\n\nAsset 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.\n\n## Uploading an Asset\n\n**Endpoint:** `POST https://upload.heygen.com/v1/asset`\n\n### Request\n\n| Header | Required | Description |\n|--------|:--------:|-------------|\n| `X-Api-Key` | ✓ | Your HeyGen API key |\n| `Content-Type` | ✓ | MIME type of the file (e.g. `image/jpeg`) |\n\nThe request body is the raw binary file data. No JSON or form fields are needed.\n\n### Response\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `code` | number | Status code (`100` = success) |\n| `data.id` | string | Unique asset ID for use in video generation |\n| `data.name` | string | Asset name |\n| `data.file_type` | string | `image`, `video`, or `audio` |\n| `data.url` | string | Accessible URL for the uploaded file |\n| `data.image_key` | string \\| null | Key for creating uploaded photo avatars (images only) |\n| `data.folder_id` | string | Folder ID (empty if not in a folder) |\n| `data.meta` | string \\| null | Asset metadata |\n| `data.created_ts` | number | Unix timestamp of creation |\n\n### curl\n\n```bash\ncurl -X POST \"https://upload.heygen.com/v1/asset\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: image/jpeg\" \\\n  --data-binary '@./background.jpg'\n```\n\n### TypeScript\n\n```typescript\nimport fs from \"fs\";\n\ninterface AssetUploadResponse {\n  code: number;\n  data: {\n    id: string;\n    name: string;\n    file_type: string;\n    url: string;\n    image_key: string | null;\n    folder_id: string;\n    meta: string | null;\n    created_ts: number;\n  };\n  msg: string | null;\n  message: string | null;\n}\n\nasync function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  const fileBuffer = fs.readFileSync(filePath);\n\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n    },\n    body: fileBuffer,\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}\n\n// Usage\nconst asset = await uploadAsset(\"./background.jpg\", \"image/jpeg\");\nconsole.log(`Uploaded asset: ${asset.id}`);\nconsole.log(`Asset URL: ${asset.url}`);\n```\n\n### TypeScript (with streams for large files)\n\n```typescript\nimport fs from \"fs\";\nimport { stat } from \"fs/promises\";\n\nasync function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  const fileStats = await stat(filePath);\n  const fileStream = fs.createReadStream(filePath);\n\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n      \"Content-Length\": fileStats.size.toString(),\n    },\n    body: fileStream as any,\n    // @ts-ignore - duplex is needed for streaming\n    duplex: \"half\",\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}\n```\n\n### Python\n\n```python\nimport requests\nimport os\n\ndef upload_asset(file_path: str, content_type: str) -> dict:\n    with open(file_path, \"rb\") as f:\n        response = requests.post(\n            \"https://upload.heygen.com/v1/asset\",\n            headers={\n                \"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"],\n                \"Content-Type\": content_type\n            },\n            data=f\n        )\n\n    data = response.json()\n    if data.get(\"code\") != 100:\n        raise Exception(data.get(\"message\", \"Upload failed\"))\n\n    return data[\"data\"]\n\n\n# Usage\nasset = upload_asset(\"./background.jpg\", \"image/jpeg\")\nprint(f\"Uploaded asset: {asset['id']}\")\nprint(f\"Asset URL: {asset['url']}\")\n```\n\n## Supported Content Types\n\n| Type | Content-Type | Use Case |\n|------|--------------|----------|\n| JPEG | `image/jpeg` | Backgrounds, talking photos |\n| PNG | `image/png` | Backgrounds, overlays |\n| MP4 | `video/mp4` | Video backgrounds |\n| WebM | `video/webm` | Video backgrounds |\n| MP3 | `audio/mpeg` | Custom audio input |\n| WAV | `audio/wav` | Custom audio input |\n\n## Uploading from URL\n\nIf your asset is already hosted online:\n\n```typescript\nasync function uploadFromUrl(sourceUrl: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  // 1. Download the file\n  const sourceResponse = await fetch(sourceUrl);\n  const buffer = Buffer.from(await sourceResponse.arrayBuffer());\n\n  // 2. Upload directly to HeyGen\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n    },\n    body: buffer,\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}\n```\n\n## Using Uploaded Assets\n\n### As Background Image\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello, this is a video with a custom background!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"image\",\n        url: asset.url,  // Use the URL from the upload response\n      },\n    },\n  ],\n};\n```\n\n### As Talking Photo Source\n\n```typescript\nconst talkingPhotoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"talking_photo\",\n        talking_photo_id: asset.id,  // Use the ID from the upload response\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello from my talking photo!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n  ],\n};\n```\n\n### As Audio Input\n\n```typescript\nconst audioConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"audio\",\n        audio_url: asset.url,  // Use the URL from the upload response\n      },\n    },\n  ],\n};\n```\n\n## Complete Upload Workflow\n\n```typescript\nasync function createVideoWithCustomBackground(\n  backgroundPath: string,\n  script: string\n): Promise<string> {\n  // 1. Upload background\n  console.log(\"Uploading background...\");\n  const background = await uploadAsset(backgroundPath, \"image/jpeg\");\n\n  // 2. Create video config\n  const config = {\n    video_inputs: [\n      {\n        character: {\n          type: \"avatar\",\n          avatar_id: \"josh_lite3_20230714\",\n          avatar_style: \"normal\",\n        },\n        voice: {\n          type: \"text\",\n          input_text: script,\n          voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n        },\n        background: {\n          type: \"image\",\n          url: background.url,\n        },\n      },\n    ],\n    dimension: { width: 1920, height: 1080 },\n  };\n\n  // 3. Generate video\n  console.log(\"Generating video...\");\n  const response = await fetch(\"https://api.heygen.com/v2/video/generate\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify(config),\n  });\n\n  const { data } = await response.json();\n  return data.video_id;\n}\n```\n\n## Asset Limitations\n\n- **File size**: 10MB maximum\n- **Image dimensions**: Recommended to match video dimensions\n- **Audio duration**: Should match expected video length\n- **Retention**: Assets may be deleted after a period of inactivity\n\n## Best Practices\n\n1. **Optimize images** - Resize to match video dimensions before uploading\n2. **Use appropriate formats** - JPEG for photos, PNG for graphics with transparency\n3. **Validate before upload** - Check file type and size locally first\n4. **Handle upload errors** - Implement retry logic for failed uploads\n5. **Cache asset IDs** - Reuse assets across multiple video generations\n\nFile v2.8.0:references/authentication.md\n\n---\nname: authentication\ndescription: API key setup, X-Api-Key header, and authentication patterns for HeyGen\n---\n\n# HeyGen Authentication\n\nAll HeyGen API requests require authentication using an API key passed in the `X-Api-Key` header.\n\n## Getting Your API Key\n\n1. Go to https://app.heygen.com/settings?from=&nav=API\n2. Log in if prompted\n3. Copy your API key\n\n## Environment Setup\n\nStore your API key securely as an environment variable:\n\n```bash\nexport HEYGEN_API_KEY=\"your-api-key-here\"\n```\n\nFor `.env` files:\n\n```\nHEYGEN_API_KEY=your-api-key-here\n```\n\n## Making Authenticated Requests\n\n### curl\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatars\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n### TypeScript/JavaScript (fetch)\n\n```typescript\nconst response = await fetch(\"https://api.heygen.com/v2/avatars\", {\n  headers: {\n    \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n  },\n});\nconst { data } = await response.json();\n```\n\n### TypeScript/JavaScript (axios)\n\n```typescript\nimport axios from \"axios\";\n\nconst client = axios.create({\n  baseURL: \"https://api.heygen.com\",\n  headers: {\n    \"X-Api-Key\": process.env.HEYGEN_API_KEY,\n  },\n});\n\nconst { data } = await client.get(\"/v2/avatars\");\n```\n\n### Python (requests)\n\n```python\nimport os\nimport requests\n\nresponse = requests.get(\n    \"https://api.heygen.com/v2/avatars\",\n    headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n)\ndata = response.json()\n```\n\n### Python (httpx)\n\n```python\nimport os\nimport httpx\n\nasync with httpx.AsyncClient() as client:\n    response = await client.get(\n        \"https://api.heygen.com/v2/avatars\",\n        headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n    )\n    data = response.json()\n```\n\n## Creating a Reusable API Client\n\n### TypeScript\n\n```typescript\nclass HeyGenClient {\n  private baseUrl = \"https://api.heygen.com\";\n  private apiKey: string;\n\n  constructor(apiKey: string) {\n    this.apiKey = apiKey;\n  }\n\n  async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n    const response = await fetch(`${this.baseUrl}${endpoint}`, {\n      ...options,\n      headers: {\n        \"X-Api-Key\": this.apiKey,\n        \"Content-Type\": \"application/json\",\n        ...options.headers,\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || `HTTP ${response.status}`);\n    }\n\n    return response.json();\n  }\n\n  get<T>(endpoint: string): Promise<T> {\n    return this.request<T>(endpoint);\n  }\n\n  post<T>(endpoint: string, body: unknown): Promise<T> {\n    return this.request<T>(endpoint, {\n      method: \"POST\",\n      body: JSON.stringify(body),\n    });\n  }\n}\n\n// Usage\nconst client = new HeyGenClient(process.env.HEYGEN_API_KEY!);\nconst avatars = await client.get(\"/v2/avatars\");\n```\n\n## API Response Format\n\nAll HeyGen API responses follow this structure:\n\n```typescript\ninterface ApiResponse<T> {\n  error: null | string;\n  data: T;\n}\n```\n\nSuccessful response example:\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"avatars\": [...]\n  }\n}\n```\n\nError response example:\n\n```json\n{\n  \"error\": \"Invalid API key\",\n  \"data\": null\n}\n```\n\n## Error Handling\n\nCommon authentication errors:\n\n| Status Code | Error | Cause |\n|-------------|-------|-------|\n| 401 | Invalid API key | API key is missing or incorrect |\n| 403 | Forbidden | API key doesn't have required permissions |\n| 429 | Rate limit exceeded | Too many requests |\n\n### Handling Errors\n\n```typescript\nasync function makeRequest(endpoint: string) {\n  const response = await fetch(`https://api.heygen.com${endpoint}`, {\n    headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n  });\n\n  const json = await response.json();\n\n  if (!response.ok || json.error) {\n    throw new Error(json.error || `HTTP ${response.status}`);\n  }\n\n  return json.data;\n}\n```\n\n## Rate Limiting\n\nHeyGen enforces rate limits on API requests:\n- Standard rate limits apply per API key\n- Some endpoints (like video generation) have stricter limits\n- Use exponential backoff when receiving 429 errors\n\n```typescript\nasync function requestWithRetry(\n  fn: () => Promise<Response>,\n  maxRetries = 3\n): Promise<Response> {\n  for (let i = 0; i < maxRetries; i++) {\n    const response = await fn();\n\n    if (response.status === 429) {\n      const waitTime = Math.pow(2, i) * 1000;\n      await new Promise((resolve) => setTimeout(resolve, waitTime));\n      continue;\n    }\n\n    return response;\n  }\n\n  throw new Error(\"Max retries exceeded\");\n}\n```\n\n## Security Best Practices\n\n1. **Never expose API keys in client-side code** - Always make API calls from a backend server\n2. **Use environment variables** - Don't hardcode API keys in source code\n3. **Rotate keys periodically** - Generate new API keys regularly\n4. **Monitor usage** - Check your HeyGen dashboard for unusual activity\n\nFile v2.8.0:references/avatars.md\n\n---\nname: avatars\ndescription: Listing avatars, avatar styles, and avatar_id selection for HeyGen\n---\n\n# HeyGen Avatars\n\nAvatars are the AI-generated presenters in HeyGen videos. You can use public avatars provided by HeyGen or create custom avatars.\n\n## Previewing Avatars Before Generation\n\nAlways preview avatars before generating a video to ensure they match user preferences. Each avatar has preview URLs that can be opened directly in the browser - no downloading required.\n\n### Quick Preview: Open URL in Browser (Recommended)\n\nThe fastest way to preview avatars is to open the URL directly in the default browser. **Do not download the image first** - just pass the URL to `open`:\n\n```bash\n# macOS: Open URL directly in default browser (no download)\nopen \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n\n# Open preview video to see animation\nopen \"https://files.heygen.ai/avatar/preview/josh.mp4\"\n\n# Linux: Use xdg-open\nxdg-open \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n\n# Windows: Use start\nstart \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n```\n\nThe `open` command on macOS opens URLs directly in the default browser - it does not download the file. This is the quickest way to let users see avatar previews.\n\n### List Avatars and Open Previews\n\n```typescript\nasync function listAndPreviewAvatars(openInBrowser = true): Promise<void> {\n  const response = await fetch(\"https://api.heygen.com/v2/avatars\", {\n    headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n  });\n  const { data } = await response.json();\n\n  for (const avatar of data.avatars.slice(0, 5)) {\n    console.log(`\\n${avatar.avatar_name} (${avatar.gender})`);\n    console.log(`  ID: ${avatar.avatar_id}`);\n    console.log(`  Preview: ${avatar.preview_image_url}`);\n  }\n\n  // Open preview URLs directly in browser (no download needed)\n  if (openInBrowser) {\n    const { execSync } = require(\"child_process\");\n    for (const avatar of data.avatars.slice(0, 3)) {\n      // 'open' on macOS opens the URL in default browser - doesn't download\n      execSync(`open \"${avatar.preview_image_url}\"`);\n    }\n  }\n}\n```\n\n**Note:** The `open` command passes the URL to the browser - it does not download. The browser fetches and displays the image directly.\n\n### Workflow: Preview Before Generate\n\n1. **List available avatars** - get names, genders, and preview URLs\n2. **Open previews in browser** - `open <preview_image_url>` for quick visual check\n3. **User selects** preferred avatar by name or ID\n4. **Get avatar details** for `default_voice_id`\n5. **Generate video** with selected avatar\n\n```bash\n# Example workflow in terminal\n# 1. List avatars (agent shows options)\n# 2. Open preview for candidate\nopen \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n# 3. User says \"use Josh\"\n# 4. Agent gets details and generates\n```\n\n### Preview Fields in API Response\n\n| Field | Description |\n|-------|-------------|\n| `preview_image_url` | Static image of the avatar (JPG) - open in browser |\n| `preview_video_url` | Short video clip showing avatar animation |\n\nBoth URLs are publicly accessible - no authentication needed to view.\n\n## Listing Available Avatars\n\n### curl\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatars\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n### TypeScript\n\n```typescript\ninterface Avatar {\n  avatar_id: string;\n  avatar_name: string;\n  gender: \"male\" | \"female\";\n  preview_image_url: string;\n  preview_video_url: string;\n}\n\ninterface AvatarsResponse {\n  error: null | string;\n  data: {\n    avatars: Avatar[];\n    talking_photos: TalkingPhoto[];\n  };\n}\n\nasync function listAvatars(): Promise<Avatar[]> {\n  const response = await fetch(\"https://api.heygen.com/v2/avatars\", {\n    headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n  });\n\n  const json: AvatarsResponse = await response.json();\n\n  if (json.error) {\n    throw new Error(json.error);\n  }\n\n  return json.data.avatars;\n}\n```\n\n### Python\n\n```python\nimport requests\nimport os\n\ndef list_avatars() -> list:\n    response = requests.get(\n        \"https://api.heygen.com/v2/avatars\",\n        headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n    )\n\n    data = response.json()\n    if data.get(\"error\"):\n        raise Exception(data[\"error\"])\n\n    return data[\"data\"][\"avatars\"]\n```\n\n## Response Format\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"avatars\": [\n      {\n        \"avatar_id\": \"josh_lite3_20230714\",\n        \"avatar_name\": \"Josh\",\n        \"gender\": \"male\",\n        \"preview_image_url\": \"https://files.heygen.ai/...\",\n        \"preview_video_url\": \"https://files.heygen.ai/...\"\n      },\n      {\n        \"avatar_id\": \"angela_expressive_20231010\",\n        \"avatar_name\": \"Angela\",\n        \"gender\": \"female\",\n        \"preview_image_url\": \"https://files.heygen.ai/...\",\n        \"preview_video_url\": \"https://files.heygen.ai/...\"\n      }\n    ],\n    \"talking_photos\": []\n  }\n}\n```\n\n## Avatar Types\n\n### Public Avatars\n\nHeyGen provides a library of public avatars that anyone can use:\n\n```typescript\n// List only public avatars\nconst avatars = await listAvatars();\nconst publicAvatars = avatars.filter((a) => !a.avatar_id.startsWith(\"custom_\"));\n```\n\n### Private/Custom Avatars\n\nCustom avatars created from your own training footage:\n\n```typescript\nconst customAvatars = avatars.filter((a) => a.avatar_id.startsWith(\"custom_\"));\n```\n\n## Avatar Styles\n\nAvatars support different rendering styles:\n\n| Style | Description |\n|-------|-------------|\n| `normal` | Full body shot, standard framing |\n| `closeUp` | Close-up on face, more expressive |\n| `circle` | Avatar in circular frame (talking head) |\n| `voice_only` | Audio only, no video rendering |\n\n### When to Use Each Style\n\n| Use Case | Recommended Style |\n|----------|-------------------|\n| Full-screen presenter video | `normal` |\n| Personal/intimate content | `closeUp` |\n| Picture-in-picture overlay | `circle` |\n| Small corner widget | `circle` |\n| Podcast/audio content | `voice_only` |\n| Motion graphics with avatar overlay | `normal` or `closeUp` + transparent bg |\n\n### Using Avatar Styles\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\", // \"normal\" | \"closeUp\" | \"circle\" | \"voice_only\"\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello, world!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n  ],\n};\n```\n\n### Circle Style for Talking Heads\n\nCircle style is ideal for overlay compositions:\n\n```typescript\n// Circle avatar for picture-in-picture\n{\n  character: {\n    type: \"avatar\",\n    avatar_id: \"josh_lite3_20230714\",\n    avatar_style: \"circle\",\n  },\n  voice: { ... },\n  background: {\n    type: \"color\",\n    value: \"#00FF00\", // Green for chroma key, or use webm endpoint\n  },\n}\n```\n\n## Searching and Filtering Avatars\n\n### By Gender\n\n```typescript\nfunction filterByGender(avatars: Avatar[], gender: \"male\" | \"female\"): Avatar[] {\n  return avatars.filter((a) => a.gender === gender);\n}\n\nconst maleAvatars = filterByGender(avatars, \"male\");\nconst femaleAvatars = filterByGender(avatars, \"female\");\n```\n\n### By Name\n\n```typescript\nfunction searchByName(avatars: Avatar[], query: string): Avatar[] {\n  const lowerQuery = query.toLowerCase();\n  return avatars.filter((a) =>\n    a.avatar_name.toLowerCase().includes(lowerQuery)\n  );\n}\n\nconst results = searchByName(avatars, \"josh\");\n```\n\n## Avatar Groups\n\nAvatars are organized into groups for better management.\n\n### List Avatar Groups\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatar_group.list?include_public=true\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n#### Query Parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `include_public` | bool | false | Include public avatars in results |\n\n#### TypeScript\n\n```typescript\ninterface AvatarGroupItem {\n  id: string;\n  name: string;\n  created_at: number;\n  num_looks: number;\n  preview_image: string;\n  group_type: string;\n  train_status: string;\n  default_voice_id: string | null;\n}\n\ninterface AvatarGroupListResponse {\n  error: null | string;\n  data: {\n    avatar_group_list: AvatarGroupItem[];\n  };\n}\n\nasync function listAvatarGroups(\n  includePublic = true\n): Promise<AvatarGroupListResponse[\"data\"]> {\n  const params = new URLSearchParams({\n    include_public: includePublic.toString(),\n  });\n\n  const response = await fetch(\n    `https://api.heygen.com/v2/avatar_group.list?${params}`,\n    { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n  );\n\n  const json: AvatarGroupListResponse = await response.json();\n\n  if (json.error) {\n    throw new Error(json.error);\n  }\n\n  return json.data;\n}\n```\n\n### Get Avatars in a Group\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatar_group/{group_id}/avatars\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n## Using Avatars in Video Generation\n\n### Basic Avatar Usage\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Welcome to our product demo!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n  ],\n  dimension: { width: 1920, height: 1080 },\n};\n```\n\n### Multiple Scenes with Different Avatars\n\n```typescript\nconst multiSceneConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hi, I'm Josh. Let me introduce my colleague.\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"angela_expressive_20231010\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello! I'm Angela. Nice to meet you!\",\n        voice_id: \"2d5b0e6a8c3f47d9a1b2c3d4e5f60718\",\n      },\n    },\n  ],\n};\n```\n\n## Using Avatar's Default Voice\n\nMany avatars have a `default_voice_id` that's pre-matched for natural results. **This is the recommended approach** rather than manually selecting voices.\n\n### Recommended Flow\n\n```\n1. GET /v2/avatars           → Get list of avatar_ids\n2. GET /v2/avatar/{id}/details → Get default_voice_id for chosen avatar\n3. POST /v2/video/generate   → Use avatar_id + default_voice_id\n```\n\n### Get Avatar Details (v2 API)\n\nGiven an `avatar_id`, fetch its details including the default voice:\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatar/{avatar_id}/details\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n#### Response Format\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"type\": \"avatar\",\n    \"id\": \"josh_lite3_20230714\",\n    \"name\": \"Josh\",\n    \"gender\": \"male\",\n    \"preview_image_url\": \"https://files.heygen.ai/...\",\n    \"preview_video_url\": \"https://files.heygen.ai/...\",\n    \"premium\": false,\n    \"is_public\": true,\n    \"default_voice_id\": \"1bd001e7e50f421d891986aad5158bc8\",\n    \"tags\": [\"AVATAR_IV\"]\n  }\n}\n```\n\n#### TypeScript\n\n```typescript\ninterface AvatarDetails {\n  type: \"avatar\";\n  id: string;\n  name: string;\n  gender: \"male\" | \"female\";\n  preview_image_url: string;\n  preview_video_url: string;\n  premium: boolean;\n  is_public: boolean;\n  default_voice_id: string | null;\n  tags: string[];\n}\n\nasync function getAvatarDetails(avatarId: string): Promise<AvatarDetails> {\n  const response = await fetch(\n    `https://api.heygen.com/v2/avatar/${avatarId}/details`,\n    { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n  );\n\n  const json = await response.json();\n\n  if (json.error) {\n    throw new Error(json.error);\n  }\n\n  return json.data;\n}\n\n// Usage: Get default voice for a known avatar\nconst details = await getAvatarDetails(\"josh_lite3_20230714\");\nif (details.default_voice_id) {\n  console.log(`Using ${details.name} with default voice: ${details.default_voice_id}`);\n} else {\n  console.log(`${details.name} has no default voice, select manually`);\n}\n```\n\n#### Complete Example: Generate Video with Any Avatar's Default Voice\n\n```typescript\nasync function generateWithAvatarDefaultVoice(\n  avatarId: string,\n  script: string\n): Promise<string> {\n  // 1. Get avatar details to find default voice\n  const avatar = await getAvatarDetails(avatarId);\n\n  if (!avatar.default_voice_id) {\n    throw new Error(`Avatar ${avatar.name} has no default voice`);\n  }\n\n  // 2. Generate video with the avatar's default voice\n  const videoId = await generateVideo({\n    video_inputs: [{\n      character: {\n        type: \"avatar\",\n        avatar_id: avatar.id,\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: script,\n        voice_id: avatar.default_voice_id,\n      },\n    }],\n    dimension: { width: 1920, height: 1080 },\n  });\n\n  return videoId;\n}\n```\n\n### Why Use Default Voice?\n\n1. **Guaranteed gender match** - Avatar and voice are pre-paired\n2. **Natural lip sync** - Default voices are optimized for the avatar\n3. **Simpler code** - No need to fetch and match voices separately\n4. **Better quality** - HeyGen has tested this combination\n\n## Selecting the Right Avatar\n\n### Avatar Categories\n\nHeyGen avatars fall into distinct categories. Match the category to your use case:\n\n| Category | Examples | Best For |\n|----------|----------|----------|\n| **Business/Professional** | Josh, Angela, Wayne | Corporate videos, product demos, training |\n| **Casual/Friendly** | Lily, various lifestyle avatars | Social media, informal content |\n| **Themed/Seasonal** | Holiday-themed, costume avatars | Specific campaigns, seasonal content |\n| **Expressive** | Avatars with \"expressive\" in name | Engaging storytelling, dynamic content |\n\n### Selection Guidelines\n\n**For business/professional content:**\n- Choose avatars with neutral attire (business casual or formal)\n- Avoid themed or seasonal avatars (holiday costumes, casual clothing)\n- Preview the avatar to verify professional appearance\n- Consider your audience demographics when selecting gender and appearance\n\n**For casual/social content:**\n- More flexibility in avatar choice\n- Themed avatars can work for specific campaigns\n- Match avatar energy to content tone\n\n### Common Mistakes to Avoid\n\n1. **Using themed avatars for business content** - A holiday-themed avatar looks unprofessional in a product demo\n2. **Not previewing before generation** - Always `open <preview_url>` to verify appearance\n3. **Ignoring avatar style** - A `circle` style avatar may not work for full-screen presentations\n4. **Mismatched voice gender** - Always use the avatar's `default_voice_id` or match genders manually\n\n### Selection Checklist\n\nBefore generating a video:\n- [ ] Previewed avatar image/video in browser\n- [ ] Avatar appearance matches content tone (professional vs casual)\n- [ ] Avatar style (`normal`, `closeUp`, `circle`) fits the video format\n- [ ] Voice gender matches avatar gender\n- [ ] Using `default_voice_id` when available\n\n## Helper Functions\n\n### Get Avatar by ID\n\n```typescript\nasync function getAvatarById(avatarId: string): Promise<Avatar | null> {\n  const avatars = await listAvatars();\n  return avatars.find((a) => a.avatar_id === avatarId) || null;\n}\n```\n\n### Validate Avatar ID\n\n```typescript\nasync function isValidAvatarId(avatarId: string): Promise<boolean> {\n  const avatar = await getAvatarById(avatarId);\n  return avatar !== null;\n}\n```\n\n### Get Random Avatar\n\n```typescript\nasync function getRandomAvatar(gender?: \"male\" | \"female\"): Promise<Avatar> {\n  let avatars = await listAvatars();\n\n  if (gender) {\n    avatars = avatars.filter((a) => a.gender === gender);\n  }\n\n  const randomIndex = Math.floor(Math.random() * avatars.length);\n  return avatars[randomIndex];\n}\n```\n\n## Common Avatar IDs\n\nSome commonly used public avatar IDs (availability may vary):\n\n| Avatar ID | Name | Gender |\n|-----------|------|--------|\n| `josh_lite3_20230714` | Josh | Male |\n| `angela_expressive_20231010` | Angela | Female |\n| `wayne_20240422` | Wayne | Male |\n| `lily_20230614` | Lily | Female |\n\nAlways verify avatar availability by calling the list endpoint before using.\n\nFile v2.8.0:references/backgrounds.md\n\n---\nname: backgrounds\ndescription: Solid colors, images, and video backgrounds for HeyGen videos\n---\n\n# Video Backgrounds\n\nHeyGen supports various background types to customize the appearance of your avatar videos.\n\n## Background Types\n\n| Type | Description |\n|------|-------------|\n| `color` | Solid color background |\n| `image` | Static image background |\n| `video` | Looping video background |\n\n## Color Backgrounds\n\nThe simplest option - use a solid color:\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello with a colored background!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"color\",\n        value: \"#FFFFFF\", // White background\n      },\n    },\n  ],\n};\n```\n\n### Common Color Values\n\n| Color | Hex Value | Use Case |\n|-------|-----------|----------|\n| White | `#FFFFFF` | Clean, professional |\n| Black | `#000000` | Dramatic, cinematic |\n| Blue | `#0066CC` | Corporate, trustworthy |\n| Green | `#00FF00` | Chroma key (for compositing) |\n| Gray | `#808080` | Neutral, modern |\n\n### Using Transparent/Green Screen\n\nFor compositing in post-production:\n\n```typescript\nbackground: {\n  type: \"color\",\n  value: \"#00FF00\", // Green screen\n}\n```\n\n## Image Backgrounds\n\nUse a static image as background:\n\n### From URL\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Check out this custom background!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"image\",\n        url: \"https://example.com/my-background.jpg\",\n      },\n    },\n  ],\n};\n```\n\n### From Uploaded Asset\n\nFirst upload your image, then use the asset URL:\n\n```typescript\n// 1. Upload the image\nconst assetId = await uploadFile(\"./background.jpg\", \"image/jpeg\");\n\n// 2. Use in video config\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {...},\n      voice: {...},\n      background: {\n        type: \"image\",\n        url: `https://files.heygen.ai/asset/${assetId}`,\n      },\n    },\n  ],\n};\n```\n\n### Image Requirements\n\n- **Formats**: JPEG, PNG\n- **Recommended size**: Match video dimensions (e.g., 1920x1080 for 1080p)\n- **Aspect ratio**: Should match video aspect ratio\n- **File size**: Under 10MB recommended\n\n## Video Backgrounds\n\nUse a looping video as background:\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Dynamic video background!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"video\",\n        url: \"https://example.com/background-loop.mp4\",\n      },\n    },\n  ],\n};\n```\n\n### Video Requirements\n\n- **Format**: MP4 (H.264 codec recommended)\n- **Looping**: Video will loop if shorter than avatar content\n- **Audio**: Background video audio is typically muted\n- **File size**: Under 100MB recommended\n\n## Different Backgrounds Per Scene\n\nUse different backgrounds for each scene:\n\n```typescript\nconst multiBackgroundConfig = {\n  video_inputs: [\n    // Scene 1: Office background\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Let me start with an introduction.\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"image\",\n        url: \"https://example.com/office-bg.jpg\",\n      },\n    },\n    // Scene 2: Product showcase\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"closeUp\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Now let me show you our product.\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"image\",\n        url: \"https://example.com/product-bg.jpg\",\n      },\n    },\n    // Scene 3: Call to action\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Get started today!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"color\",\n        value: \"#1a1a2e\",\n      },\n    },\n  ],\n};\n```\n\n## Background Helper Functions\n\n### TypeScript\n\n```typescript\ntype BackgroundType = \"color\" | \"image\" | \"video\";\n\ninterface Background {\n  type: BackgroundType;\n  value?: string;\n  url?: string;\n}\n\nfunction createColorBackground(hexColor: string): Background {\n  return { type: \"color\", value: hexColor };\n}\n\nfunction createImageBackground(imageUrl: string): Background {\n  return { type: \"image\", url: imageUrl };\n}\n\nfunction createVideoBackground(videoUrl: string): Background {\n  return { type: \"video\", url: videoUrl };\n}\n\n// Preset backgrounds\nconst backgrounds = {\n  white: createColorBackground(\"#FFFFFF\"),\n  black: createColorBackground(\"#000000\"),\n  greenScreen: createColorBackground(\"#00FF00\"),\n  corporate: createColorBackground(\"#0066CC\"),\n};\n```\n\n## Best Practices\n\n1. **Match dimensions** - Background should match video dimensions\n2. **Consider avatar position** - Leave space where avatar will appear\n3. **Use contrasting colors** - Ensure avatar is visible against background\n4. **Optimize file sizes** - Compress images/videos for faster processing\n5. **Test with green screen** - For professional post-production workflows\n6. **Keep backgrounds simple** - Avoid distracting elements behind the avatar\n\n## Common Issues\n\n### Background Not Showing\n\n```typescript\n// Wrong: missing url/value\nbackground: {\n  type: \"image\"\n}\n\n// Correct\nbackground: {\n  type: \"image\",\n  url: \"https://example.com/bg.jpg\"\n}\n```\n\n### Aspect Ratio Mismatch\n\nIf your background doesn't match the video dimensions, it may be cropped or stretched. Always match your background aspect ratio to your video dimensions:\n\n```typescript\n// For 1920x1080 video\n// Use 1920x1080 background image\n\n// For 1080x1920 portrait video\n// Use 1080x1920 background image\n```\n\n### Video Background Audio\n\nBackground video audio is typically muted to avoid conflicting with the avatar's voice. If you need background music, add it as a separate audio track in post-production.\n\nFile v2.8.0:references/captions.md\n\n---\nname: captions\ndescription: Auto-generated captions and subtitle options for HeyGen videos\n---\n\n# Video Captions\n\nHeyGen can automatically generate captions (subtitles) for your videos, improving accessibility and engagement.\n\n## Enabling Captions\n\nCaptions can be enabled when generating a video:\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello! This video will have automatic captions.\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n  ],\n  // Caption settings (availability varies by plan)\n  caption: true,\n};\n```\n\n## Caption Configuration Options\n\n```typescript\ninterface CaptionConfig {\n  // Enable/disable captions\n  enabled: boolean;\n\n  // Caption style\n  style?: {\n    font_family?: string;\n    font_size?: number;\n    font_color?: string;\n    background_color?: string;\n    position?: \"top\" | \"bottom\";\n  };\n\n  // Language for caption generation\n  language?: string;\n}\n```\n\n## Caption Styles\n\n### Basic Captions\n\n```typescript\nconst config = {\n  video_inputs: [...],\n  caption: true, // Enable with default styling\n};\n```\n\n### Styled Captions\n\n```typescript\nconst config = {\n  video_inputs: [...],\n  caption: {\n    enabled: true,\n    style: {\n      font_family: \"Arial\",\n      font_size: 32,\n      font_color: \"#FFFFFF\",\n      background_color: \"rgba(0, 0, 0, 0.7)\",\n      position: \"bottom\",\n    },\n  },\n};\n```\n\n## Multi-Language Captions\n\nFor videos in different languages, captions are generated based on the voice language:\n\n```typescript\n// Spanish video with Spanish captions\nconst spanishConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"¡Hola! Este video tendrá subtítulos en español.\",\n        voice_id: \"spanish_voice_id\",\n      },\n    },\n  ],\n  caption: true,\n};\n```\n\n## Working with SRT Files\n\n### SRT File Format\n\nStandard SRT format:\n\n```srt\n1\n00:00:00,000 --> 00:00:03,000\nHello! This video will have\n\n2\n00:00:03,000 --> 00:00:06,000\nautomatic captions generated.\n\n3\n00:00:06,000 --> 00:00:09,000\nThey sync with the audio.\n```\n\n### Using Custom SRT\n\nFor video translation, you can provide your own SRT:\n\n```typescript\nconst translationConfig = {\n  input_video_id: \"original_video_id\",\n  output_languages: [\"es-ES\", \"fr-FR\"],\n  srt_key: \"path/to/custom.srt\", // Custom SRT file\n  srt_role: \"input\", // \"input\" or \"output\"\n};\n```\n\n## Caption Positioning\n\n### Bottom (Default)\n\nStandard position for most videos:\n\n```typescript\ncaption: {\n  enabled: true,\n  style: {\n    position: \"bottom\"\n  }\n}\n```\n\n### Top\n\nFor videos where bottom space is occupied:\n\n```typescript\ncaption: {\n  enabled: true,\n  style: {\n    position: \"top\"\n  }\n}\n```\n\n## Accessibility Best Practices\n\n1. **Always enable captions** - Improves accessibility for deaf/hard-of-hearing viewers\n2. **Use high contrast** - White text on dark background or vice versa\n3. **Readable font size** - At least 24px for standard video, larger for mobile\n4. **Don't cover important content** - Position captions away from key visual elements\n5. **Sync timing** - Ensure captions match audio timing accurately\n\n## Caption Helper Functions\n\n```typescript\ninterface CaptionStyle {\n  font_family: string;\n  font_size: number;\n  font_color: string;\n  background_color: string;\n  position: \"top\" | \"bottom\";\n}\n\nconst captionPresets: Record<string, CaptionStyle> = {\n  default: {\n    font_family: \"Arial\",\n    font_size: 32,\n    font_color: \"#FFFFFF\",\n    background_color: \"rgba(0, 0, 0, 0.7)\",\n    position: \"bottom\",\n  },\n  minimal: {\n    font_family: \"Arial\",\n    font_size: 28,\n    font_color: \"#FFFFFF\",\n    background_color: \"transparent\",\n    position: \"bottom\",\n  },\n  bold: {\n    font_family: \"Arial\",\n    font_size: 36,\n    font_color: \"#FFFFFF\",\n    background_color: \"rgba(0, 0, 0, 0.9)\",\n    position: \"bottom\",\n  },\n  branded: {\n    font_family: \"Roboto\",\n    font_size: 30,\n    font_color: \"#00D1FF\",\n    background_color: \"rgba(26, 26, 46, 0.9)\",\n    position: \"bottom\",\n  },\n};\n\nfunction createCaptionConfig(preset: keyof typeof captionPresets) {\n  return {\n    enabled: true,\n    style: captionPresets[preset],\n  };\n}\n```\n\n## Social Media Caption Considerations\n\n### TikTok / Instagram Reels\n\n- Position captions in center or upper portion\n- Avoid bottom 20% (covered by UI elements)\n- Use larger font sizes for mobile viewing\n\n```typescript\nconst socialCaptions = {\n  enabled: true,\n  style: {\n    font_size: 42,\n    position: \"top\", // Avoid bottom UI elements\n  },\n};\n```\n\n### YouTube\n\n- Standard bottom captions work well\n- YouTube also supports closed captions upload\n\n### LinkedIn\n\n- Captions highly recommended (many watch without sound)\n- Professional styling preferred\n\n## Limitations\n\n- Caption styles may be limited depending on your subscription tier\n- Some advanced caption features may require the web interface\n- Multi-speaker caption detection may have limited availability\n- Caption accuracy depends on audio quality and speech clarity\n\n## Integration with Video Translation\n\nWhen using video translation, captions are automatically handled:\n\n```typescript\n// Video translation includes caption generation\nconst translationConfig = {\n  input_video_id: \"original_video_id\",\n  output_languages: [\"es-ES\"],\n  // Captions generated in target language\n};\n```\n\nSee [video-translation.md](video-translation.md) for more details.\n\nFile v2.8.0:references/dimensions.md\n\n---\nname: dimensions\ndescription: Resolution options (720p/1080p) and aspect ratios for HeyGen videos\n---\n\n# Video Dimensions and Resolution\n\nHeyGen supports various video dimensions and aspect ratios to fit different platforms and use cases.\n\n## Standard Resolutions\n\n### Landscape (16:9)\n\n| Resolution | Width | Height | Use Case |\n|------------|-------|--------|----------|\n| 720p | 1280 | 720 | Standard quality, faster processing |\n| 1080p | 1920 | 1080 | High quality, most common |\n\n### Portrait (9:16)\n\n| Resolution | Width | Height | Use Case |\n|------------|-------|--------|----------|\n| 720p | 720 | 1280 | Mobile-first content |\n| 1080p | 1080 | 1920 | High quality vertical |\n\n### Square (1:1)\n\n| Resolution | Width | Height | Use Case |\n|------------|-------|--------|----------|\n| 720p | 720 | 720 | Social media posts |\n| 1080p | 1080 | 1080 | High quality square |\n\n## Setting Dimensions\n\n### TypeScript\n\n```typescript\n// Landscape 1080p\nconst landscapeConfig = {\n  video_inputs: [...],\n  dimension: {\n    width: 1920,\n    height: 1080\n  }\n};\n\n// Portrait 1080p\nconst portraitConfig = {\n  video_inputs: [...],\n  dimension: {\n    width: 1080,\n    height: 1920\n  }\n};\n\n// Square 1080p\nconst squareConfig = {\n  video_inputs: [...],\n  dimension: {\n    width: 1080,\n    height: 1080\n  }\n};\n```\n\n### curl\n\n```bash\n# Landscape 1080p\ncurl -X POST \"https://api.heygen.com/v2/video/generate\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"video_inputs\": [...],\n    \"dimension\": {\n      \"width\": 1920,\n      \"height\": 1080\n    }\n  }'\n```\n\n## Dimension Helper Functions\n\n```typescript\ntype AspectRatio = \"16:9\" | \"9:16\" | \"1:1\" | \"4:3\" | \"4:5\";\ntype Quality = \"720p\" | \"1080p\";\n\ninterface Dimensions {\n  width: number;\n  height: number;\n}\n\nfunction getDimensions(aspectRatio: AspectRatio, quality: Quality): Dimensions {\n  const configs: Record<AspectRatio, Record<Quality, Dimensions>> = {\n    \"16:9\": {\n      \"720p\": { width: 1280, height: 720 },\n      \"1080p\": { width: 1920, height: 1080 },\n    },\n    \"9:16\": {\n      \"720p\": { width: 720, height: 1280 },\n      \"1080p\": { width: 1080, height: 1920 },\n    },\n    \"1:1\": {\n      \"720p\": { width: 720, height: 720 },\n      \"1080p\": { width: 1080, height: 1080 },\n    },\n    \"4:3\": {\n      \"720p\": { width: 960, height: 720 },\n      \"1080p\": { width: 1440, height: 1080 },\n    },\n    \"4:5\": {\n      \"720p\": { width: 576, height: 720 },\n      \"1080p\": { width: 864, height: 1080 },\n    },\n  };\n\n  return configs[aspectRatio][quality];\n}\n\n// Usage\nconst youTubeDimensions = getDimensions(\"16:9\", \"1080p\");\nconst tikTokDimensions = getDimensions(\"9:16\", \"1080p\");\nconst instagramDimensions = getDimensions(\"1:1\", \"1080p\");\n```\n\n## Platform-Specific Recommendations\n\n### YouTube\n\n```typescript\nconst youtubeConfig = {\n  video_inputs: [...],\n  dimension: { width: 1920, height: 1080 }, // 16:9 landscape\n};\n```\n\n### TikTok / Instagram Reels / YouTube Shorts\n\n```typescript\nconst shortFormConfig = {\n  video_inputs: [...],\n  dimension: { width: 1080, height: 1920 }, // 9:16 portrait\n};\n```\n\n### Instagram Feed Post\n\n```typescript\nconst instagramFeedConfig = {\n  video_inputs: [...],\n  dimension: { width: 1080, height: 1080 }, // 1:1 square\n};\n```\n\n### LinkedIn\n\n```typescript\nconst linkedinConfig = {\n  video_inputs: [...],\n  dimension: { width: 1920, height: 1080 }, // 16:9 landscape preferred\n};\n```\n\n### Twitter/X\n\n```typescript\nconst twitterConfig = {\n  video_inputs: [...],\n  dimension: { width: 1280, height: 720 }, // 16:9, 720p is common\n};\n```\n\n## Avatar IV Dimensions\n\nFor Avatar IV (photo-based avatars), dimensions are set via orientation:\n\n```typescript\ntype VideoOrientation = \"portrait\" | \"landscape\" | \"square\";\n\nfunction getAvatarIVDimensions(orientation: VideoOrientation): Dimensions {\n  switch (orientation) {\n    case \"portrait\":\n      return { width: 720, height: 1280 };\n    case \"landscape\":\n      return { width: 1280, height: 720 };\n    case \"square\":\n      return { width: 720, height: 720 };\n  }\n}\n```\n\n## Custom Dimensions\n\nHeyGen supports custom dimensions within limits:\n\n```typescript\nconst customConfig = {\n  video_inputs: [...],\n  dimension: {\n    width: 1600,\n    height: 900  // Custom 16:9 at non-standard resolution\n  }\n};\n```\n\n### Dimension Constraints\n\n- **Minimum**: 128px on any side\n- **Maximum**: 4096px on any side\n- **Must be even numbers**: Both width and height must be divisible by 2\n\n```typescript\nfunction validateDimensions(width: number, height: number): boolean {\n  if (width < 128 || height < 128) {\n    throw new Error(\"Dimensions must be at least 128px\");\n  }\n  if (width > 4096 || height > 4096) {\n    throw new Error(\"Dimensions cannot exceed 4096px\");\n  }\n  if (width % 2 !== 0 || height % 2 !== 0) {\n    throw new Error(\"Dimensions must be even numbers\");\n  }\n  return true;\n}\n```\n\n## Resolution vs. Credit Cost\n\nHigher resolutions may consume more credits:\n\n| Resolution | Relative Cost |\n|------------|---------------|\n| 720p | Base rate |\n| 1080p | ~1.5x base rate |\n\nConsider using 720p for drafts and testing, then 1080p for final output.\n\n## Background Considerations\n\nMatch background image/video dimensions to your video dimensions:\n\n```typescript\n// For 1080p landscape video\nconst config = {\n  video_inputs: [\n    {\n      character: {...},\n      voice: {...},\n      background: {\n        type: \"image\",\n        url: \"https://example.com/1920x1080-background.jpg\" // Match video dimensions\n      }\n    }\n  ],\n  dimension: { width: 1920, height: 1080 }\n};\n```\n\n## Creating a Video Config Factory\n\n```typescript\ninterface VideoConfigOptions {\n  script: string;\n  avatarId: string;\n  voiceId: string;\n  platform: \"youtube\" | \"tiktok\" | \"instagram_feed\" | \"instagram_story\" | \"linkedin\";\n  quality?: \"720p\" | \"1080p\";\n}\n\nfunction createVideoConfig(options: VideoConfigOptions) {\n  const platformDimensions: Record<string, Dimensions> = {\n    youtube: { width: 1920, height: 1080 },\n    tiktok: { width: 1080, height: 1920 },\n    instagram_feed: { width: 1080, height: 1080 },\n    instagram_story: { width: 1080, height: 1920 },\n    linkedin: { width: 1920, height: 1080 },\n  };\n\n  const dimension = platformDimensions[options.platform];\n\n  // Scale down for 720p if requested\n  if (options.quality === \"720p\") {\n    dimension.width = Math.round((dimension.width * 720) / 1080);\n    dimension.height = Math.round((dimension.height * 720) / 1080);\n  }\n\n  return {\n    video_inputs: [\n      {\n        character: {\n          type: \"avatar\",\n          avatar_id: options.avatarId,\n          avatar_style: \"normal\",\n        },\n        voice: {\n          type: \"text\",\n          input_text: options.script,\n          voice_id: options.voiceId,\n        },\n      },\n    ],\n    dimension,\n  };\n}\n\n// Usage\nconst tiktokVideo = createVideoConfig({\n  script: \"Hey everyone! Check this out!\",\n  avatarId: \"josh_lite3_20230714\",\n  voiceId: \"1bd001e7e50f421d891986aad5158bc8\",\n  platform: \"tiktok\",\n  quality: \"1080p\",\n});\n```\n\nFile v2.8.0:references/photo-avatars.md\n\n---\nname: photo-avatars\ndescription: Creating avatars from photos (talking photos) for HeyGen\n---\n\n# Photo Avatars (Talking Photos)\n\nPhoto avatars allow you to animate a static photo and make it speak. This is useful for creating personalized video content from portraits, headshots, or any suitable image.\n\n## Creating a Photo Avatar from an Uploaded Image\n\nThe workflow is: **Upload Image → Create Avatar Group → Use in Video**\n\n### Step 1: Upload the Image\n\nUpload a portrait photo using the asset upload endpoint. The response includes an `image_key` which you'll use in the next step.\n\n```bash\ncurl -X POST \"https://upload.heygen.com/v1/asset\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: image/jpeg\" \\\n  --data-binary '@./portrait.jpg'\n```\n\nResponse:\n```json\n{\n  \"code\": 100,\n  \"data\": {\n    \"id\": \"741299e941764988b432ed3a6757878f\",\n    \"name\": \"741299e941764988b432ed3a6757878f\",\n    \"file_type\": \"image\",\n    \"url\": \"https://resource2.heygen.ai/image/.../original.jpg\",\n    \"image_key\": \"image/741299e941764988b432ed3a6757878f/original.jpg\"\n  }\n}\n```\n\n> **Important:** Save the `image_key` field (not the `id`). The `image_key` is the S3 path used to create the photo avatar.\n\nSee [assets.md](assets.md) for full upload details.\n\n### Step 2: Create Photo Avatar Group\n\nUse the `image_key` from the upload response to create a photo avatar group. This processes the image and creates a usable photo avatar.\n\n**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/avatar_group/create`\n\n```bash\ncurl -X POST \"https://api.heygen.com/v2/photo_avatar/avatar_group/create\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"image_key\": \"image/741299e941764988b432ed3a6757878f/original.jpg\",\n    \"name\": \"My Photo Avatar\"\n  }'\n```\n\n| Field | Type | Req | Description |\n|-------|------|:---:|-------------|\n| `image_key` | string | ✓ | S3 image key from upload response |\n| `name` | string | ✓ | Display name for the avatar |\n| `generation_id` | string | | If using AI-generated photo (see below) |\n\nResponse:\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"id\": \"045c260bc0364727b2cbe50442c3a5bf\",\n    \"image_url\": \"https://files2.heygen.ai/...\",\n    \"created_at\": 1771798135.777256,\n    \"name\": \"My Photo Avatar\",\n    \"status\": \"pending\",\n    \"group_id\": \"045c260bc0364727b2cbe50442c3a5bf\",\n    \"is_motion\": false,\n    \"business_type\": \"uploaded\"\n  }\n}\n```\n\nThe `id` (same as `group_id`) is your `talking_photo_id` for video generation.\n\n### Step 3: Wait for Processing\n\nThe photo avatar starts with `status: \"pending\"` and transitions to `\"completed\"` within seconds. Poll the status endpoint:\n\n**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/{id}`\n\n```bash\ncurl \"https://api.heygen.com/v2/photo_avatar/045c260bc0364727b2cbe50442c3a5bf\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\nWait until `status` is `\"completed\"` before using in video generation.\n\n### Step 4: Use in Video Generation\n\nUse the photo avatar `id` as `talking_photo_id`:\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"talking_photo\",\n        talking_photo_id: \"045c260bc0364727b2cbe50442c3a5bf\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello! This is my photo avatar speaking.\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n  ],\n  dimension: { width: 1920, height: 1080 },\n};\n```\n\n## TypeScript: Complete Workflow\n\n```typescript\nimport fs from \"fs\";\n\ninterface AssetUploadResponse {\n  code: number;\n  data: {\n    id: string;\n    image_key: string;\n    url: string;\n  };\n}\n\ninterface PhotoAvatarResponse {\n  error: string | null;\n  data: {\n    id: string;\n    group_id: string;\n    image_url: string;\n    name: string;\n    status: string;\n    is_motion: boolean;\n    business_type: string;\n  };\n}\n\nasync function createPhotoAvatar(\n  imagePath: string,\n  name: string\n): Promise<string> {\n  // 1. Upload image\n  const fileBuffer = fs.readFileSync(imagePath);\n  const uploadResponse = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": \"image/jpeg\",\n    },\n    body: fileBuffer,\n  });\n\n  const uploadJson: AssetUploadResponse = await uploadResponse.json();\n  if (uploadJson.code !== 100) {\n    throw new Error(\"Upload failed\");\n  }\n\n  const imageKey = uploadJson.data.image_key;\n\n  // 2. Create avatar group\n  const createResponse = await fetch(\n    \"https://api.heygen.com/v2/photo_avatar/avatar_group/create\",\n    {\n      method: \"POST\",\n      headers: {\n        \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({ image_key: imageKey, name }),\n    }\n  );\n\n  const createJson: PhotoAvatarResponse = await createResponse.json();\n  if (createJson.error) {\n    throw new Error(createJson.error);\n  }\n\n  const photoAvatarId = createJson.data.id;\n\n  // 3. Wait for processing\n  await waitForPhotoAvatar(photoAvatarId);\n\n  return photoAvatarId;\n}\n\nasync function waitForPhotoAvatar(id: string): Promise<void> {\n  for (let i = 0; i < 30; i++) {\n    const response = await fetch(\n      `https://api.heygen.com/v2/photo_avatar/${id}`,\n      { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n    );\n\n    const json: PhotoAvatarResponse = await response.json();\n\n    if (json.data.status === \"completed\") return;\n    if (json.data.status === \"failed\") {\n      throw new Error(\"Photo avatar processing failed\");\n    }\n\n    await new Promise((r) => setTimeout(r, 2000));\n  }\n\n  throw new Error(\"Photo avatar processing timed out\");\n}\n\nasync function createVideoFromPhoto(\n  photoPath: string,\n  script: string,\n  voiceId: string\n): Promise<string> {\n  // 1. Create photo avatar\n  const talkingPhotoId = await createPhotoAvatar(photoPath, \"Video Avatar\");\n\n  // 2. Generate video\n  const response = await fetch(\"https://api.heygen.com/v2/video/generate\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      video_inputs: [\n        {\n          character: {\n            type: \"talking_photo\",\n            talking_photo_id: talkingPhotoId,\n          },\n          voice: {\n            type: \"text\",\n            input_text: script,\n            voice_id: voiceId,\n          },\n        },\n      ],\n      dimension: { width: 1920, height: 1080 },\n    }),\n  });\n\n  const { data } = await response.json();\n  return data.video_id;\n}\n```\n\n## Python: Complete Workflow\n\n```python\nimport requests\nimport os\nimport time\n\ndef create_photo_avatar(image_path: str, name: str) -> str:\n    api_key = os.environ[\"HEYGEN_API_KEY\"]\n\n    # 1. Upload image\n    with open(image_path, \"rb\") as f:\n        upload_resp = requests.post(\n            \"https://upload.heygen.com/v1/asset\",\n            headers={\n                \"X-Api-Key\": api_key,\n                \"Content-Type\": \"image/jpeg\",\n            },\n            data=f,\n        )\n\n    upload_data = upload_resp.json()\n    if upload_data.get(\"code\") != 100:\n        raise Exception(\"Upload failed\")\n\n    image_key = upload_data[\"data\"][\"image_key\"]\n\n    # 2. Create avatar group\n    create_resp = requests.post(\n        \"https://api.heygen.com/v2/photo_avatar/avatar_group/create\",\n        headers={\n            \"X-Api-Key\": api_key,\n            \"Content-Type\": \"application/json\",\n        },\n        json={\"image_key\": image_key, \"name\": name},\n    )\n\n    create_data = create_resp.json()\n    if create_data.get(\"error\"):\n        raise Exception(create_data[\"error\"])\n\n    photo_avatar_id = create_data[\"data\"][\"id\"]\n\n    # 3. Wait for processing\n    for _ in range(30):\n        status_resp = requests.get(\n            f\"https://api.heygen.com/v2/photo_avatar/{photo_avatar_id}\",\n            headers={\"X-Api-Key\": api_key},\n        )\n        status = status_resp.json()[\"data\"][\"status\"]\n        if status == \"completed\":\n            return photo_avatar_id\n        if status == \"failed\":\n            raise Exception(\"Photo avatar processing failed\")\n        time.sleep(2)\n\n    raise Exception(\"Photo avatar processing timed out\")\n```\n\n## Listing Existing Talking Photos\n\nRetrieve all talking photos in your account:\n\n**Endpoint:** `GET https://api.heygen.com/v1/talking_photo.list`\n\n```bash\ncurl \"https://api.heygen.com/v1/talking_photo.list\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\nResponse:\n```json\n{\n  \"code\": 100,\n  \"data\": [\n    {\n      \"id\": \"ef0ed70f72c6497793e5e36e434d2aea\",\n      \"image_url\": \"https://files2.heygen.ai/talking_photo/.../image.WEBP\",\n      \"circle_image\": \"\"\n    }\n  ]\n}\n```\n\nEach `id` can be used as `talking_photo_id` in video generation.\n\n## Adding Photos to an Existing Group\n\nAdd additional photo looks to an existing avatar group:\n\n**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/avatar_group/add`\n\n```typescript\nasync function addPhotosToGroup(\n  groupId: string,\n  imageKeys: string[],\n  name: string\n): Promise<void> {\n  const response = await fetch(\n    \"https://api.heygen.com/v2/photo_avatar/avatar_group/add\",\n    {\n      method: \"POST\",\n      headers: {\n        \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        group_id: groupId,\n        image_keys: imageKeys,\n        name,\n      }),\n    }\n  );\n\n  const json = await response.json();\n  if (json.error) {\n    throw new Error(json.error);\n  }\n}\n```\n\n## Training a Photo Avatar Group\n\nTrain the avatar group for improved animation quality:\n\n**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/train`\n\n```bash\ncurl -X POST \"https://api.heygen.com/v2/photo_avatar/train\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"group_id\": \"045c260bc0364727b2cbe50442c3a5bf\"}'\n```\n\nCheck training status:\n\n**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/train/status/{group_id}`\n\n## Avatar IV Video Generation\n\nAvatar IV is HeyGen's latest photo avatar technology with improved quality and natural motion. It generates a video directly from an uploaded image, bypassing the avatar group creation step.\n\n**Endpoint:** `POST https://api.heygen.com/v2/video/av4/generate`\n\n```bash\ncurl -X POST \"https://api.heygen.com/v2/video/av4/generate\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"image_key\": \"image/741299e941764988b432ed3a6757878f/original.jpg\",\n    \"script\": \"Hello! This is Avatar IV with enhanced quality.\",\n    \"voice_id\": \"1bd001e7e50f421d891986aad5158bc8\",\n    \"video_orientation\": \"landscape\",\n    \"video_title\": \"My Avatar IV Video\"\n  }'\n```\n\n| Field | Type | Req | Description |\n|-------|------|:---:|-------------|\n| `image_key` | string | ✓ | S3 image key from asset upload |\n| `script` | string | ✓ | Text for the avatar to speak |\n| `voice_id` | string | ✓ | Voice to use |\n| `video_orientation` | string | | `\"portrait\"`, `\"landscape\"`, or `\"square\"` |\n| `video_title` | string | | Title for the video |\n| `fit` | string | | `\"cover\"` or `\"contain\"` |\n| `custom_motion_prompt` | string | | Motion/expression description |\n| `enhance_custom_motion_prompt` | boolean | | Enhance the motion prompt with AI |\n\n### TypeScript\n\n```typescript\ninterface AvatarIVRequest {\n  image_key: string;\n  script: string;\n  voice_id: string;\n  video_orientation?: \"portrait\" | \"landscape\" | \"square\";\n  video_title?: string;\n  fit?: \"cover\" | \"contain\";\n  custom_motion_prompt?: string;\n  enhance_custom_motion_prompt?: boolean;\n}\n\ninterface AvatarIVResponse {\n  error: null | string;\n  data: {\n    video_id: string;\n  };\n}\n\nasync function generateAvatarIVVideo(\n  config: AvatarIVRequest\n): Promise<string> {\n  const response = await fetch(\n    \"https://api.heygen.com/v2/video/av4/generate\",\n    {\n      method: \"POST\",\n      headers: {\n        \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify(config),\n    }\n  );\n\n  const json: AvatarIVResponse = await response.json();\n\n  if (json.error) {\n    throw new Error(json.error);\n  }\n\n  return json.data.video_id;\n}\n```\n\n### Avatar IV Options\n\n| Orientation | Dimensions | Use Case |\n|-------------|------------|----------|\n| `portrait` | 720x1280 | TikTok, Stories |\n| `landscape` | 1280x720 | YouTube, Web |\n| `square` | 720x720 | Instagram Feed |\n\n| Fit | Description |\n|-----|-------------|\n| `cover` | Fill the frame, may crop edges |\n| `contain` | Fit entire image, may show background |\n\n### Custom Motion Prompts\n\n```typescript\nconst videoId = await generateAvatarIVVideo({\n  image_key: \"image/.../original.jpg\",\n  script: \"Let me tell you about our product.\",\n  voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n  custom_motion_prompt: \"nodding head and smiling\",\n  enhance_custom_motion_prompt: true,\n});\n```\n\n## Generating AI Photo Avatars\n\nGenerate synthetic photo avatars from text descriptions instead of uploading a photo.\n\n**Endpoint:** `POST https://api.heygen.com/v2/photo_avatar/photo/generate`\n\n> **IMPORTANT: All 8 fields are REQUIRED.** The API will reject requests missing any field.\n> When a user asks to \"generate an AI avatar of a professional man\", you need to ask for or select values for ALL fields below.\n\n### Required Fields (ALL must be provided)\n\n| Field | Type | Allowed Values |\n|-------|------|----------------|\n| `name` | string | Name for the generated avatar |\n| `age` | enum | `\"Young Adult\"`, `\"Early Middle Age\"`, `\"Late Middle Age\"`, `\"Senior\"`, `\"Unspecified\"` |\n| `gender` | enum | `\"Woman\"`, `\"Man\"`, `\"Unspecified\"` |\n| `ethnicity` | enum | `\"White\"`, `\"Black\"`, `\"Asian American\"`, `\"East Asian\"`, `\"South East Asian\"`, `\"South Asian\"`, `\"Middle Eastern\"`, `\"Pacific\"`, `\"Hispanic\"`, `\"Unspecified\"` |\n| `orientation` | enum | `\"square\"`, `\"horizontal\"`, `\"vertical\"` |\n| `pose` | enum | `\"half_body\"`, `\"close_up\"`, `\"full_body\"` |\n| `style` | enum | `\"Realistic\"`, `\"Pixar\"`, `\"Cinematic\"`, `\"Vintage\"`, `\"Noir\"`, `\"Cyberpunk\"`, `\"Unspecified\"` |\n| `appearance` | string | Text prompt describing appearance (clothing, mood, lighting, etc). Max 1000 chars |\n\n### curl Example\n\n```bash\ncurl -X POST \"https://api.heygen.com/v2/photo_avatar/photo/generate\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Sarah Product Demo\",\n    \"age\": \"Young Adult\",\n    \"gender\": \"Woman\",\n    \"ethnicity\": \"White\",\n    \"orientation\": \"horizontal\",\n    \"pose\": \"half_body\",\n    \"style\": \"Realistic\",\n    \"appearance\": \"Professional woman with a friendly smile, wearing a navy blue blazer over a white blouse, soft studio lighting, clean neutral background\"\n  }'\n```\n\nResponse:\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"generation_id\": \"6a7f7f2795de4599bec7cf1e06babe30\"\n  }\n}\n```\n\n### Check Generation Status\n\n**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/generation/{generation_id}`\n\nThe response includes multiple generated images to choose from:\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"id\": \"6a7f7f2795de4599bec7cf1e06babe30\",\n    \"status\": \"success\",\n    \"image_url_list\": [\n      \"https://resource2.heygen.ai/photo_generation/.../image1.jpg\",\n      \"https://resource2.heygen.ai/photo_generation/.../image2.jpg\",\n      \"https://resource2.heygen.ai/photo_generation/.../image3.jpg\",\n      \"https://resource2.heygen.ai/photo_generation/.../image4.jpg\"\n    ],\n    \"image_key_list\": [\n      \"photo_generation/.../image1.jpg\",\n      \"photo_generation/.../image2.jpg\",\n      \"photo_generation/.../image3.jpg\",\n      \"photo_generation/.../image4.jpg\"\n    ]\n  }\n}\n```\n\n### TypeScript\n\n```typescript\ninterface GeneratePhotoAvatarRequest {\n  name: string;\n  age: \"Young Adult\" | \"Early Middle Age\" | \"Late Middle Age\" | \"Senior\" | \"Unspecified\";\n  gender: \"Woman\" | \"Man\" | \"Unspecified\";\n  ethnicity: \"White\" | \"Black\" | \"Asian American\" | \"East Asian\" | \"South East Asian\" | \"South Asian\" | \"Middle Eastern\" | \"Pacific\" | \"Hispanic\" | \"Unspecified\";\n  orientation: \"square\" | \"horizontal\" | \"vertical\";\n  pose: \"half_body\" | \"close_up\" | \"full_body\";\n  style: \"Realistic\" | \"Pixar\" | \"Cinematic\" | \"Vintage\" | \"Noir\" | \"Cyberpunk\" | \"Unspecified\";\n  appearance: string;\n}\n\ninterface GeneratePhotoAvatarResponse {\n  error: string | null;\n  data: {\n    generation_id: string;\n  };\n}\n\ninterface PhotoGenerationStatus {\n  error: string | null;\n  data: {\n    id: string;\n    status: \"pending\" | \"processing\" | \"success\" | \"failed\";\n    msg: string | null;\n    image_url_list?: string[];\n    image_key_list?: string[];\n  };\n}\n\nasync function generatePhotoAvatar(\n  config: GeneratePhotoAvatarRequest\n): Promise<string> {\n  const response = await fetch(\n    \"https://api.heygen.com/v2/photo_avatar/photo/generate\",\n    {\n      method: \"POST\",\n      headers: {\n        \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify(config),\n    }\n  );\n\n  const json: GeneratePhotoAvatarResponse = await response.json();\n\n  if (json.error) {\n    throw new Error(`Photo avatar generation failed: ${json.error}`);\n  }\n\n  return json.data.generation_id;\n}\n\nasync function waitForPhotoGeneration(\n  generationId: string\n): Promise<string[]> {\n  for (let i = 0; i < 60; i++) {\n    const response = await fetch(\n      `https://api.heygen.com/v2/photo_avatar/generation/${generationId}`,\n      { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n    );\n\n    const json: PhotoGenerationStatus = await response.json();\n\n    if (json.error) throw new Error(json.error);\n\n    if (json.data.status === \"success\") {\n      return json.data.image_key_list!;\n    }\n\n    if (json.data.status === \"failed\") {\n      throw new Error(json.data.msg ?? \"Photo generation failed\");\n    }\n\n    await new Promise((r) => setTimeout(r, 5000));\n  }\n\n  throw new Error(\"Photo generation timed out\");\n}\n```\n\n### AI Photo → Avatar Group → Video\n\nUse a generated AI photo to create an avatar group, then generate a video:\n\n```typescript\n// 1. Generate AI photo\nconst generationId = await generatePhotoAvatar({\n  name: \"Product Demo Host\",\n  age: \"Young Adult\",\n  gender: \"Woman\",\n  ethnicity: \"Unspecified\",\n  orientation: \"horizontal\",\n  pose: \"half_body\",\n  style: \"Realistic\",\n  appearance: \"Professional woman, navy blazer, friendly smile, soft lighting\",\n});\n\n// 2. Wait for generation and pick first result\nconst imageKeys = await waitForPhotoGeneration(generationId);\nconst selectedImageKey = imageKeys[0];\n\n// 3. Create avatar group from the AI photo\nconst createResponse = await fetch(\n  \"https://api.heygen.com/v2/photo_avatar/avatar_group/create\",\n  {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify({\n      image_key: selectedImageKey,\n      name: \"Product Demo Host\",\n      generation_id: generationId,\n    }),\n  }\n);\n\nconst { data } = await createResponse.json();\nconst talkingPhotoId = data.id;\n\n// 4. Generate video (after status is \"completed\")\nconst videoId = await generateVideo({\n  video_inputs: [{\n    character: {\n      type: \"talking_photo\",\n      talking_photo_id: talkingPhotoId,\n    },\n    voice: {\n      type: \"text\",\n      input_text: \"Welcome to our product demo!\",\n      voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n    },\n  }],\n  dimension: { width: 1920, height: 1080 },\n});\n```\n\n### Pre-Generation Checklist\n\nBefore calling the AI generation API, ensure you have values for ALL fields:\n\n| # | Field | Question to Ask / Default |\n|---|-------|---------------------------|\n| 1 | `name` | What should we call this avatar? |\n| 2 | `age` | Young Adult / Early Middle Age / Late Middle Age / Senior? |\n| 3 | `gender` | Woman / Man? |\n| 4 | `ethnicity` | Which ethnicity? (see enum values above) |\n| 5 | `orientation` | horizontal (landscape) / vertical (portrait) / square? |\n| 6 | `pose` | half_body (recommended) / close_up / full_body? |\n| 7 | `style` | Realistic (recommended) / Cinematic / other? |\n| 8 | `appearance` | Describe clothing, expression, lighting, background |\n\n**If the user only provides a vague request** like \"create a professional looking man\", ask them to specify the missing fields OR make reasonable defaults (e.g., \"Early Middle Age\", \"Realistic\" style, \"half_body\" pose, \"horizontal\" orientation).\n\n### Appearance Prompt Tips\n\nThe `appearance` field is a text prompt - be descriptive:\n\n**Good prompts:**\n- \"Professional woman with shoulder-length brown hair, wearing a light blue button-down shirt, warm friendly smile, soft studio lighting, clean white background\"\n- \"Young man with short black hair, casual tech startup style, wearing a dark hoodie, confident expression, modern office background with plants\"\n\n**Avoid:**\n- Vague descriptions: \"a nice person\"\n- Conflicting attributes\n- Requesting specific real people\n\n## Managing Photo Avatars\n\n### Get Photo Avatar Details\n\n**Endpoint:** `GET https://api.heygen.com/v2/photo_avatar/{id}`\n\n```typescript\nasync function getPhotoAvatar(id: string): Promise<PhotoAvatarResponse> {\n  const response = await fetch(\n    `https://api.heygen.com/v2/photo_avatar/${id}`,\n    { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n  );\n  return response.json();\n}\n```\n\n### Delete Photo Avatar\n\n**Endpoint:** `DELETE https://api.heygen.com/v2/photo_avatar/{id}`\n\n```typescript\nasync function deletePhotoAvatar(id: string): Promise<void> {\n  const response = await fetch(\n    `https://api.heygen.com/v2/photo_avatar/${id}`,\n    {\n      method: \"DELETE\",\n      headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n    }\n  );\n\n  if (!response.ok) {\n    throw new Error(\"Failed to delete photo avatar\");\n  }\n}\n```\n\n### Delete Photo Avatar Group\n\n**Endpoint:** `DELETE https://api.heygen.com/v2/photo_avatar_group/{group_id}`\n\n```typescript\nasync function deletePhotoAvatarGroup(groupId: string): Promise<void> {\n  const response = await fetch(\n    `https://api.heygen.com/v2/photo_avatar_group/${groupId}`,\n    {\n      method: \"DELETE\",\n      headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n    }\n  );\n\n  if (!response.ok) {\n    throw new Error(\"Failed to delete photo avatar group\");\n  }\n}\n```\n\n## API Reference\n\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| `upload.heygen.com/v1/asset` | POST | Upload image (returns `image_key`) |\n| `/v2/photo_avatar/avatar_group/create` | POST | Create photo avatar from `image_key` |\n| `/v2/photo_avatar/avatar_group/add` | POST | Add photos to existing group |\n| `/v2/photo_avatar/train` | POST | Train avatar group |\n| `/v2/photo_avatar/train/status/{group_id}` | GET | Check training status |\n| `/v2/photo_avatar/{id}` | GET | Get photo avatar details/status |\n| `/v2/photo_avatar/{id}` | DELETE | Delete photo avatar |\n| `/v2/photo_avatar_group/{id}` | DELETE | Delete avatar group |\n| `/v2/photo_avatar/photo/generate` | POST | Generate AI photo from text |\n| `/v2/photo_avatar/generation/{id}` | GET | Check AI generation status |\n| `/v2/video/av4/generate` | POST | Avatar IV video from `image_key` |\n| `/v1/talking_photo.list` | GET | List all existing talking photos |\n| `/v2/video/generate` | POST | Generate video with `talking_photo_id` |\n\n## Photo Requirements\n\n### Technical Requirements\n\n| Aspect | Requirement |\n|--------|-------------|\n| Format | JPEG, PNG |\n| Resolution | Minimum 512x512px |\n| File size | Under 10MB |\n| Face visibility | Clear, front-facing |\n\n### Quality Guidelines\n\n1. **Lighting** - Even, natural lighting on face\n2. **Expression** - Neutral or slight smile\n3. **Background** - Simple, uncluttered\n4. **Face position** - Centered, not cut off\n5. **Clarity** - Sharp, in focus\n6. **Angle** - Straight-on or slight angle\n\n## Best Practices\n\n1. **Use high-quality photos** - Better input = better output\n2. **Front-facing portraits** - Work best for animation\n3. **Neutral expressions** - Allow for more natural animation\n4. **Use Avatar IV for best quality** - Latest generation technology\n5. **Train avatar groups** - Improves animation quality\n6. **Reuse photo avatar IDs** - Once created, use the same `talking_photo_id` across multiple videos\n\n## Limitations\n\n- Photo quality significantly affects output\n- Side-profile photos have limited support\n- Full-body photos may not animate properly\n- Some expressions may look unnatural\n- Processing time varies by complexity\n\nFile v2.8.0:references/prompt-examples.md\n\n---\nname: prompt-examples\ndescription: Full production prompt examples and ready-to-use templates for Video Agent\n---\n\n# Video Agent Prompt Examples\n\n## Full Example: Brief to Production Prompt\n\n### Input Brief\n\n```\nTopic: Monthly company report for a SaaS startup\nKey data: $141M ARR (up from $54M), 1.85M signups (+28%), 3M paid videos/month\nCustomer story: Creator built AI character, 2.5M followers, 20 min/video\nChallenge: Organic traffic volatile, -16% last week\nDuration: ~90 seconds\nTone: Confident CEO, data-backed\n```\n\n### Output Prompt\n\n```\nFORMAT: Bloomberg-style company report. 90 seconds. Fast-paced, data-dense.\nRecord-breaking month. Proud but analytical.\n\nTONE: Confident, direct, data-backed. Highlights hit hard with numbers.\nCustomer stories are the emotional core. Challenges are honest — no spin.\n\nAVATAR: Man in simple black crew-neck tee, standing in a modern glass-walled\noffice at golden hour. Behind him, a wall-mounted display shows the company logo\nin soft blue glow. Monitor to his right shows a dashboard with upward-trending\ncharts. Desk beside him: laptop, half-empty flat white, scattered sticky notes.\nWarm afternoon light through floor-to-ceiling windows, long shadows on polished\nconcrete. Minimal, focused startup HQ.\n\nSTYLE — SWISS PULSE (Müller-Brockmann): Grid-locked compositions. Black (#1a1a1a),\nwhite, electric blue (#0066FF), warm amber (#FF9500) for records. Helvetica Bold\nheadlines, Regular labels. Numbers LARGE. Animated counters count up from 0.\nDiagonal compositions on accent moments. Grid wipe transitions. No dissolves.\n\nCRITICAL ON-SCREEN TEXT (display literally):\n- \"1.85M SIGNUPS — +28% MoM\"\n- \"$2.12M NEW SUBSCRIPTION REVENUE\"\n- \"$54M → $141M ARR\"\n- \"2.5M FOLLOWERS\" and \"20 MIN / VIDEO\"\n- Quote: \"Use technology to serve the message, not distract from it.\"\n- \"ORGANIC: 65% OF SUBS — VOLATILE\"\n\nMUSIC: Upbeat electronic with a driving beat. Tycho meets Bloomberg opening theme.\nBuilds through highlights, warms for customer story, softens for challenges, peaks\non close.\n\n---\n\nSCENE 1 — A-ROLL (8s)\n[Avatar center-frame, energetic, leaning slightly forward]\nVOICEOVER: \"January was a record month. New highs across acquisition, revenue,\nand product velocity. Here's the full picture.\"\nLower-third SLIDES in: \"COMPANY NAME | JANUARY 2026\" white on blue bar.\nGrid wipe.\n\nSCENE 2 — FULL SCREEN B-ROLL (12s)\n[NO AVATAR — motion graphic only]\nVOICEOVER: \"One-point-eight-five million signups — twenty-eight percent month\nover month. Two-point-one-two million in new subscription revenue. Both all-time\nhighs.\"\nLAYER 1: Dark #1a1a1a background with thin grid lines pulsing at 8% opacity.\nLAYER 2: \"1.85M\" SLAMS in from left, white Bold 140pt. \"SIGNUPS\" types on\n         in electric blue 32pt uppercase. \"+28% MoM\" appears in amber.\nLAYER 3: Three stat cards CASCADE from top-right, staggered 0.3s:\n         \"$2.12M New Revenue\" — \"$3.4M Business ARR\" — \"$3M Pro ARR.\"\n         Each number COUNTS UP from 0.\nLAYER 4: Bottom ticker scrolls: \"Non-brand search +36% • Brand impressions 9.2M\n         • Weekly subs +20.5%\"\nLAYER 5: Grid lines RIPPLE outward on \"1.85M\" slam. Diagonal amber bar behind\n         stat cards.\nHard cut.\n\nSCENE 3 — FULL SCREEN B-ROLL (12s)\n[NO AVATAR — motion graphic only]\nVOICEOVER: \"Zoom out. Twelve months ago — fifty-four million ARR. Today —\none hundred forty-one million. Nearly three X in a single year.\"\nLAYER 1: Dark background, subtle grid scrolling upward.\nLAYER 2: Animated line chart DRAWS ITSELF left to right. Y-axis: $50M to $150M.\n         Final point \"$140.84M\" glows amber and pulses.\nLAYER 3: Milestone annotations float in at key data points.\nLAYER 4: Second smaller chart below — \"Paid Videos\" 0.91M to 2.97M, same style.\nLAYER 5: Thin grid lines converge toward final data point. Scan line sweeps.\nGrid wipe.\n\nSCENE 4 — A-ROLL (8s)\n[Avatar center-frame, warm tone, genuine smile]\nVOICEOVER: \"But the numbers only tell half the story. The other half is the\npeople building on the platform.\"\nLower-third: \"Customer Spotlight\"\n\nSCENE 5 — FULL SCREEN B-ROLL (12s)\n[NO AVATAR — warm palette]\nVOICEOVER: \"An AI character built entirely on the platform. Twenty minutes\nper video. Two-point-five million Instagram followers. The creator's principle:\nuse technology to serve the message, not distract from it.\"\nLAYER 1: Dark background with warm amber grid lines at low opacity.\nLAYER 2: \"CHARACTER NAME\" in large white, center-top, 80pt.\nLAYER 3: Stats cascade from right: \"2.5M Followers\" COUNTS UP in amber —\n         \"20 min/video\" — \"7x Faster.\" Each a glowing node.\nLAYER 4: Quote card SLIDES UP: \"Use technology to serve the message, not\n         distract from it.\" Types on word by word.\nLAYER 5: Warm light bloom. Grid lines soften into curved arcs.\nGrid wipe.\n\nSCENE 6 — A-ROLL (10s)\n[Avatar center-frame, serious/candid]\nVOICEOVER: \"Now the honest part. Organic drives sixty-five percent of\nsubscriptions and it's volatile. Non-brand traffic dropped sixteen percent\nlast week. We've rebuilt attribution and we're investing in SEO.\"\nLower-third: \"Challenges\"\n\nSCENE 7 — A-ROLL (7s)\n[Avatar center-frame, energy lifts, direct eye contact]\nVOICEOVER: \"Fifty-four million to one-forty-one in twelve months. Three million\npaid videos a month. January set the bar — now we raise it.\"\nEnd card: Logo centered, blue glow fade-in. Grid lines converge. Music peaks.\n\n---\n\nNARRATION STYLE: CEO energy — conviction backed by data. Fast on highlights.\nWarm on customer stories. Candid on challenges. Close with forward momentum.\n```\n\n## Ready-to-Use Templates\n\n### Tech News Briefing\n```\nFORMAT: 75-second high-energy tech briefing. Think: Bloomberg meets Vice.\n\nAVATAR: [Presenter in tech-casual at a multi-monitor station.\nDescribe clothing, monitor content, desk items, lighting.]\n\nSTYLE — DECONSTRUCTED (Brody): Dark grey #1a1a1a, rust orange #D4501E.\nType at angles, overlapping. Gritty textures. Smash cut transitions.\n\nCRITICAL ON-SCREEN TEXT:\n- [List every stat, quote, handle that must appear]\n\nSCENE 1 — A-ROLL (8s): Hook with energy. State what's happening.\nSCENE 2 — B-ROLL (12s): First story with layered visuals (L1-L5).\nSCENE 3 — A-ROLL + OVERLAY (10s): Second story, split frame.\nSCENE 4 — B-ROLL (10s): Third story or dramatic data point.\nSCENE 5 — A-ROLL (8s): Wrap-up and forward look.\n```\n\n### Product Comparison\n```\nFORMAT: 60-second comparison. [Product A] vs [Product B]. Data-driven.\n\nAVATAR: [Presenter in review studio. Desk with both products visible.]\n\nSTYLE — DIGITAL GRID (Crouwel): Dark #0a0a0a, cyan #00D4FF and amber #FFB800.\nTwo-color coding: cyan = Product A, amber = Product B. Monospaced type.\n\nCRITICAL ON-SCREEN TEXT:\n- [Key stats for each product]\n- [Pricing, features, differentiators]\n\nUse SPLIT FRAME B-roll: Product A left, Product B right.\n```\n\n### Strategy Presentation\n```\nFORMAT: 90-second strategy briefing. Bloomberg meets board meeting.\n\nAVATAR: [Executive in blazer over tee. Conference room with whiteboard frameworks.]\n\nSTYLE — SWISS PULSE (Müller-Brockmann): Black/white + blue #0066FF.\nGrid-locked. Helvetica. Animated counters. Grid wipe transitions.\n\nCRITICAL ON-SCREEN TEXT:\n- [Framework labels, quadrant labels, key quotes]\n\nBuild frameworks visually: draw axes, plot positions, animate labels.\n```\n\n### Social Ad (30 seconds)\n```\nFORMAT: 30-second social ad. Maximum energy. Portrait 9:16.\n\nAVATAR: [Creator-style presenter. Ring light, colorful background.]\n\nSTYLE — CARNIVAL SURGE (Lins): Hot pink, yellow, teal. Collage layering.\nText MASSIVE at angles. Confetti. Smash cuts.\n\nThree scenes: Hook (8s) → Value prop (12s) → CTA (10s).\nText fills 50-80% of every frame. Numbers SLAM.\n```\n\n### Premium Report\n```\nFORMAT: 120-second investor-grade report. Understated authority.\n\nAVATAR: [Tailored merino sweater. Architectural room, diffused natural light.]\n\nSTYLE — VELVET STANDARD (Vignelli): Black, white, gold #c9a84c.\nThin ALL CAPS, wide spacing. Generous negative space.\nSlow cross-dissolves. Numbers fade in with weight.\n```\n\nFile v2.8.0:references/prompt-optimizer.md\n\n---\nname: prompt-optimizer\ndescription: Write production-quality prompts for HeyGen Video Agent — from basic ideas to fully art-directed scene-by-scene scripts\n---\n\n# Video Agent Prompt Optimizer\n\nWrite effective prompts for the HeyGen Video Agent API. Based on patterns from 40+ produced videos.\n\n**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\").\n\n## Reference Files\n\n| File | Load when... |\n|------|-------------|\n| [visual-styles.md](visual-styles.md) | Choosing a visual style (20 styles with full specs) |\n| [prompt-examples.md](prompt-examples.md) | Writing a prompt from scratch (full production example + templates) |\n\n## Workflow: Brief to Prompt\n\n1. **Pull data** — Research the topic: web search, APIs, internal docs. Gather real quotes, stats, handles\n2. **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\n3. **Choose a style** — Match mood first, content second. Ask: *\"What should the viewer FEEL?\"* See [visual-styles.md](visual-styles.md)\n4. **Write the avatar** — Thematic wardrobe matching content's emotional context. Brand logos and content-specific props in the set (see Avatar Guide below)\n5. **Extract critical text** — List every number, quote, handle, and label that must appear literally\n6. **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\n7. **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\n8. **Layer each B-roll scene** — L1 background, L2 hero, L3 supporting, L4 info bar, L5 effects. Every element must MOVE\n9. **Add music direction** — Reference artists, describe energy arc\n10. **Add narration style** — How to deliver: fast/slow, where to pause, emotional register per section\n\n## Prompt Anatomy\n\nEvery production-quality prompt follows this structure:\n\n```\nFORMAT:    What kind of video, how long, what energy\nTONE:      Emotional register, references\nAVATAR:    Detailed physical + environment description (60-100 words)\nSTYLE:     Named aesthetic with colors, typography, motion rules, transitions\nCRITICAL ON-SCREEN TEXT:  Exact strings that must appear\nSCENE-BY-SCENE:  Individual scene breakdowns with VO and layered visuals\nMUSIC:     Genre, reference artists, energy arc\nNARRATION STYLE:  How to deliver the voiceover\n```\n\n### FORMAT\n\n```\nFORMAT: 75-second high-energy tech daily briefing. Think: a creator who just got amazing news.\nFORMAT: Bloomberg-style strategy briefing. 100-120 seconds. CEO-delivered.\n```\n\n### TONE\n\n```\nTONE: Confident, direct, data-backed. Highlights hit hard. Lowlights are honest — no spin.\nTONE: Edgy, punk tech commentary. Vice News meets The Face magazine — raw, confrontational.\n```\n\n### CRITICAL ON-SCREEN TEXT\n\nList every exact string that must appear on screen. Without this, the agent may summarize, round numbers, or rephrase quotes.\n\n```\nCRITICAL ON-SCREEN TEXT (display literally):\n- \"$141M ARR — All-Time High\"\n- \"1.85M Signups — +28% MoM\"\n- Quote: \"Use technology to serve the message, not distract from it.\" — Shalev Hani\n- \"@username\" — exact social handle\n```\n\n### MUSIC & NARRATION\n\n```\nMUSIC: Driving electronic, heavy bass drops on key numbers. Run the Jewels meets\na tech keynote. Builds relentlessly, only softens for customer stories.\n\nNARRATION STYLE: High energy throughout. Let numbers PUNCH — pause before big ones,\nthen deliver hard. Customer stories get warmth. The close should feel like a mic drop.\n```\n\n## Avatar Description Guide\n\n**The avatar is NOT a fixed headshot** — design it for each video like a movie character. Think costume designer + set designer.\n\n### Thematic Wardrobe Rule\n\nThe avatar's outfit and environment MUST match the content's emotional/cultural context:\n\n| Content Type | Avatar Design | NOT This |\n|---|---|---|\n| Chinese New Year | Red qipao with gold embroidery, lantern-lit courtyard | \"Reporter in a blazer\" |\n| Breaking tech news | Field reporter, windswept hair, earpiece, city skyline | \"Anchor at a desk\" |\n| Sleep science | Oversized cream knit, cross-legged on bed, warm lamp | \"Analyst in a lab\" |\n| Reddit community | Messy desk, Reddit alien on monitors, upvote arrows on wall | \"Researcher in a studio\" |\n\n### What to Specify\n\n| Element | Weak | Strong |\n|---------|------|--------|\n| Clothing | \"Business casual\" | \"Black ribbed merino turtleneck, high collar framing jaw\" |\n| Environment | \"An office\" | \"Glass-walled conference room. Whiteboard with hand-drawn tier pyramid\" |\n| Monitor content | \"Computer screens\" | \"Monitor shows scrolling green terminal text and red security alerts\" |\n| Lighting | \"Well lit\" | \"Cool blue monitor glow from left, warm amber desk lamp from right\" |\n\n### Template\n\n```\nAVATAR: [Clothing — fabric, color, fit, accessories, posture].\n[Setting — specific props, brand logos, what's on the walls].\n[Monitors/desk — content visible on screens, items on desk].\n[Lighting — direction, color temperature]. [Mood of the space].\n60-100 words. 3+ content-specific props. Brand elements visible.\n```\n\n## Scene Types\n\n| Type | Format | When to Use |\n|------|--------|-------------|\n| **A-ROLL** | Avatar speaking to camera | Intros, key insights, CTAs, emotional beats |\n| **FULL SCREEN B-ROLL** | No avatar — motion graphics only | Data visualization, information-dense content |\n| **A-ROLL + OVERLAY** | Split frame: avatar + content | Presenting data while maintaining human connection |\n\n**Rotation is mandatory.** Never 3+ of the same type in a row. Every prompt needs at least 2 pure B-roll scenes.\n\n**Voiceover on EVERY scene.** Every B-roll scene MUST include a `VOICEOVER:` line. Silent B-roll = broken video.\n\n### Scene Anatomy\n\n**A-ROLL:**\n```\nSCENE 1 — A-ROLL (10s)\n[Avatar center-frame, excited, hands gesturing]\nVOICEOVER: \"The exact script for this scene.\"\nLower-third: \"TITLE TEXT\" white on blue bar.\n```\n\n**B-ROLL with layers:**\n```\nSCENE 2 — FULL SCREEN B-ROLL (12s)\n[NO AVATAR — motion graphic only]\nVOICEOVER: \"The exact script for this scene.\"\nLAYER 1: Dark #1a1a1a background with subtle grid lines pulsing.\nLAYER 2: \"HEADLINE\" SLAMS in from left in white Bold 100pt at -5 degrees.\nLAYER 3: Three data cards CASCADE from right, staggered 0.3s.\nLAYER 4: Bottom ticker SLIDES in: \"supporting text scrolling continuously.\"\nLAYER 5: Grid lines RIPPLE outward from impact point.\nHard cut.\n```\n\n**A-ROLL + OVERLAY:**\n```\nSCENE 3 — A-ROLL + OVERLAY (10s)\n[SPLIT — Avatar LEFT 35%. Content RIGHT 65%. NO overlap.]\nAvatar gestures toward content side.\nVOICEOVER: \"The exact script for this scene.\"\nRIGHT SIDE: \"HEADLINE\" in cyan 60pt. Three stats COUNT UP below.\n```\n\nAlternate which side the avatar appears on between overlay scenes.\n\n## The Visual Layer System\n\nBreak B-roll into 5 stacked layers. This is the most powerful technique for motion graphics scenes.\n\n| Layer | Purpose | Examples |\n|-------|---------|---------|\n| **L1** | Background | Textured surface, grid, gradient, color field |\n| **L2** | Hero content | Main headline/number that dominates the frame |\n| **L3** | Supporting data | Cards, stats, bullet points, secondary information |\n| **L4** | Information bar | Tickers, labels, source attributions, quotes |\n| **L5** | Effects | Particles, glitches, grid animations, ambient motion |\n\nEvery B-roll: 4+ layers. Every overlay content side: 3+ layers. **Every element must MOVE.**\n\n## Motion Vocabulary\n\n### High Energy\n| Verb | Example |\n|------|---------|\n| **SLAMS** | `\"$95M\" SLAMS in from left at -5 degrees` |\n| **CRASHES** | `Title CRASHES in from right, screen-shake on impact` |\n| **PUNCHES** | `Quote card PUNCHES up from bottom` |\n| **STAMPS** | `Data blocks STAMP in staggered 0.4s` |\n| **SHATTERS** | `Text SHATTERS after 1.5s, revealing number underneath` |\n\n### Medium Energy\n| Verb | Example |\n|------|---------|\n| **CASCADE** | `Three cards CASCADE from top, staggered 0.3s` |\n| **SLIDES** | `Ticker SLIDES in from right — continuous scroll` |\n| **DROPS** | `\"TIER 1\" DROPS in with white flash` |\n| **FILLS** | `Progress bar FILLS 0 to 90% in orange` |\n| **DRAWS** | `Chart line DRAWS itself left to right` |\n\n### Low Energy\n| Verb | Example |\n|------|---------|\n| **types on** | `Quote types on word by word in italic white` |\n| **fades in** | `Logo fades in at center, held for 3 seconds` |\n| **FLOATS** | `Bokeh orbs FLOAT across frame at different speeds` |\n| **morphs** | `Number morphs from 17 to 18.9` |\n| **COUNTS UP** | `\"1.85M\" COUNTS UP from 0 in amber 96pt` |\n\n## Transition Types\n\n| Transition | Energy | Styles It Fits |\n|------------|--------|---------------|\n| Smash cut | Aggressive | Deconstructed, Maximalist, Carnival Surge |\n| White flash frame | Punchy | Deconstructed, Maximalist |\n| Grid wipe | Systematic | Swiss Pulse, Digital Grid |\n| Hard cut | Clean | Swiss Pulse, Shadow Cut |\n| Liquid dissolve | Elegant | Data Drift, Dream State |\n| Slow cross-dissolve | Refined | Velvet Standard |\n| Pop cut / bounce | Fun | Play Mode, Carnival Surge |\n| Snap cut | Urgent | Red Wire, Contact Sheet |\n| Soft dissolve | Warm | Soft Signal, Warm Grain, Quiet Drama |\n| Iris wipe | Nostalgic | Heritage Reel |\n\n## Timing Guidelines\n\n| Content Type | Duration |\n|--------------|----------|\n| Hook/Intro (A-roll) | 6-10 seconds |\n| Data-heavy B-roll | 10-15 seconds (NEVER ≤5s — causes black frames) |\n| A-roll + Overlay | 8-12 seconds |\n| CTA / Close (A-roll) | 6-8 seconds |\n\n**Common video lengths:** Social clip: 30-45s (5-7 scenes) | Briefing: 60-75s (7-9 scenes) | Deep dive: 90-120s (10-13 scenes)\n\n**Speaking pace:** ~150 words/minute. Calculate: `words / 150 * 60 = seconds`\n\n## What Doesn't Work\n\nPatterns that consistently produce poor results:\n\n**Layout language** — Screen coordinates cause empty/black B-roll:\n```\n❌ \"UPPER-LEFT: headline in 48pt Helvetica\"\n❌ \"CENTER-SCREEN: display at coordinates (400, 300)\"\n✅ \"135K\" SLAMS in from left, white Impact 120pt, fills 40% of frame.\n```\n\n**Named artists without specs** — \"Ikko Tanaka style\" means nothing to Video Agent. Translate to concrete rules:\n```\n❌ \"Use an Ikko Tanaka style\"\n✅ \"Flat color blocks, maximum 3 colors per frame, 60% negative space, typography as primary element\"\n```\n\n**Style examples injected into prompts** — Full example scenes from a style library confuse the agent. Use the style's **rules**, not example scenes.\n\n**Forced short B-roll (≤5 seconds)** — Too short for rendering. Every tested video with 5s B-roll had empty/black screens. Use 10-15s.\n\n**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.\"*\n\n## Production Insights\n\n### Style Performance (from 40+ videos)\n\n| Rank | Style | Strength |\n|------|-------|----------|\n| 1 | Deconstructed (Brody) | Most reliable across all topics |\n| 2 | Swiss Pulse (Müller-Brockmann) | Best for data-heavy content |\n| 3 | Digital Grid (Crouwel) | Strong for tech topics |\n| 4 | Geometric Bold (Tanaka) | Elegant and versatile |\n| 5 | Maximalist Type (Scher) | High energy, use sparingly |\n\n### Duration by Approach\n\n| Approach | Avg Duration | Quality |\n|----------|-------------|---------|\n| Natural storyboard + custom avatar | ~106s | Best |\n| Natural storyboard, no custom avatar | ~69s | Good |\n| Forced short scenes + custom avatar | ~71s | Mixed |\n| Layout language prompts | ~48s | Poor |\n\n## Quality Checklist\n\n- [ ] Thesis-driven — story, not bullet points\n- [ ] Style named with colors, typography, motion, transitions (see [visual-styles.md](visual-styles.md))\n- [ ] Avatar has thematic wardrobe + branded environment (60-100 words)\n- [ ] Critical text listed — every stat, quote, label\n- [ ] Scenes rotate types — never 3+ same type. At least 2 B-roll scenes\n- [ ] Every scene has VOICEOVER — including B-roll\n- [ ] B-roll scenes have 4+ layers, every element has motion verbs\n- [ ] B-roll scenes are 10-15 seconds (never ≤5s)\n- [ ] Brand logos appear when discussing companies\n- [ ] Every element moves — no static frames\n\nFile v2.8.0:references/quota.md\n\n---\nname: quota\ndescription: Credit system, usage limits, and checking remaining quota for HeyGen\n---\n\n# HeyGen Quota and Credits\n\nHeyGen uses a credit-based system for video generation. Understanding quota management helps prevent failed video generation requests.\n\n## Checking Remaining Quota\n\n### curl\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/user/remaining_quota\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n### TypeScript\n\n```typescript\ninterface QuotaResponse {\n  error: null | string;\n  data: {\n    remaining_quota: number;\n    used_quota: number;\n  };\n}\n\nconst response = await fetch(\"https://api.heygen.com/v2/user/remaining_quota\", {\n  headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n});\n\nconst { data }: QuotaResponse = await response.json();\nconsole.log(`Remaining credits: ${data.remaining_quota}`);\n```\n\n### Python\n\n```python\nimport requests\nimport os\n\nresponse = requests.get(\n    \"https://api.heygen.com/v2/user/remaining_quota\",\n    headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n)\n\ndata = response.json()[\"data\"]\nprint(f\"Remaining credits: {data['remaining_quota']}\")\n```\n\n## Response Format\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"remaining_quota\": 450,\n    \"used_quota\": 50\n  }\n}\n```\n\n## Credit Consumption\n\nDifferent operations consume different amounts of credits:\n\n| Operation | Credit Cost | Notes |\n|-----------|-------------|-------|\n| Standard video (1 min) | ~1 credit per minute | Varies by resolution |\n| 720p video | Base rate | Standard quality |\n| 1080p video | ~1.5x base rate | Higher quality |\n| Video translation | Varies | Depends on video length |\n| Streaming avatar | Per session | Real-time usage |\n\n## Pre-Generation Quota Check\n\nAlways verify sufficient quota before generating videos:\n\n```typescript\nasync function generateVideoWithQuotaCheck(videoConfig: VideoConfig) {\n  // Check quota first\n  const quotaResponse = await fetch(\n    \"https://api.heygen.com/v2/user/remaining_quota\",\n    { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n  );\n\n  const { data: quota } = await quotaResponse.json();\n\n  // Estimate required credits (rough estimate: 1 credit per minute)\n  const estimatedMinutes = videoConfig.estimatedDuration / 60;\n  const requiredCredits = Math.ceil(estimatedMinutes);\n\n  if (quota.remaining_quota < requiredCredits) {\n    throw new Error(\n      `Insufficient credits. Need ${requiredCredits}, have ${quota.remaining_quota}`\n    );\n  }\n\n  // Proceed with video generation\n  return generateVideo(videoConfig);\n}\n```\n\n## Quota Management Best Practices\n\n### 1. Monitor Usage Regularly\n\n```typescript\nasync function logQuotaUsage() {\n  const response = await fetch(\n    \"https://api.heygen.com/v2/user/remaining_quota\",\n    { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n  );\n\n  const { data } = await response.json();\n\n  console.log({\n    remaining: data.remaining_quota,\n    used: data.used_quota,\n    percentUsed: (\n      (data.used_quota / (data.remaining_quota + data.used_quota)) *\n      100\n    ).toFixed(1),\n  });\n}\n```\n\n### 2. Set Up Alerts\n\n```typescript\nconst QUOTA_WARNING_THRESHOLD = 50;\n\nasync function checkQuotaWithAlert() {\n  const response = await fetch(\n    \"https://api.heygen.com/v2/user/remaining_quota\",\n    { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n  );\n\n  const { data } = await response.json();\n\n  if (data.remaining_quota < QUOTA_WARNING_THRESHOLD) {\n    // Send alert (email, Slack, etc.)\n    await sendAlert(`Low HeyGen quota: ${data.remaining_quota} credits remaining`);\n  }\n\n  return data;\n}\n```\n\n### 3. Use Test Mode for Development\n\nWhen available, use test mode to avoid consuming credits during development:\n\n```typescript\nconst videoConfig = {\n  test: true, // Use test mode during development\n  video_inputs: [...],\n};\n\n// Test videos may have watermarks but don't consume credits\n```\n\n## Subscription Tiers\n\nDifferent subscription tiers have different quota allocations and features:\n\n| Tier | Features |\n|------|----------|\n| Free | Limited credits, basic features |\n| Creator | More credits, standard avatars |\n| Team | Higher limits, team collaboration |\n| Enterprise | Custom limits, API access, priority support |\n\nAPI access typically requires Enterprise tier or higher.\n\n## Error Handling for Quota Issues\n\n```typescript\nasync function handleQuotaError(error: any) {\n  if (error.message.includes(\"quota\") || error.message.includes(\"credit\")) {\n    console.error(\"Quota exceeded. Consider:\");\n    console.error(\"1. Upgrading your subscription\");\n    console.error(\"2. Waiting for quota reset\");\n    console.error(\"3. Purchasing additional credits\");\n\n    // Check current quota\n    const quota = await getQuota();\n    console.error(`Current remaining: ${quota.remaining_quota}`);\n  }\n\n  throw error;\n}\n```\n\nArchive v2.6.0: 22 files, 86738 bytes\n\nFiles: references/assets.md (8484b), references/authentication.md (4770b), references/avatars.md (16134b), references/backgrounds.md (6696b), references/captions.md (5631b), references/dimensions.md (6975b), references/photo-avatars.md (15355b), references/prompt-optimizer.md (40889b), references/quota.md (4765b), references/remotion-integration.md (18550b), references/scripts.md (10143b), references/templates.md (9990b), references/text-overlays.md (6964b), references/text-to-speech.md (8693b), references/video-agent.md (9037b), references/video-generation.md (22105b), references/video-status.md (12892b), references/video-translation.md (11118b), references/voices.md (11892b), references/webhooks.md (9302b), SKILL.md (4302b), _meta.json (130b)\n\nFile v2.6.0:SKILL.md\n\n---\nname: heygen\ndescription: |\n  HeyGen AI video creation API. Use when: (1) Using Video Agent for one-shot prompt-to-video generation, (2) Generating AI avatar videos with /v2/video/generate, (3) Working with HeyGen avatars, voices, backgrounds, or captions, (4) Creating transparent WebM videos for compositing, (5) Polling video status or handling webhooks, (6) Integrating HeyGen with Remotion for programmatic video, (7) Translating or dubbing existing videos, (8) Generating standalone TTS audio with the Starfish model via /v1/audio.\nhomepage: https://docs.heygen.com/reference/generate-video-agent\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - HEYGEN_API_KEY\n    primaryEnv: HEYGEN_API_KEY\n---\n\n# HeyGen API\n\nAI avatar video creation API for generating talking-head videos, explainers, and presentations.\n\n## Default Workflow\n\n**Prefer Video Agent API** (`POST /v1/video_agent/generate`) for most video requests.\nAlways use [prompt-optimizer.md](references/prompt-optimizer.md) guidelines to structure prompts with scenes, timing, and visual styles.\n\nOnly use v2/video/generate when user explicitly needs:\n- Exact script without AI modification\n- Specific voice_id selection\n- Different avatars/backgrounds per scene\n- Precise per-scene timing control\n- Programmatic/batch generation with exact specs\n\n## Quick Reference\n\n| Task | Read |\n|------|------|\n| Generate video from prompt (easy) | [prompt-optimizer.md](references/prompt-optimizer.md) → [video-agent.md](references/video-agent.md) |\n| Generate video with precise control | [video-generation.md](references/video-generation.md), [avatars.md](references/avatars.md), [voices.md](references/voices.md) |\n| Check video status / get download URL | [video-status.md](references/video-status.md) |\n| Add captions or text overlays | [captions.md](references/captions.md), [text-overlays.md](references/text-overlays.md) |\n| Transparent video for compositing | [video-generation.md](references/video-generation.md) (WebM section) |\n| Generate standalone TTS audio | [text-to-speech.md](references/text-to-speech.md) |\n| Translate/dub existing video | [video-translation.md](references/video-translation.md) |\n| Use with Remotion | [remotion-integration.md](references/remotion-integration.md) |\n\n## Reference Files\n\n### Foundation\n- [references/authentication.md](references/authentication.md) - API key setup and X-Api-Key header\n- [references/quota.md](references/quota.md) - Credit system and usage limits\n- [references/video-status.md](references/video-status.md) - Polling patterns and download URLs\n- [references/assets.md](references/assets.md) - Uploading images, videos, audio\n\n### Core Video Creation\n- [references/avatars.md](references/avatars.md) - Listing avatars, styles, avatar_id selection\n- [references/voices.md](references/voices.md) - Listing voices, locales, speed/pitch\n- [references/scripts.md](references/scripts.md) - Writing scripts, pauses, pacing\n- [references/video-generation.md](references/video-generation.md) - POST /v2/video/generate and multi-scene videos\n- [references/video-agent.md](references/video-agent.md) - One-shot prompt video generation\n- [references/prompt-optimizer.md](references/prompt-optimizer.md) - Writing effective Video Agent prompts\n- [references/dimensions.md](references/dimensions.md) - Resolution and aspect ratios\n\n### Video Customization\n- [references/backgrounds.md](references/backgrounds.md) - Solid colors, images, video backgrounds\n- [references/text-overlays.md](references/text-overlays.md) - Adding text with fonts and positioning\n- [references/captions.md](references/captions.md) - Auto-generated captions and subtitles\n\n### Advanced Features\n- [references/templates.md](references/templates.md) - Template listing and variable replacement\n- [references/video-translation.md](references/video-translation.md) - Translating videos and dubbing\n- [references/text-to-speech.md](references/text-to-speech.md) - Standalone TTS audio with Starfish model\n- [references/photo-avatars.md](references/photo-avatars.md) - Creating avatars from photos\n- [references/webhooks.md](references/webhooks.md) - Webhook endpoints and events\n\n### Integration\n- [references/remotion-integration.md](references/remotion-integration.md) - Using HeyGen in Remotion compositions\n\nFile v2.6.0:_meta.json\n\n{\n  \"ownerId\": \"kn7dnc0jepdz3jy0rg589kcxns80dmr5\",\n  \"slug\": \"video-agent\",\n  \"version\": \"2.6.0\",\n  \"publishedAt\": 1771797680257\n}\n\nFile v2.6.0:references/assets.md\n\n---\nname: assets\ndescription: Uploading images, videos, and audio for use in HeyGen video generation\n---\n\n# Asset Upload and Management\n\nHeyGen allows you to upload custom assets (images, videos, audio) for use in video generation, such as backgrounds, talking photo sources, and custom audio.\n\n## Upload Flow\n\nAsset 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.\n\n## Uploading an Asset\n\n**Endpoint:** `POST https://upload.heygen.com/v1/asset`\n\n### Request\n\n| Header | Required | Description |\n|--------|:--------:|-------------|\n| `X-Api-Key` | ✓ | Your HeyGen API key |\n| `Content-Type` | ✓ | MIME type of the file (e.g. `image/jpeg`) |\n\nThe request body is the raw binary file data. No JSON or form fields are needed.\n\n### Response\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `code` | number | Status code (`100` = success) |\n| `data.id` | string | Unique asset ID for use in video generation |\n| `data.name` | string | Asset name |\n| `data.file_type` | string | `image`, `video`, or `audio` |\n| `data.url` | string | Accessible URL for the uploaded file |\n| `data.image_key` | string \\| null | Key for creating uploaded photo avatars (images only) |\n| `data.folder_id` | string | Folder ID (empty if not in a folder) |\n| `data.meta` | string \\| null | Asset metadata |\n| `data.created_ts` | number | Unix timestamp of creation |\n\n### curl\n\n```bash\ncurl -X POST \"https://upload.heygen.com/v1/asset\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: image/jpeg\" \\\n  --data-binary '@./background.jpg'\n```\n\n### TypeScript\n\n```typescript\nimport fs from \"fs\";\n\ninterface AssetUploadResponse {\n  code: number;\n  data: {\n    id: string;\n    name: string;\n    file_type: string;\n    url: string;\n    image_key: string | null;\n    folder_id: string;\n    meta: string | null;\n    created_ts: number;\n  };\n  msg: string | null;\n  message: string | null;\n}\n\nasync function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  const fileBuffer = fs.readFileSync(filePath);\n\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n    },\n    body: fileBuffer,\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}\n\n// Usage\nconst asset = await uploadAsset(\"./background.jpg\", \"image/jpeg\");\nconsole.log(`Uploaded asset: ${asset.id}`);\nconsole.log(`Asset URL: ${asset.url}`);\n```\n\n### TypeScript (with streams for large files)\n\n```typescript\nimport fs from \"fs\";\nimport { stat } from \"fs/promises\";\n\nasync function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  const fileStats = await stat(filePath);\n  const fileStream = fs.createReadStream(filePath);\n\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n      \"Content-Length\": fileStats.size.toString(),\n    },\n    body: fileStream as any,\n    // @ts-ignore - duplex is needed for streaming\n    duplex: \"half\",\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}\n```\n\n### Python\n\n```python\nimport requests\nimport os\n\ndef upload_asset(file_path: str, content_type: str) -> dict:\n    with open(file_path, \"rb\") as f:\n        response = requests.post(\n            \"https://upload.heygen.com/v1/asset\",\n            headers={\n                \"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"],\n                \"Content-Type\": content_type\n            },\n            data=f\n        )\n\n    data = response.json()\n    if data.get(\"code\") != 100:\n        raise Exception(data.get(\"message\", \"Upload failed\"))\n\n    return data[\"data\"]\n\n\n# Usage\nasset = upload_asset(\"./background.jpg\", \"image/jpeg\")\nprint(f\"Uploaded asset: {asset['id']}\")\nprint(f\"Asset URL: {asset['url']}\")\n```\n\n## Supported Content Types\n\n| Type | Content-Type | Use Case |\n|------|--------------|----------|\n| JPEG | `image/jpeg` | Backgrounds, talking photos |\n| PNG | `image/png` | Backgrounds, overlays |\n| MP4 | `video/mp4` | Video backgrounds |\n| WebM | `video/webm` | Video backgrounds |\n| MP3 | `audio/mpeg` | Custom audio input |\n| WAV | `audio/wav` | Custom audio input |\n\n## Uploading from URL\n\nIf your asset is already hosted online:\n\n```typescript\nasync function uploadFromUrl(sourceUrl: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  // 1. Download the file\n  const sourceResponse = await fetch(sourceUrl);\n  const buffer = Buffer.from(await sourceResponse.arrayBuffer());\n\n  // 2. Upload directly to HeyGen\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n    },\n    body: buffer,\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}\n```\n\n## Using Uploaded Assets\n\n### As Background Image\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello, this is a video with a custom background!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"image\",\n        url: asset.url,  // Use the URL from the upload response\n      },\n    },\n  ],\n};\n```\n\n### As Talking Photo Source\n\n```typescript\nconst talkingPhotoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"talking_photo\",\n        talking_photo_id: asset.id,  // Use the ID from the upload response\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello from my talking photo!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n  ],\n};\n```\n\n### As Audio Input\n\n```typescript\nconst audioConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"audio\",\n        audio_url: asset.url,  // Use the URL from the upload response\n      },\n    },\n  ],\n};\n```\n\n## Complete Upload Workflow\n\n```typescript\nasync function createVideoWithCustomBackground(\n  backgroundPath: string,\n  script: string\n): Promise<string> {\n  // 1. Upload background\n  console.log(\"Uploading background...\");\n  const background = await uploadAsset(backgroundPath, \"image/jpeg\");\n\n  // 2. Create video config\n  const config = {\n    video_inputs: [\n      {\n        character: {\n          type: \"avatar\",\n          avatar_id: \"josh_lite3_20230714\",\n          avatar_style: \"normal\",\n        },\n        voice: {\n          type: \"text\",\n          input_text: script,\n          voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n        },\n        background: {\n          type: \"image\",\n          url: background.url,\n        },\n      },\n    ],\n    dimension: { width: 1920, height: 1080 },\n  };\n\n  // 3. Generate video\n  console.log(\"Generating video...\");\n  const response = await fetch(\"https://api.heygen.com/v2/video/generate\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": \"application/json\",\n    },\n    body: JSON.stringify(config),\n  });\n\n  const { data } = await response.json();\n  return data.video_id;\n}\n```\n\n## Asset Limitations\n\n- **File size**: 10MB maximum\n- **Image dimensions**: Recommended to match video dimensions\n- **Audio duration**: Should match expected video length\n- **Retention**: Assets may be deleted after a period of inactivity\n\n## Best Practices\n\n1. **Optimize images** - Resize to match video dimensions before uploading\n2. **Use appropriate formats** - JPEG for photos, PNG for graphics with transparency\n3. **Validate before upload** - Check file type and size locally first\n4. **Handle upload errors** - Implement retry logic for failed uploads\n5. **Cache asset IDs** - Reuse assets across multiple video generations\n\nFile v2.6.0:references/authentication.md\n\n---\nname: authentication\ndescription: API key setup, X-Api-Key header, and authentication patterns for HeyGen\n---\n\n# HeyGen Authentication\n\nAll HeyGen API requests require authentication using an API key passed in the `X-Api-Key` header.\n\n## Getting Your API Key\n\n1. Go to https://app.heygen.com/settings?from=&nav=API\n2. Log in if prompted\n3. Copy your API key\n\n## Environment Setup\n\nStore your API key securely as an environment variable:\n\n```bash\nexport HEYGEN_API_KEY=\"your-api-key-here\"\n```\n\nFor `.env` files:\n\n```\nHEYGEN_API_KEY=your-api-key-here\n```\n\n## Making Authenticated Requests\n\n### curl\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatars\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n### TypeScript/JavaScript (fetch)\n\n```typescript\nconst response = await fetch(\"https://api.heygen.com/v2/avatars\", {\n  headers: {\n    \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n  },\n});\nconst { data } = await response.json();\n```\n\n### TypeScript/JavaScript (axios)\n\n```typescript\nimport axios from \"axios\";\n\nconst client = axios.create({\n  baseURL: \"https://api.heygen.com\",\n  headers: {\n    \"X-Api-Key\": process.env.HEYGEN_API_KEY,\n  },\n});\n\nconst { data } = await client.get(\"/v2/avatars\");\n```\n\n### Python (requests)\n\n```python\nimport os\nimport requests\n\nresponse = requests.get(\n    \"https://api.heygen.com/v2/avatars\",\n    headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n)\ndata = response.json()\n```\n\n### Python (httpx)\n\n```python\nimport os\nimport httpx\n\nasync with httpx.AsyncClient() as client:\n    response = await client.get(\n        \"https://api.heygen.com/v2/avatars\",\n        headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n    )\n    data = response.json()\n```\n\n## Creating a Reusable API Client\n\n### TypeScript\n\n```typescript\nclass HeyGenClient {\n  private baseUrl = \"https://api.heygen.com\";\n  private apiKey: string;\n\n  constructor(apiKey: string) {\n    this.apiKey = apiKey;\n  }\n\n  async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n    const response = await fetch(`${this.baseUrl}${endpoint}`, {\n      ...options,\n      headers: {\n        \"X-Api-Key\": this.apiKey,\n        \"Content-Type\": \"application/json\",\n        ...options.headers,\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || `HTTP ${response.status}`);\n    }\n\n    return response.json();\n  }\n\n  get<T>(endpoint: string): Promise<T> {\n    return this.request<T>(endpoint);\n  }\n\n  post<T>(endpoint: string, body: unknown): Promise<T> {\n    return this.request<T>(endpoint, {\n      method: \"POST\",\n      body: JSON.stringify(body),\n    });\n  }\n}\n\n// Usage\nconst client = new HeyGenClient(process.env.HEYGEN_API_KEY!);\nconst avatars = await client.get(\"/v2/avatars\");\n```\n\n## API Response Format\n\nAll HeyGen API responses follow this structure:\n\n```typescript\ninterface ApiResponse<T> {\n  error: null | string;\n  data: T;\n}\n```\n\nSuccessful response example:\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"avatars\": [...]\n  }\n}\n```\n\nError response example:\n\n```json\n{\n  \"error\": \"Invalid API key\",\n  \"data\": null\n}\n```\n\n## Error Handling\n\nCommon authentication errors:\n\n| Status Code | Error | Cause |\n|-------------|-------|-------|\n| 401 | Invalid API key | API key is missing or incorrect |\n| 403 | Forbidden | API key doesn't have required permissions |\n| 429 | Rate limit exceeded | Too many requests |\n\n### Handling Errors\n\n```typescript\nasync function makeRequest(endpoint: string) {\n  const response = await fetch(`https://api.heygen.com${endpoint}`, {\n    headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n  });\n\n  const json = await response.json();\n\n  if (!response.ok || json.error) {\n    throw new Error(json.error || `HTTP ${response.status}`);\n  }\n\n  return json.data;\n}\n```\n\n## Rate Limiting\n\nHeyGen enforces rate limits on API requests:\n- Standard rate limits apply per API key\n- Some endpoints (like video generation) have stricter limits\n- Use exponential backoff when receiving 429 errors\n\n```typescript\nasync function requestWithRetry(\n  fn: () => Promise<Response>,\n  maxRetries = 3\n): Promise<Response> {\n  for (let i = 0; i < maxRetries; i++) {\n    const response = await fn();\n\n    if (response.status === 429) {\n      const waitTime = Math.pow(2, i) * 1000;\n      await new Promise((resolve) => setTimeout(resolve, waitTime));\n      continue;\n    }\n\n    return response;\n  }\n\n  throw new Error(\"Max retries exceeded\");\n}\n```\n\n## Security Best Practices\n\n1. **Never expose API keys in client-side code** - Always make API calls from a backend server\n2. **Use environment variables** - Don't hardcode API keys in source code\n3. **Rotate keys periodically** - Generate new API keys regularly\n4. **Monitor usage** - Check your HeyGen dashboard for unusual activity\n\nFile v2.6.0:references/avatars.md\n\n---\nname: avatars\ndescription: Listing avatars, avatar styles, and avatar_id selection for HeyGen\n---\n\n# HeyGen Avatars\n\nAvatars are the AI-generated presenters in HeyGen videos. You can use public avatars provided by HeyGen or create custom avatars.\n\n## Previewing Avatars Before Generation\n\nAlways preview avatars before generating a video to ensure they match user preferences. Each avatar has preview URLs that can be opened directly in the browser - no downloading required.\n\n### Quick Preview: Open URL in Browser (Recommended)\n\nThe fastest way to preview avatars is to open the URL directly in the default browser. **Do not download the image first** - just pass the URL to `open`:\n\n```bash\n# macOS: Open URL directly in default browser (no download)\nopen \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n\n# Open preview video to see animation\nopen \"https://files.heygen.ai/avatar/preview/josh.mp4\"\n\n# Linux: Use xdg-open\nxdg-open \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n\n# Windows: Use start\nstart \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n```\n\nThe `open` command on macOS opens URLs directly in the default browser - it does not download the file. This is the quickest way to let users see avatar previews.\n\n### List Avatars and Open Previews\n\n```typescript\nasync function listAndPreviewAvatars(openInBrowser = true): Promise<void> {\n  const response = await fetch(\"https://api.heygen.com/v2/avatars\", {\n    headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n  });\n  const { data } = await response.json();\n\n  for (const avatar of data.avatars.slice(0, 5)) {\n    console.log(`\\n${avatar.avatar_name} (${avatar.gender})`);\n    console.log(`  ID: ${avatar.avatar_id}`);\n    console.log(`  Preview: ${avatar.preview_image_url}`);\n  }\n\n  // Open preview URLs directly in browser (no download needed)\n  if (openInBrowser) {\n    const { execSync } = require(\"child_process\");\n    for (const avatar of data.avatars.slice(0, 3)) {\n      // 'open' on macOS opens the URL in default browser - doesn't download\n      execSync(`open \"${avatar.preview_image_url}\"`);\n    }\n  }\n}\n```\n\n**Note:** The `open` command passes the URL to the browser - it does not download. The browser fetches and displays the image directly.\n\n### Workflow: Preview Before Generate\n\n1. **List available avatars** - get names, genders, and preview URLs\n2. **Open previews in browser** - `open <preview_image_url>` for quick visual check\n3. **User selects** preferred avatar by name or ID\n4. **Get avatar details** for `default_voice_id`\n5. **Generate video** with selected avatar\n\n```bash\n# Example workflow in terminal\n# 1. List avatars (agent shows options)\n# 2. Open preview for candidate\nopen \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n# 3. User says \"use Josh\"\n# 4. Agent gets details and generates\n```\n\n### Preview Fields in API Response\n\n| Field | Description |\n|-------|-------------|\n| `preview_image_url` | Static image of the avatar (JPG) - open in browser |\n| `preview_video_url` | Short video clip showing avatar animation |\n\nBoth URLs are publicly accessible - no authentication needed to view.\n\n## Listing Available Avatars\n\n### curl\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatars\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n### TypeScript\n\n```typescript\ninterface Avatar {\n  avatar_id: string;\n  avatar_name: string;\n  gender: \"male\" | \"female\";\n  preview_image_url: string;\n  preview_video_url: string;\n}\n\ninterface AvatarsResponse {\n  error: null | string;\n  data: {\n    avatars: Avatar[];\n    talking_photos: TalkingPhoto[];\n  };\n}\n\nasync function listAvatars(): Promise<Avatar[]> {\n  const response = await fetch(\"https://api.heygen.com/v2/avatars\", {\n    headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n  });\n\n  const json: AvatarsResponse = await response.json();\n\n  if (json.error) {\n    throw new Error(json.error);\n  }\n\n  return json.data.avatars;\n}\n```\n\n### Python\n\n```python\nimport requests\nimport os\n\ndef list_avatars() -> list:\n    response = requests.get(\n        \"https://api.heygen.com/v2/avatars\",\n        headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n    )\n\n    data = response.json()\n    if data.get(\"error\"):\n        raise Exception(data[\"error\"])\n\n    return data[\"data\"][\"avatars\"]\n```\n\n## Response Format\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"avatars\": [\n      {\n        \"avatar_id\": \"josh_lite3_20230714\",\n        \"avatar_name\": \"Josh\",\n        \"gender\": \"male\",\n        \"preview_image_url\": \"https://files.heygen.ai/...\",\n        \"preview_video_url\": \"https://files.heygen.ai/...\"\n      },\n      {\n        \"avatar_id\": \"angela_expressive_20231010\",\n        \"avatar_name\": \"Angela\",\n        \"gender\": \"female\",\n        \"preview_image_url\": \"https://files.heygen.ai/...\",\n        \"preview_video_url\": \"https://files.heygen.ai/...\"\n      }\n    ],\n    \"talking_photos\": []\n  }\n}\n```\n\n## Avatar Types\n\n### Public Avatars\n\nHeyGen provides a library of public avatars that anyone can use:\n\n```typescript\n// List only public avatars\nconst avatars = await listAvatars();\nconst publicAvatars = avatars.filter((a) => !a.avatar_id.startsWith(\"custom_\"));\n```\n\n### Private/Custom Avatars\n\nCustom avatars created from your own training footage:\n\n```typescript\nconst customAvatars = avatars.filter((a) => a.avatar_id.startsWith(\"custom_\"));\n```\n\n## Avatar Styles\n\nAvatars support different rendering styles:\n\n| Style | Description |\n|-------|-------------|\n| `normal` | Full body shot, standard framing |\n| `closeUp` | Close-up on face, more expressive |\n| `circle` | Avatar in circular frame (talking head) |\n| `voice_only` | Audio only, no video rendering |\n\n### When to Use Each Style\n\n| Use Case | Recommended Style |\n|----------|-------------------|\n| Full-screen presenter video | `normal` |\n| Personal/intimate content | `closeUp` |\n| Picture-in-picture overlay | `circle` |\n| Small corner widget | `circle` |\n| Podcast/audio content | `voice_only` |\n| Motion graphics with avatar overlay | `normal` or `closeUp` + transparent bg |\n\n### Using Avatar Styles\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\", // \"normal\" | \"closeUp\" | \"circle\" | \"voice_only\"\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello, world!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n  ],\n};\n```\n\n### Circle Style for Talking Heads\n\nCircle style is ideal for overlay compositions:\n\n```typescript\n// Circle avatar for picture-in-picture\n{\n  character: {\n    type: \"avatar\",\n    avatar_id: \"josh_lite3_20230714\",\n    avatar_style: \"circle\",\n  },\n  voice: { ... },\n  background: {\n    type: \"color\",\n    value: \"#00FF00\", // Green for chroma key, or use webm endpoint\n  },\n}\n```\n\n## Searching and Filtering Avatars\n\n### By Gender\n\n```typescript\nfunction filterByGender(avatars: Avatar[], gender: \"male\" | \"female\"): Avatar[] {\n  return avatars.filter((a) => a.gender === gender);\n}\n\nconst maleAvatars = filterByGender(avatars, \"male\");\nconst femaleAvatars = filterByGender(avatars, \"female\");\n```\n\n### By Name\n\n```typescript\nfunction searchByName(avatars: Avatar[], query: string): Avatar[] {\n  const lowerQuery = query.toLowerCase();\n  return avatars.filter((a) =>\n    a.avatar_name.toLowerCase().includes(lowerQuery)\n  );\n}\n\nconst results = searchByName(avatars, \"josh\");\n```\n\n## Avatar Groups\n\nAvatars are organized into groups for better management.\n\n### List Avatar Groups\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatar_group.list?include_public=true\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n#### Query Parameters\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `include_public` | bool | false | Include public avatars in results |\n\n#### TypeScript\n\n```typescript\ninterface AvatarGroupItem {\n  id: string;\n  name: string;\n  created_at: number;\n  num_looks: number;\n  preview_image: string;\n  group_type: string;\n  train_status: string;\n  default_voice_id: string | null;\n}\n\ninterface AvatarGroupListResponse {\n  error: null | string;\n  data: {\n    avatar_group_list: AvatarGroupItem[];\n  };\n}\n\nasync function listAvatarGroups(\n  includePublic = true\n): Promise<AvatarGroupListResponse[\"data\"]> {\n  const params = new URLSearchParams({\n    include_public: includePublic.toString(),\n  });\n\n  const response = await fetch(\n    `https://api.heygen.com/v2/avatar_group.list?${params}`,\n    { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n  );\n\n  const json: AvatarGroupListResponse = await response.json();\n\n  if (json.error) {\n    throw new Error(json.error);\n  }\n\n  return json.data;\n}\n```\n\n### Get Avatars in a Group\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatar_group/{group_id}/avatars\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n## Using Avatars in Video Generation\n\n### Basic Avatar Usage\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Welcome to our product demo!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n  ],\n  dimension: { width: 1920, height: 1080 },\n};\n```\n\n### Multiple Scenes with Different Avatars\n\n```typescript\nconst multiSceneConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hi, I'm Josh. Let me introduce my colleague.\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n    },\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"angela_expressive_20231010\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello! I'm Angela. Nice to meet you!\",\n        voice_id: \"2d5b0e6a8c3f47d9a1b2c3d4e5f60718\",\n      },\n    },\n  ],\n};\n```\n\n## Using Avatar's Default Voice\n\nMany avatars have a `default_voice_id` that's pre-matched for natural results. **This is the recommended approach** rather than manually selecting voices.\n\n### Recommended Flow\n\n```\n1. GET /v2/avatars           → Get list of avatar_ids\n2. GET /v2/avatar/{id}/details → Get default_voice_id for chosen avatar\n3. POST /v2/video/generate   → Use avatar_id + default_voice_id\n```\n\n### Get Avatar Details (v2 API)\n\nGiven an `avatar_id`, fetch its details including the default voice:\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatar/{avatar_id}/details\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n#### Response Format\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"type\": \"avatar\",\n    \"id\": \"josh_lite3_20230714\",\n    \"name\": \"Josh\",\n    \"gender\": \"male\",\n    \"preview_image_url\": \"https://files.heygen.ai/...\",\n    \"preview_video_url\": \"https://files.heygen.ai/...\",\n    \"premium\": false,\n    \"is_public\": true,\n    \"default_voice_id\": \"1bd001e7e50f421d891986aad5158bc8\",\n    \"tags\": [\"AVATAR_IV\"]\n  }\n}\n```\n\n#### TypeScript\n\n```typescript\ninterface AvatarDetails {\n  type: \"avatar\";\n  id: string;\n  name: string;\n  gender: \"male\" | \"female\";\n  preview_image_url: string;\n  preview_video_url: string;\n  premium: boolean;\n  is_public: boolean;\n  default_voice_id: string | null;\n  tags: string[];\n}\n\nasync function getAvatarDetails(avatarId: string): Promise<AvatarDetails> {\n  const response = await fetch(\n    `https://api.heygen.com/v2/avatar/${avatarId}/details`,\n    { headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! } }\n  );\n\n  const json = await response.json();\n\n  if (json.error) {\n    throw new Error(json.error);\n  }\n\n  return json.data;\n}\n\n// Usage: Get default voice for a known avatar\nconst details = await getAvatarDetails(\"josh_lite3_20230714\");\nif (details.default_voice_id) {\n  console.log(`Using ${details.name} with default voice: ${details.default_voice_id}`);\n} else {\n  console.log(`${details.name} has no default voice, select manually`);\n}\n```\n\n#### Complete Example: Generate Video with Any Avatar's Default Voice\n\n```typescript\nasync function generateWithAvatarDefaultVoice(\n  avatarId: string,\n  script: string\n): Promise<string> {\n  // 1. Get avatar details to find default voice\n  const avatar = await getAvatarDetails(avatarId);\n\n  if (!avatar.default_voice_id) {\n    throw new Error(`Avatar ${avatar.name} has no default voice`);\n  }\n\n  // 2. Generate video with the avatar's default voice\n  const videoId = await generateVideo({\n    video_inputs: [{\n      character: {\n        type: \"avatar\",\n        avatar_id: avatar.id,\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: script,\n        voice_id: avatar.default_voice_id,\n      },\n    }],\n    dimension: { width: 1920, height: 1080 },\n  });\n\n  return videoId;\n}\n```\n\n### Why Use Default Voice?\n\n1. **Guaranteed gender match** - Avatar and voice are pre-paired\n2. **Natural lip sync** - Default voices are optimized for the avatar\n3. **Simpler code** - No need to fetch and match voices separately\n4. **Better quality** - HeyGen has tested this combination\n\n## Selecting the Right Avatar\n\n### Avatar Categories\n\nHeyGen avatars fall into distinct categories. Match the category to your use case:\n\n| Category | Examples | Best For |\n|----------|----------|----------|\n| **Business/Professional** | Josh, Angela, Wayne | Corporate videos, product demos, training |\n| **Casual/Friendly** | Lily, various lifestyle avatars | Social media, informal content |\n| **Themed/Seasonal** | Holiday-themed, costume avatars | Specific campaigns, seasonal content |\n| **Expressive** | Avatars with \"expressive\" in name | Engaging storytelling, dynamic content |\n\n### Selection Guidelines\n\n**For business/professional content:**\n- Choose avatars with neutral attire (business casual or formal)\n- Avoid themed or seasonal avatars (holiday costumes, casual clothing)\n- Preview the avatar to verify professional appearance\n- Consider your audience demographics when selecting gender and appearance\n\n**For casual/social content:**\n- More flexibility in avatar choice\n- Themed avatars can work for specific campaigns\n- Match avatar energy to content tone\n\n### Common Mistakes to Avoid\n\n1. **Using themed avatars for business content** - A holiday-themed avatar looks unprofessional in a product demo\n2. **Not previewing before generation** - Always `open <preview_url>` to verify appearance\n3. **Ignoring avatar style** - A `circle` style avatar may not work for full-screen presentations\n4. **Mismatched voice gender** - Always use the avatar's `default_voice_id` or match genders manually\n\n### Selection Checklist\n\nBefore generating a video:\n- [ ] Previewed avatar image/video in browser\n- [ ] Avatar appearance matches content tone (professional vs casual)\n- [ ] Avatar style (`normal`, `closeUp`, `circle`) fits the video format\n- [ ] Voice gender matches avatar gender\n- [ ] Using `default_voice_id` when available\n\n## Helper Functions\n\n### Get Avatar by ID\n\n```typescript\nasync function getAvatarById(avatarId: string): Promise<Avatar | null> {\n  const avatars = await listAvatars();\n  return avatars.find((a) => a.avatar_id === avatarId) || null;\n}\n```\n\n### Validate Avatar ID\n\n```typescript\nasync function isValidAvatarId(avatarId: string): Promise<boolean> {\n  const avatar = await getAvatarById(avatarId);\n  return avatar !== null;\n}\n```\n\n### Get Random Avatar\n\n```typescript\nasync function getRandomAvatar(gender?: \"male\" | \"female\"): Promise<Avatar> {\n  let avatars = await listAvatars();\n\n  if (gender) {\n    avatars = avatars.filter((a) => a.gender === gender);\n  }\n\n  const randomIndex = Math.floor(Math.random() * avatars.length);\n  return avatars[randomIndex];\n}\n```\n\n## Common Avatar IDs\n\nSome commonly used public avatar IDs (availability may vary):\n\n| Avatar ID | Name | Gender |\n|-----------|------|--------|\n| `josh_lite3_20230714` | Josh | Male |\n| `angela_expressive_20231010` | Angela | Female |\n| `wayne_20240422` | Wayne | Male |\n| `lily_20230614` | Lily | Female |\n\nAlways verify avatar availability by calling the list endpoint before using.\n\nFile v2.6.0:references/backgrounds.md\n\n---\nname: backgrounds\ndescription: Solid colors, images, and video backgrounds for HeyGen videos\n---\n\n# Video Backgrounds\n\nHeyGen supports various background types to customize the appearance of your avatar videos.\n\n## Background Types\n\n| Type | Description |\n|------|-------------|\n| `color` | Solid color background |\n| `image` | Static image background |\n| `video` | Looping video background |\n\n## Color Backgrounds\n\nThe simplest option - use a solid color:\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Hello with a colored background!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"color\",\n        value: \"#FFFFFF\", // White background\n      },\n    },\n  ],\n};\n```\n\n### Common Color Values\n\n| Color | Hex Value | Use Case |\n|-------|-----------|----------|\n| White | `#FFFFFF` | Clean, professional |\n| Black | `#000000` | Dramatic, cinematic |\n| Blue | `#0066CC` | Corporate, trustworthy |\n| Green | `#00FF00` | Chroma key (for compositing) |\n| Gray | `#808080` | Neutral, modern |\n\n### Using Transparent/Green Screen\n\nFor compositing in post-production:\n\n```typescript\nbackground: {\n  type: \"color\",\n  value: \"#00FF00\", // Green screen\n}\n```\n\n## Image Backgrounds\n\nUse a static image as background:\n\n### From URL\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Check out this custom background!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"image\",\n        url: \"https://example.com/my-background.jpg\",\n      },\n    },\n  ],\n};\n```\n\n### From Uploaded Asset\n\nFirst upload your image, then use the asset URL:\n\n```typescript\n// 1. Upload the image\nconst assetId = await uploadFile(\"./background.jpg\", \"image/jpeg\");\n\n// 2. Use in video config\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {...},\n      voice: {...},\n      background: {\n        type: \"image\",\n        url: `https://files.heygen.ai/asset/${assetId}`,\n      },\n    },\n  ],\n};\n```\n\n### Image Requirements\n\n- **Formats**: JPEG, PNG\n- **Recommended size**: Match video dimensions (e.g., 1920x1080 for 1080p)\n- **Aspect ratio**: Should match video aspect ratio\n- **File size**: Under 10MB recommended\n\n## Video Backgrounds\n\nUse a looping video as background:\n\n```typescript\nconst videoConfig = {\n  video_inputs: [\n    {\n      character: {\n        type: \"avatar\",\n        avatar_id: \"josh_lite3_20230714\",\n        avatar_style: \"normal\",\n      },\n      voice: {\n        type: \"text\",\n        input_text: \"Dynamic video background!\",\n        voice_id: \"1bd001e7e50f421d891986aad5158bc8\",\n      },\n      background: {\n        type: \"video\",\n        url: \"https://example.com/background-loop.mp4\",\n      },\n    },\n  ],\n};\n```\n\n### Video Requirements\n\n- **Format**: MP4 (H.264 codec recommended)\n- **Looping**: Video will loop if shorter than avatar content\n- **Audio**: Background video audio is typically muted\n- **File size**: Under 100MB recommended\n\n## Different Backgrounds Per Scene\n\nUse different backgrounds for each scene:\n\n```typescript\nc","readmeExcerpt":"Skill: Video Agent Owner: michaelwang11394 Summary: HeyGen AI video creation API. Use when: (1) Using Video Agent for one-shot prompt-to-video generation, (2) Generating AI avatar videos with /v2/video/generat... Tags: ai-avatar:2.8.0, ai-video:2.8.0, avatar:2.8.0, digital-human:2.8.0, heygen:2.8.0, latest:2.8.0, talking-head:2.8.0, text-to-video:2.8.0, video:2.8.0, video-generation:2.8.0 Version history: v2.8.0 | 20","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"curl -X POST \"https://upload.heygen.com/v1/asset\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: image/jpeg\" \\\n  --data-binary '@./background.jpg'"},{"language":"bash","snippet":"curl -X POST \"https://upload.heygen.com/v1/asset\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: image/jpeg\" \\\n  --data-binary '@./background.jpg'"},{"language":"typescript","snippet":"import fs from \"fs\";\n\ninterface AssetUploadResponse {\n  code: number;\n  data: {\n    id: string;\n    name: string;\n    file_type: string;\n    url: string;\n    image_key: string | null;\n    folder_id: string;\n    meta: string | null;\n    created_ts: number;\n  };\n  msg: string | null;\n  message: string | null;\n}\n\nasync function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  const fileBuffer = fs.readFileSync(filePath);\n\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n    },\n    body: fileBuffer,\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}\n\n// Usage\nconst asset = await uploadAsset(\"./background.jpg\", \"image/jpeg\");\nconsole.log(`Uploaded asset: ${asset.id}`);\nconsole.log(`Asset URL: ${asset.url}`);"},{"language":"typescript","snippet":"import fs from \"fs\";\nimport { stat } from \"fs/promises\";\n\nasync function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  const fileStats = await stat(filePath);\n  const fileStream = fs.createReadStream(filePath);\n\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n      \"Content-Length\": fileStats.size.toString(),\n    },\n    body: fileStream as any,\n    // @ts-ignore - duplex is needed for streaming\n    duplex: \"half\",\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}"},{"language":"python","snippet":"import requests\nimport os\n\ndef upload_asset(file_path: str, content_type: str) -> dict:\n    with open(file_path, \"rb\") as f:\n        response = requests.post(\n            \"https://upload.heygen.com/v1/asset\",\n            headers={\n                \"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"],\n                \"Content-Type\": content_type\n            },\n            data=f\n        )\n\n    data = response.json()\n    if data.get(\"code\") != 100:\n        raise Exception(data.get(\"message\", \"Upload failed\"))\n\n    return data[\"data\"]\n\n\n# Usage\nasset = upload_asset(\"./background.jpg\", \"image/jpeg\")\nprint(f\"Uploaded asset: {asset['id']}\")\nprint(f\"Asset URL: {asset['url']}\")"},{"language":"typescript","snippet":"async function uploadFromUrl(sourceUrl: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  // 1. Download the file\n  const sourceResponse = await fetch(sourceUrl);\n  const buffer = Buffer.from(await sourceResponse.arrayBuffer());\n\n  // 2. Upload directly to HeyGen\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n    },\n    body: buffer,\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: heygen\ndescription: |\n  HeyGen AI video creation API. Use when: (1) Using Video Agent for one-shot prompt-to-video generation, (2) Generating AI avatar videos with /v2/video/generate, (3) Working with HeyGen avatars, voices, backgrounds, or captions, (4) Creating transparent WebM videos for compositing, (5) Polling video status or handling webhooks, (6) Integrating HeyGen with Remotion for programmatic video, (7) Translating or dubbing existing videos, (8) Generating standalone TTS audio with the Starfish model via /v1/audio.\nhomepage: https://docs.heygen.com/reference/generate-video-agent\nmetadata:\n  openclaw:\n    requires:\n      env:\n        - HEYGEN_API_KEY\n    primaryEnv: HEYGEN_API_KEY\n---\n\n# HeyGen API\n\nAI avatar video creation API for generating talking-head videos, explainers, and presentations.\n\n## Default Workflow\n\n**Prefer Video Agent API** (`POST /v1/video_agent/generate`) for most video requests.\nAlways use [prompt-optimizer.md](references/prompt-optimizer.md) guidelines to structure prompts with scenes, timing, and visual styles.\n\nOnly use v2/video/generate when user explicitly needs:\n- Exact script without AI modification\n- Specific voice_id selection\n- Different avatars/backgrounds per scene\n- Precise per-scene timing control\n- Programmatic/batch generation with exact specs\n\n## Quick Reference\n\n| Task | Read |\n|------|------|\n| Generate video from prompt (easy) | [prompt-optimizer.md](references/prompt-optimizer.md) → [visual-styles.md](references/visual-styles.md) → [video-agent.md](references/video-agent.md) |\n| Generate video with precise control | [video-generation.md](references/video-generation.md), [avatars.md](references/avatars.md), [voices.md](references/voices.md) |\n| Check video status / get download URL | [video-status.md](references/video-status.md) |\n| Add captions or text overlays | [captions.md](references/captions.md), [text-overlays.md](references/text-overlays.md) |\n| Transparent video for compositing | [video-generation.md](references/video-generation.md) (WebM section) |\n| Generate standalone TTS audio | [text-to-speech.md](references/text-to-speech.md) |\n| Translate/dub existing video | [video-translation.md](references/video-translation.md) |\n| Use with Remotion | [remotion-integration.md](references/remotion-integration.md) |\n\n## Reference Files\n\n### Foundation\n- [references/authentication.md](references/authentication.md) - API key setup and X-Api-Key header\n- [references/quota.md](references/quota.md) - Credit system and usage limits\n- [references/video-status.md](references/video-status.md) - Polling patterns and download URLs\n- [references/assets.md](references/assets.md) - Uploading images, videos, audio\n\n### Core Video Creation\n- [references/avatars.md](references/avatars.md) - Listing avatars, styles, avatar_id selection\n- [references/voices.md](references/voices.md) - Listing voices, locales, speed/pitch\n- [references/scripts.md](references/scripts.md) - Writing scripts, pauses, pacing\n- "},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7dnc0jepdz3jy0rg589kcxns80dmr5\",\n  \"slug\": \"video-agent\",\n  \"version\": \"2.8.0\",\n  \"publishedAt\": 1771867395703\n}"},{"path":"references/assets.md","content":"---\nname: assets\ndescription: Uploading images, videos, and audio for use in HeyGen video generation\n---\n\n# Asset Upload and Management\n\nHeyGen allows you to upload custom assets (images, videos, audio) for use in video generation, such as backgrounds, talking photo sources, and custom audio.\n\n## Upload Flow\n\nAsset 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.\n\n## Uploading an Asset\n\n**Endpoint:** `POST https://upload.heygen.com/v1/asset`\n\n### Request\n\n| Header | Required | Description |\n|--------|:--------:|-------------|\n| `X-Api-Key` | ✓ | Your HeyGen API key |\n| `Content-Type` | ✓ | MIME type of the file (e.g. `image/jpeg`) |\n\nThe request body is the raw binary file data. No JSON or form fields are needed.\n\n### Response\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `code` | number | Status code (`100` = success) |\n| `data.id` | string | Unique asset ID for use in video generation |\n| `data.name` | string | Asset name |\n| `data.file_type` | string | `image`, `video`, or `audio` |\n| `data.url` | string | Accessible URL for the uploaded file |\n| `data.image_key` | string \\| null | Key for creating uploaded photo avatars (images only) |\n| `data.folder_id` | string | Folder ID (empty if not in a folder) |\n| `data.meta` | string \\| null | Asset metadata |\n| `data.created_ts` | number | Unix timestamp of creation |\n\n### curl\n\n```bash\ncurl -X POST \"https://upload.heygen.com/v1/asset\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\" \\\n  -H \"Content-Type: image/jpeg\" \\\n  --data-binary '@./background.jpg'\n```\n\n### TypeScript\n\n```typescript\nimport fs from \"fs\";\n\ninterface AssetUploadResponse {\n  code: number;\n  data: {\n    id: string;\n    name: string;\n    file_type: string;\n    url: string;\n    image_key: string | null;\n    folder_id: string;\n    meta: string | null;\n    created_ts: number;\n  };\n  msg: string | null;\n  message: string | null;\n}\n\nasync function uploadAsset(filePath: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  const fileBuffer = fs.readFileSync(filePath);\n\n  const response = await fetch(\"https://upload.heygen.com/v1/asset\", {\n    method: \"POST\",\n    headers: {\n      \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n      \"Content-Type\": contentType,\n    },\n    body: fileBuffer,\n  });\n\n  const json: AssetUploadResponse = await response.json();\n\n  if (json.code !== 100) {\n    throw new Error(json.message ?? \"Upload failed\");\n  }\n\n  return json.data;\n}\n\n// Usage\nconst asset = await uploadAsset(\"./background.jpg\", \"image/jpeg\");\nconsole.log(`Uploaded asset: ${asset.id}`);\nconsole.log(`Asset URL: ${asset.url}`);\n```\n\n### TypeScript (with streams for large files)\n\n```typescript\nimport fs from \"fs\";\nimport { stat } from \"fs/promises\";\n\nasync function uploadLargeAsset(filePath: string, contentType: string): Promise<AssetUploadResponse[\"data\"]> {\n  const fileStats = await stat(filePath);\n  const fileStream = fs.createRea"},{"path":"references/authentication.md","content":"---\nname: authentication\ndescription: API key setup, X-Api-Key header, and authentication patterns for HeyGen\n---\n\n# HeyGen Authentication\n\nAll HeyGen API requests require authentication using an API key passed in the `X-Api-Key` header.\n\n## Getting Your API Key\n\n1. Go to https://app.heygen.com/settings?from=&nav=API\n2. Log in if prompted\n3. Copy your API key\n\n## Environment Setup\n\nStore your API key securely as an environment variable:\n\n```bash\nexport HEYGEN_API_KEY=\"your-api-key-here\"\n```\n\nFor `.env` files:\n\n```\nHEYGEN_API_KEY=your-api-key-here\n```\n\n## Making Authenticated Requests\n\n### curl\n\n```bash\ncurl -X GET \"https://api.heygen.com/v2/avatars\" \\\n  -H \"X-Api-Key: $HEYGEN_API_KEY\"\n```\n\n### TypeScript/JavaScript (fetch)\n\n```typescript\nconst response = await fetch(\"https://api.heygen.com/v2/avatars\", {\n  headers: {\n    \"X-Api-Key\": process.env.HEYGEN_API_KEY!,\n  },\n});\nconst { data } = await response.json();\n```\n\n### TypeScript/JavaScript (axios)\n\n```typescript\nimport axios from \"axios\";\n\nconst client = axios.create({\n  baseURL: \"https://api.heygen.com\",\n  headers: {\n    \"X-Api-Key\": process.env.HEYGEN_API_KEY,\n  },\n});\n\nconst { data } = await client.get(\"/v2/avatars\");\n```\n\n### Python (requests)\n\n```python\nimport os\nimport requests\n\nresponse = requests.get(\n    \"https://api.heygen.com/v2/avatars\",\n    headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n)\ndata = response.json()\n```\n\n### Python (httpx)\n\n```python\nimport os\nimport httpx\n\nasync with httpx.AsyncClient() as client:\n    response = await client.get(\n        \"https://api.heygen.com/v2/avatars\",\n        headers={\"X-Api-Key\": os.environ[\"HEYGEN_API_KEY\"]}\n    )\n    data = response.json()\n```\n\n## Creating a Reusable API Client\n\n### TypeScript\n\n```typescript\nclass HeyGenClient {\n  private baseUrl = \"https://api.heygen.com\";\n  private apiKey: string;\n\n  constructor(apiKey: string) {\n    this.apiKey = apiKey;\n  }\n\n  async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {\n    const response = await fetch(`${this.baseUrl}${endpoint}`, {\n      ...options,\n      headers: {\n        \"X-Api-Key\": this.apiKey,\n        \"Content-Type\": \"application/json\",\n        ...options.headers,\n      },\n    });\n\n    if (!response.ok) {\n      const error = await response.json();\n      throw new Error(error.message || `HTTP ${response.status}`);\n    }\n\n    return response.json();\n  }\n\n  get<T>(endpoint: string): Promise<T> {\n    return this.request<T>(endpoint);\n  }\n\n  post<T>(endpoint: string, body: unknown): Promise<T> {\n    return this.request<T>(endpoint, {\n      method: \"POST\",\n      body: JSON.stringify(body),\n    });\n  }\n}\n\n// Usage\nconst client = new HeyGenClient(process.env.HEYGEN_API_KEY!);\nconst avatars = await client.get(\"/v2/avatars\");\n```\n\n## API Response Format\n\nAll HeyGen API responses follow this structure:\n\n```typescript\ninterface ApiResponse<T> {\n  error: null | string;\n  data: T;\n}\n```\n\nSuccessful response example:\n\n```json\n{\n  \"error\": null,\n  \"data\": {\n    \"avatars\": [...]\n"},{"path":"references/avatars.md","content":"---\nname: avatars\ndescription: Listing avatars, avatar styles, and avatar_id selection for HeyGen\n---\n\n# HeyGen Avatars\n\nAvatars are the AI-generated presenters in HeyGen videos. You can use public avatars provided by HeyGen or create custom avatars.\n\n## Previewing Avatars Before Generation\n\nAlways preview avatars before generating a video to ensure they match user preferences. Each avatar has preview URLs that can be opened directly in the browser - no downloading required.\n\n### Quick Preview: Open URL in Browser (Recommended)\n\nThe fastest way to preview avatars is to open the URL directly in the default browser. **Do not download the image first** - just pass the URL to `open`:\n\n```bash\n# macOS: Open URL directly in default browser (no download)\nopen \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n\n# Open preview video to see animation\nopen \"https://files.heygen.ai/avatar/preview/josh.mp4\"\n\n# Linux: Use xdg-open\nxdg-open \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n\n# Windows: Use start\nstart \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n```\n\nThe `open` command on macOS opens URLs directly in the default browser - it does not download the file. This is the quickest way to let users see avatar previews.\n\n### List Avatars and Open Previews\n\n```typescript\nasync function listAndPreviewAvatars(openInBrowser = true): Promise<void> {\n  const response = await fetch(\"https://api.heygen.com/v2/avatars\", {\n    headers: { \"X-Api-Key\": process.env.HEYGEN_API_KEY! },\n  });\n  const { data } = await response.json();\n\n  for (const avatar of data.avatars.slice(0, 5)) {\n    console.log(`\\n${avatar.avatar_name} (${avatar.gender})`);\n    console.log(`  ID: ${avatar.avatar_id}`);\n    console.log(`  Preview: ${avatar.preview_image_url}`);\n  }\n\n  // Open preview URLs directly in browser (no download needed)\n  if (openInBrowser) {\n    const { execSync } = require(\"child_process\");\n    for (const avatar of data.avatars.slice(0, 3)) {\n      // 'open' on macOS opens the URL in default browser - doesn't download\n      execSync(`open \"${avatar.preview_image_url}\"`);\n    }\n  }\n}\n```\n\n**Note:** The `open` command passes the URL to the browser - it does not download. The browser fetches and displays the image directly.\n\n### Workflow: Preview Before Generate\n\n1. **List available avatars** - get names, genders, and preview URLs\n2. **Open previews in browser** - `open <preview_image_url>` for quick visual check\n3. **User selects** preferred avatar by name or ID\n4. **Get avatar details** for `default_voice_id`\n5. **Generate video** with selected avatar\n\n```bash\n# Example workflow in terminal\n# 1. List avatars (agent shows options)\n# 2. Open preview for candidate\nopen \"https://files.heygen.ai/avatar/preview/josh.jpg\"\n# 3. User says \"use Josh\"\n# 4. Agent gets details and generates\n```\n\n### Preview Fields in API Response\n\n| Field | Description |\n|-------|-------------|\n| `preview_image_url` | Static image of the avatar (JPG) - open in browser |\n| `preview_video_url` | Shor"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1474,"uniquenessScore":38,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-04-15T00:45:39.800Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"agent-directory","verified":false,"confidence":"low","updatedAt":"2026-10-09T18:51:23.882Z","emptyReason":"No close protocol neighbors were found."},"items":[],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[]}}}