Skip to content
Back to skills

Heygen Api

ASecurity

HeyGen API for AI video generation — talking avatar videos, lip-sync, and video translation. Use when creating personalized video at scale, AI avatars for marketing, automated video content pipelines, or video localization with lip-sync in any language.

  • 142 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 29, 2026
ai-agentspythonrustgoshellbashgitapi

Works with

  • cursor
  • terminal
  • cli
  • api

Security analysis

A92/100
  • mediumUses curl or wget to download content
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned October 4, 2026

npx -y skills add TerminalSkills/skills --skill heygen-api --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Heygen Api?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Heygen Api
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/terminalskills-heygen-api/badge)](https://www.skillsdirectory.com/skills/terminalskills-heygen-api)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: heygen-api
description: >-
  HeyGen API for AI video generation — talking avatar videos, lip-sync, and
  video translation. Use when creating personalized video at scale, AI avatars
  for marketing, automated video content pipelines, or video localization with
  lip-sync in any language.
license: Apache-2.0
compatibility: "HeyGen API v3 (v1/v2 endpoints are retired on October 31, 2026). Python 3.9+ with requests. HeyGen API key required."
metadata:
  author: terminal-skills
  version: "1.1.0"
  category: data-ai
  tags: ["heygen", "video-generation", "avatar", "ai-video", "lip-sync"]
  use-cases:
    - "Generate personalized sales outreach videos with AI avatars at scale"
    - "Translate and lip-sync marketing videos into multiple languages"
    - "Automate talking-head video creation from scripts"
  agents: [claude-code, openai-codex, gemini-cli, cursor]
---

# HeyGen API

## Overview

HeyGen provides REST APIs to create talking avatar videos, clone voices, translate videos with lip-sync, and run real-time avatar sessions. Use it to automate video content production at scale — from personalized sales outreach to multilingual marketing campaigns.

The current API is v3 (`/v3/...`). The v1 and v2 endpoints this skill used before (`/v2/video/generate`, `/v1/video_status.get`, `/v2/video_translate`) are retired on October 31, 2026 and already answer with a `Deprecation` header and a `warning` object naming the v3 replacement.

## Setup

```bash
python3 -m venv .venv && .venv/bin/pip install requests
read -rs HEYGEN_API_KEY && export HEYGEN_API_KEY   # paste the key; it stays out of shell history

# Check the key and see the remaining credits
curl -s https://api.heygen.com/v3/users/me -H "X-Api-Key: $HEYGEN_API_KEY"
```

Base URL: `https://api.heygen.com`. Keys are created in the API dashboard at `https://app.heygen.com/developers/api`. Every generation call spends credits.

## Core Concepts

- **Avatar look**: Avatars are groups (the character) that contain looks (outfit, pose). The look `id` is the `avatar_id` you send when creating a video.
- **Voice**: TTS or cloned voice that speaks the script. When `voice_id` is omitted, the look's default voice is used.
- **Video**: Generated asynchronously — status goes `pending` → `processing` → `completed` or `failed`; poll, or get a webhook.
- **Engine**: Avatar IV renders by default; `"engine": {"type": "avatar_v"}` or `avatar_iii` only for looks that list it in `supported_api_engines`.
- **Video Translation**: Send an existing video; HeyGen re-dubs it in the target languages with matching lip-sync.

## Instructions

### Step 1: List available avatars and voices

```python
import os
import time
import requests

BASE = "https://api.heygen.com"
HEADERS = {"X-Api-Key": os.environ["HEYGEN_API_KEY"]}

def api(method: str, path: str, **kwargs) -> dict:
    """Call the API; wait out rate limits, raise with the API's own error code and message."""
    headers = {**HEADERS, **kwargs.pop("headers", {})}   # lets a call add e.g. Idempotency-Key
    while True:
        r = requests.request(method, f"{BASE}{path}", headers=headers, timeout=60, **kwargs)
        if r.ok:
            return r.json()
        err = r.json().get("error", {})
        if r.status_code == 429 and err.get("code") == "rate_limit_exceeded":
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        raise RuntimeError(f"{r.status_code} {err.get('code')}: {err.get('message')}")

def list_all(path: str, **params) -> list:
    """Follow cursor pagination: has_more / next_token."""
    items, token = [], None
    while True:
        page = api("GET", path, params={**params, **({"token": token} if token else {})})
        items += page["data"]
        if not page.get("has_more"):
            return items
        token = page["next_token"]

looks = list_all("/v3/avatars/looks", ownership="public", limit=50)   # "private" for your own avatars
voices = list_all("/v3/voices", type="public", language="English", limit=100)

for look in looks[:5]:
    print(f"Avatar look: {look['id']} - {look['name']} ({look['avatar_type']})")
for v in voices[:5]:
    print(f"Voice: {v['voice_id']} - {v['name']} ({v['language']})")
```

### Step 2: Create a talking-head video

```python
def create_avatar_video(script: str, avatar_id: str, voice_id: str = "", title: str = "Avatar video") -> str:
    """Submit a video generation job and return the video_id."""
    body = {
        "type": "avatar",
        "avatar_id": avatar_id,          # a look id from /v3/avatars/looks
        "script": script,                # at most 5,000 characters
        "title": title,
        "resolution": "1080p",           # "720p", "1080p" or "4k"
        "aspect_ratio": "16:9",          # also "9:16", "1:1", "4:5", "5:4", "auto"
        "background": {"type": "color", "value": "#FFFFFF"},
        "voice_settings": {"speed": 1.0},  # 0.5–1.5
    }
    if voice_id:
        body["voice_id"] = voice_id
    return api("POST", "/v3/videos", json=body)["data"]["video_id"]

video_id = create_avatar_video(
    script="Hi Dana, I wanted to personally show how Brightline cut cloud costs by 30% for teams like yours.",
    avatar_id=looks[0]["id"],
    voice_id=voices[0]["voice_id"],
    title="Outreach - Dana at Brightline",
)
print(f"Video submitted: {video_id}")
```

To lip-sync your own recording instead of a script, replace `script` and `voice_id` with `audio_url` (or `audio_asset_id`); the two are mutually exclusive.

### Step 3: Poll video status

```python
def wait_for_video(video_id: str, poll_interval: int = 10, timeout: int = 900) -> dict:
    """Poll until the video is ready; return the video record."""
    start = time.time()
    while True:
        video = api("GET", f"/v3/videos/{video_id}")["data"]
        print(f"[{int(time.time() - start)}s] Status: {video['status']}")
        if video["status"] == "completed":
            return video
        if video["status"] == "failed":
            raise RuntimeError(f"{video.get('failure_code')}: {video.get('failure_message')}")
        if time.time() - start > timeout:
            raise TimeoutError(f"Video not ready after {timeout}s")
        time.sleep(poll_interval)

video = wait_for_video(video_id)
print(f"Video ready: {video['video_url']} ({video['duration']}s)")
```

### Step 4: Download the result

```python
def download_video(url: str, output_path: str = "output.mp4") -> str:
    r = requests.get(url, stream=True, timeout=120)
    r.raise_for_status()
    with open(output_path, "wb") as f:
        for chunk in r.iter_content(chunk_size=8192):
            f.write(chunk)
    size_mb = os.path.getsize(output_path) / 1024 / 1024
    print(f"Downloaded: {output_path} ({size_mb:.1f} MB)")
    return output_path

download_video(video["video_url"], "dana_brightline_outreach.mp4")
```

### Video Translation (lip-sync)

```python
def translate_video(video_url: str, languages: list[str], title: str = "Translated video") -> list[str]:
    """languages are names from GET /v3/video-translations/languages, e.g. "Spanish", "French"."""
    body = {
        "video": {"type": "url", "url": video_url},   # or {"type": "asset_id", "asset_id": ...}
        "output_languages": languages,
        "mode": "speed",                              # "precision" for better lip-sync, slower
        "title": title,
    }
    return api("POST", "/v3/video-translations", json=body)["data"]["video_translation_ids"]

def wait_for_translation(translation_id: str, poll_interval: int = 15) -> dict:
    while True:
        job = api("GET", f"/v3/video-translations/{translation_id}")["data"]
        if job["status"] == "completed":
            return job
        if job["status"] == "failed":
            raise RuntimeError(job.get("failure_message"))
        time.sleep(poll_interval)   # other statuses: "pending", "running"
```

### Webhook setup (optional)

Two ways to avoid polling. Per request: add `callback_url` and your own `callback_id` to the body of `POST /v3/videos` or `POST /v3/video-translations`. For all jobs: register an endpoint once and verify every delivery.

```python
import hashlib
import hmac

endpoint = api("POST", "/v3/webhooks/endpoints", json={
    "url": "https://hooks.brightline.io/heygen",
    "events": ["avatar_video.success", "avatar_video.fail", "video_translate.success", "video_translate.fail"],
})["data"]
# endpoint["secret"] is shown only now — store it as HEYGEN_WEBHOOK_SECRET

def is_valid_signature(raw_body: bytes, signature_header: str) -> bool:
    """The `signature` header is the hex HMAC-SHA256 of the raw request body."""
    secret = os.environ["HEYGEN_WEBHOOK_SECRET"].encode()
    expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)
```

The body carries `event_type` and `event_data` (for `avatar_video.success`: `video_id`, `url`, `callback_id`). Answer with a 2xx within 10 seconds; failed deliveries are retried for up to 24 hours, so de-duplicate on `video_id` plus `event_type`.

## Examples

### Example 1: Personalized outreach in one batch

User request: "Make a short personalized video for every contact in contacts.csv (columns: name, company, offer)."

```python
import csv

def bulk_generate_videos(csv_file: str, avatar_id: str, voice_id: str) -> str:
    with open(csv_file, newline="") as f:
        rows = list(csv.DictReader(f))
    videos = [{
        "type": "avatar",
        "avatar_id": avatar_id,
        "voice_id": voice_id,
        "title": f"Outreach - {row['name']}",
        "script": f"Hi {row['name']}, I wanted to personally reach out to you at {row['company']}. {row['offer']} Let's connect!",
    } for row in rows]
    # One call queues up to 100 videos; split longer lists into chunks of 100
    return api("POST", "/v3/videos/batches", json={"title": "Outreach batch", "videos": videos})["data"]["batch_id"]

batch_id = bulk_generate_videos("contacts.csv", looks[0]["id"], voices[0]["voice_id"])
batch = api("GET", f"/v3/videos/batches/{batch_id}", params={"limit": 100})["data"]
print(batch["status"], batch["counts_by_status"])
for item in batch["items"]:
    print(item["item_index"], item["status"], item["video_id"])
```

Result: the batch id comes back immediately; polling prints something like `processing {'completed': 1, 'processing': 2}` and then one line per contact (`0 completed vid_9f2c…`). `item_index` is the row's position in the CSV, so it maps each `video_id` back to a contact; pass the id to `wait_for_video` and `download_video`.

### Example 2: Localize a product video into Spanish and French

User request: "Dub our launch video into Spanish and French with lip-sync."

```python
ids = translate_video(
    "https://cdn.brightline.io/videos/launch-2026.mp4",   # must be publicly downloadable
    ["Spanish", "French"],
    title="Launch video",
)
for translation_id in ids:
    job = wait_for_translation(translation_id)
    download_video(job["video_url"], f"launch_{job['output_language']}.mp4")
    print(job["output_language"], job.get("srt_caption_url"))   # caption fields are omitted until the file exists
```

Result: one translation job per language. Each finished job carries `video_url`, `audio_url` and, once the caption files are ready, `srt_caption_url` and `vtt_caption_url`, so the dubbed video and its captions are saved without a separate captions call.

## Guidelines

- Migrate v1/v2 code before October 31, 2026: `/v2/video/generate` → `POST /v3/videos`, `/v1/video_status.get` → `GET /v3/videos/{video_id}`, `/v2/avatars` → `GET /v3/avatars` (groups) and `GET /v3/avatars/looks` (the ids used as `avatar_id`), `/v2/voices` → `GET /v3/voices`, `/v2/video_translate` → `POST /v3/video-translations`. The v3 body is flat (`type`, `avatar_id`, `script`) — no `video_inputs` array, no `dimension`.
- HeyGen limits concurrent jobs (10 on Pay-As-You-Go, 20 on Enterprise) and request rate; a 429 carries a `Retry-After` header. Honour it instead of sleeping a fixed second between calls. A 429 with code `quota_exceeded` or a 402 (`insufficient_credit`) will not clear by retrying.
- Download URLs are presigned and expire. Save the file promptly, or call `GET /v3/videos/{video_id}` again for a fresh URL.
- Send an `Idempotency-Key` header on create calls you may retry, so a network error does not render (and bill) the same video twice.
- Input files referenced by URL must be publicly reachable and match their extension; otherwise the job fails with `download_failed`. Upload private files through `POST /v3/assets` and pass the `asset_id`.
- A digital twin of a real person has a consent step (`POST /v3/avatars/{group_id}/consent`) before it renders; do not build avatars of people who have not agreed.
- For real-time interactive scenarios (chatbots, live calls), use LiveAvatar (`@heygen/liveavatar-web-sdk`, documented at docs.liveavatar.com). The older `@heygen/streaming-avatar` package is deprecated.
- Verify webhook signatures against the raw body bytes before trusting a payload.
- Store your API key in environment variables — never hardcode it.

Files in this skill

  • SKILL.md8.4 KB
  • _scores.json1.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…