> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nunchux.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Authenticate every request with your API key in the `X-API-Key` header or `Authorization: Bearer`. Keys start with `sk-nunchux-`.
> Image generation is synchronous: the image is in the response.
> Video generation is asynchronous: submit the job, poll its task until it reaches a terminal status, then download the output promptly, because output URLs expire.

# HeyGen Avatar

> Turn a photo or a HeyGen avatar into a lip-synced talking-avatar video with HeyGen Avatar 4 Photo and Avatar 5 Digital, driven by audio or a script in a chosen voice.

# HeyGen Avatar

HeyGen Avatar turns a photo, or a HeyGen avatar, into a lip-synced talking-avatar video. You supply the face and a speech source: an audio file that you host, or a script that HeyGen reads in a voice that you choose. The endpoint is asynchronous. Submit the job, poll until the status is terminal, then download the video. The request and response shapes mirror HeyGen's API.

## Models

HeyGen offers two engines. They take the same speech sources, share every other field, and have the same price. They differ only in where the face comes from.

| Engine | Id | Face input | Speech input | Output |
| - | - | - | - | - |
| Avatar 4 Photo | `avatar_iv` | A photo that you supply, by public URL or inline as base64 | `audio_url`, or `script` with `voice_id` | Lip-synced MP4 video of the photo |
| Avatar 5 Digital | `avatar_v` | A ready-made avatar from HeyGen's avatar catalog (`avatar_id`) | `audio_url`, or `script` with `voice_id` | Lip-synced MP4 video of the avatar |

<Note>
  The endpoint takes no `model` field, so the ids above are not values that you send. The request body selects the engine. `"type": "image"` with an `image` object selects Avatar 4 Photo. `"type": "avatar"` with an `avatar_id` and `"engine": {"type": "avatar_v"}` selects Avatar 5 Digital.
</Note>

Use Avatar 4 Photo when you have a photo of the face that you need. Use Avatar 5 Digital when you want a ready-made look from HeyGen's avatar catalog. Neither engine has a duration control. The clip is as long as the audio or the script. Common uses are presenter clips, narrated explainers, and re-voiced versions of the same face.

## Endpoints

All endpoints share the base URL `https://api.nunchux.ai`.

| Purpose | Method | Path |
| - | - | - |
| Submit a video | POST | `/v1/heygen/v3/videos` |
| Poll a job | GET | `/v1/heygen/v3/videos/{video_id}` |
| List voices | GET | `/v1/heygen/v3/voices` |

## Request

Send your API key in the `X-API-Key` header (see [Authentication](/authentication)) and set `Content-Type: application/json`. Also send a `User-Agent` header that identifies your application, for example `YourApp/1.0`. A request that keeps the default `User-Agent` of your HTTP client can be rejected.

Send one face and one speech source in the body. The face fields select the engine. The speech fields are the same on both engines.

<ParamField body="type" type="string" required>
  Selects the engine. `image` selects Avatar 4 Photo and animates the photo in `image`. `avatar` selects Avatar 5 Digital and animates the avatar in `avatar_id`.

  Options: `image`, `avatar`
</ParamField>

<ParamField body="image" type="object">
  Avatar 4 Photo only. The photo to animate. Required when `type` is `image`.

  <Expandable title="Item properties">
    <ParamField body="type" type="string" required>
      `url` for a link that you host, `base64` for inline bytes.

      Options: `url`, `base64`
    </ParamField>

    <ParamField body="url" type="string">
      Required when `image.type` is `url`. Public HTTPS URL of the portrait, hosted by you.
    </ParamField>

    <ParamField body="media_type" type="string">
      Required when `image.type` is `base64`. MIME type of the bytes, for example `image/jpeg` or `image/png`.
    </ParamField>

    <ParamField body="data" type="string">
      Required when `image.type` is `base64`. The raw base64-encoded image bytes, without a `data:` URI prefix.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="avatar_id" type="string">
  Avatar 5 Digital only. The avatar to animate, from HeyGen's avatar catalog. Required when `type` is `avatar`.
</ParamField>

<ParamField body="engine" type="object">
  Avatar 5 Digital only. Selects the Avatar 5 renderer. Omit it when `type` is `image`, because that body already selects Avatar 4 Photo.

  <Expandable title="Item properties">
    <ParamField body="type" type="string" required>
      Must be `avatar_v`.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="audio_url" type="string">
  Both engines. Public HTTPS URL of a speech audio file (WAV or MP3), hosted by you. Send this or `script` with `voice_id`. One speech source is required.
</ParamField>

<ParamField body="script" type="string">
  Both engines. Text for the avatar to speak. Requires `voice_id`. Use it instead of `audio_url`.
</ParamField>

<ParamField body="voice_id" type="string">
  Both engines. The voice that reads `script`. Required when `script` is set. List the voices with `GET /v1/heygen/v3/voices`.
</ParamField>

<ParamField body="resolution" type="string" default="1080p">
  Both engines. Output pixel tier: the size, not the shape. Resolution does not change the price or the render time.

  Options: `720p`, `1080p`, `4k`
</ParamField>

<ParamField body="aspect_ratio" type="string" default="16:9">
  Both engines. Output canvas shape, independent of `resolution`. See [Resolution and aspect ratio](#resolution-and-aspect-ratio).

  Options: `auto`, `16:9`, `9:16`, `4:5`, `5:4`, `1:1`
</ParamField>

### Resolution and aspect ratio

`resolution` sets the pixel tier and `aspect_ratio` sets the canvas. The dimensions below apply at `16:9`. At any other ratio, the same tier is fitted to that canvas. Read the shape from `aspect_ratio`, never from the tier name.

| Resolution | Dimensions at 16:9 |
| - | - |
| `720p` | 1280 × 720 |
| `1080p` | 1920 × 1080 |
| `4k` | 3840 × 2160 |

Set `aspect_ratio` explicitly. The default is `16:9`, so a portrait face sent without it comes back inside a landscape frame with bars down both sides. `auto` has a different meaning on each engine.

* Avatar 5 Digital: `auto` follows the shape of the avatar. Use it.
* Avatar 4 Photo: `auto` does not read your photo. A 1080 × 1920 portrait comes back as 1280 × 720, cropped to the face, with no bars to signal it. Send the canvas nearest to the pixels of your photo: `9:16` for a 1080 × 1920 photo, `4:5` for a 3:4 phone portrait. A mismatched ratio is padded, not reframed.
* `1:1` re-canvases every public-catalog avatar, since those are all landscape or portrait. A private or custom avatar can differ.

### Avatar 4 Photo, photo and audio

```bash theme={"system"}
# The media URLs below are placeholders. Point them at your own publicly
# reachable files before you run this.
curl https://api.nunchux.ai/v1/heygen/v3/videos \
  -H "X-API-Key: $NUNCHUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "User-Agent: YourApp/1.0" \
  -d '{
    "type": "image",
    "image": { "type": "url", "url": "https://example.com/example-1080x1920.jpg" },
    "audio_url": "https://example.com/example.mp3",
    "resolution": "1080p",
    "aspect_ratio": "9:16"
  }'
```

### Avatar 4 Photo, inline base64 photo

```bash theme={"system"}
# The media URL below is a placeholder. Point it at your own publicly
# reachable file before you run this.
# 1. Encode the photo (raw base64, no "data:" prefix)
IMG=$(base64 -w0 portrait.jpg)

# 2. Submit
curl https://api.nunchux.ai/v1/heygen/v3/videos \
  -H "X-API-Key: $NUNCHUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "User-Agent: YourApp/1.0" \
  -d '{
    "type": "image",
    "image": { "type": "base64", "media_type": "image/jpeg", "data": "'"$IMG"'" },
    "audio_url": "https://example.com/example.mp3",
    "resolution": "1080p",
    "aspect_ratio": "9:16"
  }'
```

### Avatar 5 Digital, catalog avatar and script

```bash theme={"system"}
curl https://api.nunchux.ai/v1/heygen/v3/videos \
  -H "X-API-Key: $NUNCHUX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "User-Agent: YourApp/1.0" \
  -d '{
    "type": "avatar",
    "avatar_id": "your_avatar_id",
    "engine": { "type": "avatar_v" },
    "script": "Hi! Here is a quick walkthrough of what we shipped this week.",
    "voice_id": "en_us_female_01",
    "resolution": "1080p",
    "aspect_ratio": "auto"
  }'
```

## Voices

When you drive from a `script`, choose a `voice_id` from the voices endpoint.

```bash theme={"system"}
curl https://api.nunchux.ai/v1/heygen/v3/voices \
  -H "X-API-Key: $NUNCHUX_API_KEY" \
  -H "User-Agent: YourApp/1.0"
```

The response lists each voice with its language and gender.

```json theme={"system"}
{
  "data": {
    "voices": [
      { "voice_id": "en_us_female_01", "language": "English", "gender": "female" },
      { "voice_id": "en_us_male_01",   "language": "English", "gender": "male" }
    ]
  }
}
```

## Response

Submit returns a `video_id`. Poll it until `status` is `completed` or `failed`. Then download `video_url`.

### Submit

<ResponseField name="data" type="object" required>
  The submitted job.

  <Expandable title="Item properties">
    <ResponseField name="video_id" type="string" required>
      The job handle. It is an opaque, variable-length token of about 230 characters. Store it as an unbounded string and send it verbatim on the poll path.
    </ResponseField>

    <ResponseField name="status" type="string" required>
      The initial job state: `waiting` or `pending` while the job is queued.
    </ResponseField>

    <ResponseField name="output_format" type="string" required>
      Always `mp4`.
    </ResponseField>
  </Expandable>
</ResponseField>

```json theme={"system"}
{
  "data": {
    "video_id": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1byI6IjNmOWMyYTdlODFiNGQ2MDUiLCJ2IjoiN2QxZTlmMmM0YjhhNGUwZjljM2IyYTFkNmU1ZjRjM2IiLCJtIjoidmlkZW8iLCJleHAiOjE3NTY2OTU2MDAsImlhdCI6MTc1NjA5MDgwMH0.vYKUo0L4vIUnsO4HNHxI5lPoDaYX7gY_dwslw3nWGoI",
    "status": "waiting",
    "output_format": "mp4"
  }
}
```

### Poll

<ResponseField name="data" type="object" required>
  The job status.

  <Expandable title="Item properties">
    <ResponseField name="id" type="string" required>
      The job handle, echoed back.
    </ResponseField>

    <ResponseField name="status" type="string" required>
      `waiting` or `pending` while the job is queued, then `processing`, then `completed` or `failed`. Only `completed` and `failed` are terminal. Treat any other value as still running and keep polling.
    </ResponseField>

    <ResponseField name="duration" type="number">
      Output length in seconds. Present when `status` is `completed`. This is the number of output-seconds that you are billed for.
    </ResponseField>

    <ResponseField name="video_url" type="string">
      URL of the output video. Present when `status` is `completed`. The URL expires within hours, so download it promptly.
    </ResponseField>
  </Expandable>
</ResponseField>

```json theme={"system"}
{
  "data": {
    "id": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1byI6IjNmOWMyYTdlODFiNGQ2MDUiLCJ2IjoiN2QxZTlmMmM0YjhhNGUwZjljM2IyYTFkNmU1ZjRjM2IiLCJtIjoidmlkZW8iLCJleHAiOjE3NTY2OTU2MDAsImlhdCI6MTc1NjA5MDgwMH0.vYKUo0L4vIUnsO4HNHxI5lPoDaYX7gY_dwslw3nWGoI",
    "status": "completed",
    "duration": 7.02,
    "video_url": "https://resource.heygen.ai/video/...mp4"
  }
}
```

## Example

Submit, poll, then download. The example uses Avatar 4 Photo with a hosted photo and a hosted audio file. For Avatar 5 Digital, send `"type": "avatar"` with an `avatar_id` and `"engine": {"type": "avatar_v"}` instead of the `image` object. To drive from text, send `script` and `voice_id` instead of `audio_url`.

<CodeGroup>
  ```bash cURL theme={"system"}
  # The media URLs below are placeholders. Point them at your own publicly
  # reachable files before you run this.
  BASE=https://api.nunchux.ai

  # 1. Submit
  SUBMIT=$(curl -sS --fail-with-body "$BASE/v1/heygen/v3/videos" \
    -H "X-API-Key: $NUNCHUX_API_KEY" \
    -H "Content-Type: application/json" \
    -H "User-Agent: YourApp/1.0" \
    -d '{
      "type": "image",
      "image": { "type": "url", "url": "https://example.com/example-1080x1920.jpg" },
      "audio_url": "https://example.com/example.mp3",
      "resolution": "1080p",
      "aspect_ratio": "9:16"
    }') || { echo "submit failed: $SUBMIT" >&2; exit 1; }
  VIDEO_ID=$(jq -er '.data.video_id' <<<"$SUBMIT") || { echo "no video id in: $SUBMIT" >&2; exit 1; }

  # 2. Poll until status is completed or failed
  DELAY=10
  for _ in $(seq 1 60); do
    RESP=$(curl -sS "$BASE/v1/heygen/v3/videos/$VIDEO_ID" \
      -H "X-API-Key: $NUNCHUX_API_KEY" \
      -H "User-Agent: YourApp/1.0")
    STATUS=$(jq -r '.data.status' <<<"$RESP")
    case "$STATUS" in completed|failed) break ;; esac
    sleep "$DELAY"; DELAY=$(( DELAY * 2 > 30 ? 30 : DELAY * 2 ))
  done

  # 3. Stop on failure
  [ "$STATUS" = "completed" ] || { echo "not completed ($STATUS): $(jq -c '.data' <<<"$RESP")" >&2; exit 1; }

  # 4. Download
  curl -sSL -o avatar.mp4 "$(jq -r '.data.video_url' <<<"$RESP")"
  ```

  ```python Python theme={"system"}
  # The media URLs below are placeholders. Point them at your own publicly
  # reachable files before you run this.
  # pip install requests
  import os, time, requests

  BASE = "https://api.nunchux.ai"
  HEADERS = {
      "X-API-Key": os.environ["NUNCHUX_API_KEY"],
      "User-Agent": "YourApp/1.0",
  }

  # 1. Submit
  submit = requests.post(
      f"{BASE}/v1/heygen/v3/videos",
      headers=HEADERS,
      json={
          "type": "image",
          "image": {"type": "url", "url": "https://example.com/example-1080x1920.jpg"},
          "audio_url": "https://example.com/example.mp3",
          "resolution": "1080p",
          "aspect_ratio": "9:16",
      },
  )
  if not submit.ok:
      raise RuntimeError(f"submit failed: HTTP {submit.status_code} {submit.text}")
  video_id = submit.json()["data"]["video_id"]

  # 2. Poll until status is completed or failed
  delay = 10
  for _ in range(60):
      data = requests.get(f"{BASE}/v1/heygen/v3/videos/{video_id}", headers=HEADERS).json()["data"]
      if data["status"] in ("completed", "failed"):
          break
      time.sleep(delay)
      delay = min(delay * 2, 30)

  # 3. Stop on failure
  if data["status"] != "completed":
      raise RuntimeError(f"HeyGen job not completed ({data['status']}): {data}")

  # 4. Download
  with requests.get(data["video_url"], stream=True, timeout=300) as r:
      r.raise_for_status()
      with open("avatar.mp4", "wb") as f:
          for chunk in r.iter_content(1 << 14):
              f.write(chunk)
  ```

  ```javascript JavaScript theme={"system"}
  // The media URLs below are placeholders. Point them at your own publicly
  // reachable files before you run this.
  import { writeFile } from "node:fs/promises";

  const BASE = "https://api.nunchux.ai";
  const HEADERS = {
    "X-API-Key": process.env.NUNCHUX_API_KEY,
    "Content-Type": "application/json",
    "User-Agent": "YourApp/1.0",
  };
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  // 1. Submit
  const submitRes = await fetch(`${BASE}/v1/heygen/v3/videos`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      type: "image",
      image: { type: "url", url: "https://example.com/example-1080x1920.jpg" },
      audio_url: "https://example.com/example.mp3",
      resolution: "1080p",
      aspect_ratio: "9:16",
    }),
  });
  if (!submitRes.ok) throw new Error(`submit failed: HTTP ${submitRes.status} ${await submitRes.text()}`);
  const videoId = (await submitRes.json()).data.video_id;

  // 2. Poll until status is completed or failed
  let data;
  let delay = 10_000;
  for (let i = 0; i < 60; i++) {
    data = (
      await fetch(`${BASE}/v1/heygen/v3/videos/${videoId}`, { headers: HEADERS }).then((r) =>
        r.json()
      )
    ).data;
    if (data.status === "completed" || data.status === "failed") break;
    await sleep(delay);
    delay = Math.min(delay * 2, 30_000);
  }

  // 3. Stop on failure
  if (data?.status !== "completed") {
    throw new Error(`HeyGen job not completed (${data?.status}): ${JSON.stringify(data)}`);
  }

  // 4. Download
  const res = await fetch(data.video_url);
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
  await writeFile("avatar.mp4", Buffer.from(await res.arrayBuffer()));
  ```
</CodeGroup>

## Tips

* Start from a strong face. On Avatar 4 Photo, send a clear, front-facing portrait with the face well lit and unobstructed. On Avatar 5 Digital, pick the `avatar_id` whose look fits the message.
* You host the inputs. There is no upload endpoint. HeyGen downloads a URL during the job, not at submit, so the URL must stay publicly reachable until the job completes. An inline base64 photo needs no hosting.
* Match the speech source to the job. Point `audio_url` at polished narration that you host. Use `script` with `voice_id` for fast iterations, for language swaps, and when you have nowhere to host an audio file.
* Write scripts in natural, spoken phrasing. It lip-syncs better than dense written prose.
* Set `resolution` explicitly. The default is `1080p`.
* Set `aspect_ratio` explicitly. On Avatar 4 Photo, send the canvas nearest to the pixels of your photo. On Avatar 5 Digital, send `auto`. See [Resolution and aspect ratio](#resolution-and-aspect-ratio).
* Poll every 10 s at first, then back off to 30 s. Polls are free and take no job slot. Render time scales with clip length, so allow 10 minutes or more and set your client timeout to match.
* Branch on `failed`. It is terminal, and the body carries the reason.
* Download the video as soon as `status` is `completed`. The URL expires within hours.
* Store `video_id` as an unbounded string. It is about 230 characters long, and you must send it verbatim on the poll path.
* A 5xx or a timeout on submit does not tell you whether the job was created. Do not resubmit blindly, because a second job that completes is billed too. If the submit returned a `video_id`, poll it. If it returned nothing, wait, then read your credit balance before you submit again.

## Errors and limits

* A 200 on submit means that HeyGen accepted the job. It does not mean that the job succeeded. A failure appears later as `failed` in the poll response, with the reason in the body.
* A failed job is never charged.
* `resolution` accepts `720p`, `1080p` and `4k`. Any other value returns 400 with the code `unsupported_resolution`. Nothing is charged.
* A submit with no face (`image` when `type` is `image`, `avatar_id` when `type` is `avatar`), with no speech source, or with `script` but no `voice_id` returns 400.
* A `video_id` is scoped to your account. Poll only a handle that your own submit returned. A handle that belongs to another account returns 409.
* Neither engine has a duration control. The clip is as long as the audio or the script.

A URL that HeyGen cannot download fails the submit at once with 400 and the code `invalid_parameter`. Host the file publicly, or send the photo inline as base64.

```json theme={"system"}
{
  "error": {
    "code": "invalid_parameter",
    "message": "Invalid URL in files[0]: Could not download the file. Ensure the URL is publicly accessible.",
    "doc_url": "https://developers.heygen.com/docs/error-codes#invalid-parameter"
  }
}
```

The general error contract, the rate limits and the credit rules apply to every endpoint. See [Errors](/errors), [Rate limits](/rate-limits), [Credits and pricing](/credits-pricing) and the [Partner models overview](/partner-models/overview).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.