La interfaz está localizada; las descripciones y documentos de las Skills se traducen automáticamente con IA a su idioma. El código, los comandos, rutas y términos técnicos se mantienen en el original, y prevalece el documento del autor.
Explorar Skills
Productividad · SKILL.md

heygen-api

heygen-api

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.

Independent GitHub snapshot · Automated classification · Original documentation governs

Tterminalskills / skillsApache-2.0

Documentación de la Skill

GitHub

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

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

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

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

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

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)

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.

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)."

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."

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.
Skills guardadas