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

# Veo 3.1

> Generate video with native audio from a text prompt or a starting image with Google's Veo 3.1 models.

# Veo 3.1

Veo 3.1 is Google's video model. It generates a clip with native audio from a text prompt or from a starting image. The call is asynchronous: submit a job, poll the operation until `done` is `true`, then download the clip. The request and response shapes mirror Google's own API.

## Models

Veo 3.1 ships in three tiers. Each tier is a model ID that you name in the submit path. The request body is identical across tiers.

| Model | Tier | Model ID | Tasks | Resolution | Duration |
| - | - | - | - | - | - |
| Veo 3.1 | Standard | `veo-3.1-generate-preview` | Text-to-video, image-to-video | 720p, 1080p, 4K | 4, 6 or 8 s |
| Veo 3.1 | Fast | `veo-3.1-fast-generate-preview` | Text-to-video, image-to-video | 720p, 1080p, 4K | 4, 6 or 8 s |
| Veo 3.1 | Lite | `veo-3.1-lite-generate-preview` | Text-to-video, image-to-video | 720p, 1080p | 4, 6 or 8 s |

Every tier generates native audio with the picture.

Use Standard when quality matters most. It has the highest fidelity.

Use Fast for iteration. It is quicker and cheaper than Standard, and it still reaches 4K.

Use Lite for the cheapest runs. It is the most economical tier. It stops at 1080p, so it does not accept a 4K request.

## Endpoints

The base URL is `https://api.nunchux.ai`. `{model}` is the model ID of the tier. `{handle}` is the `name` that the submit returns.

| Step | Method | Path |
| - | - | - |
| Submit | `POST` | `/v1/google/v1beta/models/{model}:predictLongRunning` |
| Poll | `GET` | `/v1/google/v1beta/operations/{handle}` |

## Request

Send your API key in the `X-API-Key` header. See [Authentication](/authentication). Set `Content-Type: application/json` on the submit. Send a `User-Agent` header that names your application, for example `YourApp/1.0`.

The body carries a single `instances` entry (the prompt, plus a starting `image` for image-to-video) and optional `parameters`.

<ParamField body="instances" type="array" required>
  One generation request. Veo takes a single instance.

  <Expandable title="Item properties">
    <ParamField body="prompt" type="string" required>
      Description of the video to generate.
    </ParamField>

    <ParamField body="image" type="object">
      The starting frame, for image-to-video. Omit it for text-to-video.

      <Expandable title="Item properties">
        <ParamField body="bytesBase64Encoded" type="string" required>
          Base64-encoded image bytes, with no `data:` prefix.
        </ParamField>

        <ParamField body="mimeType" type="string" required>
          The MIME type of the image, for example `image/jpeg` or `image/png`.
        </ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="parameters" type="object">
  Generation controls.

  <Expandable title="Item properties">
    <ParamField body="resolution" type="string" default="720p">
      Output resolution. `4k` is available on Standard and Fast only. Lite stops at `1080p`.

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

    <ParamField body="durationSeconds" type="integer" default="8">
      Clip length in whole seconds. Billed per output-second.

      Options: `4`, `6`, `8`
    </ParamField>
  </Expandable>
</ParamField>

<Note>
  Image input

  Send `bytesBase64Encoded` with its `mimeType`. Google's own Veo REST documentation shows an `inlineData` wrapper. Do not copy it here. Send `bytesBase64Encoded`.
</Note>

## Response

The submit returns only the operation handle. Poll it until `done` is `true`. On success, the video URL is under `response.generateVideoResponse.generatedSamples[0].video.uri`.

### Submit

<ResponseField name="name" type="string" required>
  The operation handle. Poll `GET /v1/google/v1beta/operations/{handle}` with it. The handle is opaque and variable in length (about 260 characters), with no `operations/` prefix. Store it as an unbounded string, not in a fixed-width column sized from the sample.
</ResponseField>

```json theme={"system"}
{
  "name": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1byI6IjNmOWMyYTdlODFiNGQ2MDUiLCJvcCI6Im1vZGVscy92ZW8tMy4xLWdlbmVyYXRlLXByZXZpZXcvb3BlcmF0aW9ucy85bG5rY2Y5enZ6d20iLCJtIjoidmlkZW8iLCJleHAiOjE3NTY2OTU2MDAsImlhdCI6MTc1NjA5MDgwMH0.QmyRdv9C0cALtiL7GKEn619WTM3IGg1hpwkQzJSLJtc"
}
```

### Poll

<ResponseField name="name" type="string" required>
  The operation handle, echoed back.
</ResponseField>

<ResponseField name="done" type="boolean" required>
  `false` while the job runs. `true` once the video is ready or the job failed. Check `error` before you read `response`.
</ResponseField>

<ResponseField name="error" type="object">
  Present once `done` is `true` and the job failed. `response` is then absent. Carries a `message` that describes the failure.
</ResponseField>

<ResponseField name="response" type="object">
  Present once `done` is `true` and the job succeeded.

  <Expandable title="Item properties">
    <ResponseField name="generateVideoResponse.generatedSamples" type="array">
      The generated clips.

      <Expandable title="Item properties">
        <ResponseField name="video.uri" type="string">
          URL of the output. It expires about an hour after the operation completes. Download it promptly.
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

The status of the job follows from `done` and from which of `error` and `response` is present.

| `done` | Present | Meaning |
| - | - | - |
| `false` | neither | The job is still running. Keep polling. |
| `true` | `response` | The job succeeded. Download `video.uri` now. |
| `true` | `error` | The job failed. `error.message` explains why. |

```json theme={"system"}
{
  "name": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1byI6IjNmOWMyYTdlODFiNGQ2MDUiLCJvcCI6Im1vZGVscy92ZW8tMy4xLWdlbmVyYXRlLXByZXZpZXcvb3BlcmF0aW9ucy85bG5rY2Y5enZ6d20iLCJtIjoidmlkZW8iLCJleHAiOjE3NTY2OTU2MDAsImlhdCI6MTc1NjA5MDgwMH0.QmyRdv9C0cALtiL7GKEn619WTM3IGg1hpwkQzJSLJtc",
  "done": true,
  "response": {
    "generateVideoResponse": {
      "generatedSamples": [
        { "video": { "uri": "https://<host>/<path>.mp4?<signature-and-expiry-params>" } }
      ]
    }
  }
}
```

## Example

Submit, poll, download. Swap the model ID in the submit path for the tier that you want. The poll starts at 5 s and backs off to 30 s.

<CodeGroup>
  ```bash cURL theme={"system"}
  BASE=https://api.nunchux.ai

  # 1. Submit
  SUBMIT=$(curl -sS --fail-with-body "$BASE/v1/google/v1beta/models/veo-3.1-generate-preview:predictLongRunning" \
    -H "X-API-Key: $NUNCHUX_API_KEY" \
    -H "Content-Type: application/json" \
    -H "User-Agent: YourApp/1.0" \
    -d '{
      "instances": [{ "prompt": "a fox trotting through a snowy forest at dawn" }],
      "parameters": { "resolution": "1080p", "durationSeconds": 8 }
    }') || { echo "submit failed: $SUBMIT" >&2; exit 1; }
  OP=$(jq -er '.name' <<<"$SUBMIT") || { echo "no operation name in: $SUBMIT" >&2; exit 1; }

  # 2. Poll until done is true
  DELAY=5
  for _ in $(seq 1 60); do
    RESP=$(curl -sS "$BASE/v1/google/v1beta/operations/$OP" \
      -H "X-API-Key: $NUNCHUX_API_KEY" \
      -H "User-Agent: YourApp/1.0")
    [ "$(jq -r '.done // false' <<<"$RESP")" = "true" ] && break
    sleep "$DELAY"; DELAY=$(( DELAY * 2 > 30 ? 30 : DELAY * 2 ))
  done
  [ "$(jq -r '.done // false' <<<"$RESP")" = "true" ] || { echo "timed out" >&2; exit 1; }

  # 3. Check for failure. done is true on failure too.
  jq -e '.error' >/dev/null 2>&1 <<<"$RESP" && { echo "failed: $(jq -c '.error' <<<"$RESP")" >&2; exit 1; }

  # 4. Download. The URL expires about an hour after completion.
  curl -sSL -o veo.mp4 "$(jq -r '.response.generateVideoResponse.generatedSamples[0].video.uri' <<<"$RESP")"
  ```

  ```python Python theme={"system"}
  # 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
  model = "veo-3.1-generate-preview"
  submit = requests.post(
      f"{BASE}/v1/google/v1beta/models/{model}:predictLongRunning",
      headers=HEADERS,
      json={
          "instances": [{"prompt": "a fox trotting through a snowy forest at dawn"}],
          "parameters": {"resolution": "1080p", "durationSeconds": 8},
      },
  )
  if not submit.ok:
      raise RuntimeError(f"submit failed: HTTP {submit.status_code} {submit.text}")
  op = submit.json()["name"]

  # 2. Poll until done is true
  delay = 5
  for _ in range(60):
      data = requests.get(f"{BASE}/v1/google/v1beta/operations/{op}", headers=HEADERS).json()
      if data.get("done"):
          break
      time.sleep(delay)
      delay = min(delay * 2, 30)
  else:
      raise TimeoutError("Veo operation did not finish")

  # 3. Check for failure. done is true on failure too.
  if "error" in data:
      raise RuntimeError(f"Veo failed: {data['error']}")

  # 4. Download. The URL expires about an hour after completion.
  url = data["response"]["generateVideoResponse"]["generatedSamples"][0]["video"]["uri"]
  with requests.get(url, stream=True, timeout=300) as r:
      r.raise_for_status()
      with open("veo.mp4", "wb") as f:
          for chunk in r.iter_content(1 << 14):
              f.write(chunk)
  ```

  ```javascript JavaScript theme={"system"}
  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 model = "veo-3.1-generate-preview";
  const submitRes = await fetch(
    `${BASE}/v1/google/v1beta/models/${model}:predictLongRunning`,
    {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify({
        instances: [{ prompt: "a fox trotting through a snowy forest at dawn" }],
        parameters: { resolution: "1080p", durationSeconds: 8 },
      }),
    }
  );
  if (!submitRes.ok) throw new Error(`submit failed: HTTP ${submitRes.status} ${await submitRes.text()}`);
  const op = (await submitRes.json()).name;

  // 2. Poll until done is true
  let data;
  let delay = 5_000;
  for (let i = 0; i < 60; i++) {
    data = await fetch(`${BASE}/v1/google/v1beta/operations/${op}`, {
      headers: HEADERS,
    }).then((r) => r.json());
    if (data.done) break;
    await sleep(delay);
    delay = Math.min(delay * 2, 30_000);
  }
  if (!data?.done) throw new Error("Veo operation did not finish");

  // 3. Check for failure. done is true on failure too.
  if (data.error) throw new Error(`Veo failed: ${JSON.stringify(data.error)}`);

  // 4. Download. The URL expires about an hour after completion.
  const uri = data.response.generateVideoResponse.generatedSamples[0].video.uri;
  const res = await fetch(uri);
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
  await writeFile("veo.mp4", Buffer.from(await res.arrayBuffer()));
  ```
</CodeGroup>

## Tips

* Poll, do not busy-wait. Veo usually takes 1 to 4 min. Poll every 5 to 10 s at first, then back off to 30 s.
* Polls are free, and they never take a simultaneous-job slot.
* Set client timeouts to 10 min or more. Render time scales with clip length.
* Check `error` before you read `response`. `done` is `true` on failure too.
* Download the clip as soon as `done` is `true`. The output URL expires about an hour after the operation completes, so do not store the URL.
* Store `name` as an unbounded string, and poll with that exact value.
* Pick `durationSeconds` deliberately. You are billed per output-second, so 4, 6 and 8 s each cost differently.
* Use Lite for cheap iteration and Standard when fidelity matters. 4K is available on Standard and Fast only.
* Do not double-submit. A submit reserves credits, so resubmitting a running job charges twice. If the submit returned a `name`, poll it.

## Errors and limits

* A `200` on the submit is acceptance, not success. A failed job does not return an HTTP error. It appears in the poll as `done: true` with `error`, and `error.message` explains the failure. Read it before you resubmit.
* `durationSeconds` is `4`, `6` or `8`. `resolution` is `720p`, `1080p` or `4k`, and `4k` is available on Standard and Fast only. A parameter that is out of range, such as a Lite request for `4k`, is rejected with a `400`. A `400` is never charged.
* Veo is priced per output-second times the duration, reserved at submit. A job that ends in failure is refunded automatically. The rates are on the [pricing page](https://nunchux.ai/pricing). See [Credits and pricing](/credits-pricing).
* A submit takes a simultaneous-job slot. Polls do not. See [Rate limits](/rate-limits).
* A `5xx` or a timeout on the submit does not tell you whether the job was created, so a bare resubmit can bill twice. If the submit returned a `name`, poll it. If it returned nothing, check `GET /v1/credits` before you try again. A charge with no delivered job is refunded automatically.
* A `5xx` on a poll is safe to retry with backoff. Polls change nothing.
* A `503` with the code `google_unreachable` means that Google could not be reached before anything was sent. Nothing was charged. Retry with backoff.
* A `503` whose message says `google pass-through not enabled` means that the route is not available. Do not retry it in a loop.

The full error contract, the retry rules and the per-plan caps are on the [Errors](/errors) and [Rate limits](/rate-limits) pages. How every asynchronous partner model submits, polls, bills and refunds is on the [Partner models overview](/partner-models/overview).


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