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
  • 3 views
  • 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…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.