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

# Video generation

> Start a video job with POST /v1/videos, poll it until it finishes, and download the MP4.

A video clip takes from half a minute to several minutes, so `POST /v1/videos` doesn't wait for it. NagaAI answers `202` at once with a job `id`, you poll `GET /v1/videos/{id}` until `status` is `completed` or `failed`, and then you download the file from `GET /v1/videos/{id}/content`. The request and job fields follow the OpenRouter Videos API.

## Generate a clip

The loop below submits a job, polls every ten seconds and saves the MP4. Choose a model whose `operations` include `video_generation` in [`GET /v1/models`](https://docs.naga.ac/get-started/models).

```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-dimmed"}}
import os
import time

import requests

BASE = "https://api.naga.ac/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['NAGA_API_KEY']}"}

response = requests.post(
    f"{BASE}/videos",
    headers=HEADERS,
    json={
        "model": "kling-v3.0-pro",
        "prompt": "A red paper boat drifting on a calm lake at dawn",
        "duration": 8,
        "resolution": "720p",
        "aspect_ratio": "16:9",
    },
)
response.raise_for_status()
job = response.json()

while job["status"] in ("pending", "in_progress"):
    time.sleep(10)
    job = requests.get(job["polling_url"], headers=HEADERS).json()

if job["status"] == "completed":
    video = requests.get(f"{BASE}/videos/{job['id']}/content", headers=HEADERS)
    with open("boat.mp4", "wb") as file:
        file.write(video.content)
else:
    print(job["error"])
```

```javascript Node.js theme={"theme":{"light":"github-light-default","dark":"github-dark-dimmed"}}
import { writeFile } from "node:fs/promises";

const BASE = "https://api.naga.ac/v1";
const headers = { Authorization: `Bearer ${process.env.NAGA_API_KEY}` };

const response = await fetch(`${BASE}/videos`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "kling-v3.0-pro",
    prompt: "A red paper boat drifting on a calm lake at dawn",
    duration: 8,
    resolution: "720p",
    aspect_ratio: "16:9",
  }),
});
if (!response.ok) throw new Error(await response.text());
let job = await response.json();

while (job.status === "pending" || job.status === "in_progress") {
  await new Promise((resolve) => setTimeout(resolve, 10_000));
  job = await (await fetch(job.polling_url, { headers })).json();
}

if (job.status === "completed") {
  const video = await fetch(`${BASE}/videos/${job.id}/content`, { headers });
  await writeFile("boat.mp4", Buffer.from(await video.arrayBuffer()));
} else {
  console.log(job.error);
}
```

```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-dimmed"}}
JOB_ID=$(curl https://api.naga.ac/v1/videos \
  -H "Authorization: Bearer $NAGA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "kling-v3.0-pro", "prompt": "A red paper boat drifting on a calm lake at dawn", "duration": 8, "resolution": "720p", "aspect_ratio": "16:9"}' \
  | jq -r .id)

# Poll until "status" is "completed" or "failed".
while true; do
  STATUS=$(curl https://api.naga.ac/v1/videos/$JOB_ID \
    -H "Authorization: Bearer $NAGA_API_KEY" | jq -r .status)
  if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then break; fi
  sleep 10
done

if [ "$STATUS" = "completed" ]; then
  curl -L -o boat.mp4 https://api.naga.ac/v1/videos/$JOB_ID/content \
    -H "Authorization: Bearer $NAGA_API_KEY"
fi
```

The first response is the queued job. Keep `id`, or poll the `polling_url` as the Python and Node.js examples do:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-dimmed"}}
{
  "id": "70418329650183274950618327495061",
  "object": "video.generation",
  "model": "kling-v3.0-pro",
  "status": "pending",
  "polling_url": "https://api.naga.ac/v1/videos/70418329650183274950618327495061",
  "created_at": 1774000000,
  "duration": 8,
  "resolution": "720p",
  "aspect_ratio": "16:9"
}
```

When the job completes, the same object gains `completed_at`, `urls` with the download link, and `usage` with the charge:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-dimmed"}}
{
  "id": "70418329650183274950618327495061",
  "object": "video.generation",
  "model": "kling-v3.0-pro",
  "status": "completed",
  "polling_url": "https://api.naga.ac/v1/videos/70418329650183274950618327495061",
  "created_at": 1774000000,
  "completed_at": 1774000095,
  "duration": 8,
  "resolution": "720p",
  "aspect_ratio": "16:9",
  "urls": ["https://api.naga.ac/v1/videos/70418329650183274950618327495061/content"],
  "usage": {"cost": "0.896", "seconds": 8}
}
```

## Choose the clip settings

NagaAI checks every value against the `parameters` of the model's `video_generation` operation in `GET /v1/models` before the job starts. A value the model does not support fails with `400` and a message that lists the supported ones.

| Field | Notes |
| - | - |
| `model` | A model id from `GET /v1/models` whose `operations` include `video_generation` |
| `prompt` | 1 to 8,192 characters |
| `duration` | Seconds, one of the `enum` values of the `duration` parameter. Without it, the model's default |
| `resolution` | One of the `enum` values of `resolution`, for example `720p`. Without it, the model's default |
| `aspect_ratio` | One of the `enum` values of `aspect_ratio`, for example `16:9`. Without it, the model's default |
| `frame_images` | Images that pin the first or the last frame: `{"type": "image_url", "image_url": {"url": "..."}, "frame_type": "first_frame"}`. The URL is `https` or a `data:image/` URL. `last_frame` needs a `first_frame` beside it. The model accepts the `frame_type` values its `frame_images` parameter lists; a model without the parameter takes none |
| `generate_audio` | `true` for a soundtrack, on models that list the `generate_audio` parameter. On such a model without a price for silent video, sound is on by default |
| `seed` | A seed for repeatable output. NagaAI drops it on models that do not list the `seed` parameter |

NagaAI checks the prompt with its own moderation before it starts the job. A blocked prompt fails with `403 inappropriate_content`.

## Read the job status

| `status` | Meaning |
| - | - |
| `pending` | Accepted, waiting for the provider |
| `in_progress` | The provider is generating the clip |
| `completed` | The clip is ready: `urls` holds its download link and `usage` holds the charge |
| `failed` | No clip: `error.type` is `moderation`, `upstream_error` or `timeout` |

`GET /v1/videos/{id}/content` answers `302` with the file's address, or `409` while the job is not `completed`. NagaAI keeps the file for seven days after the job completes, so download it if you need it longer. A job of another account, or an unknown `id`, answers `404`.

## Billing

The price is per second of video and depends on the resolution and on sound. `pricing` in `GET /v1/models` has one `output_video_second` row for each pair the model sells, with the pair in `conditions`: `{"resolution": "720p", "generate_audio": false}`.

* When the job starts, NagaAI holds its full price on the balance. If the balance minus the holds of your running jobs does not cover it, the request fails with `402`. On a key with a credit limit, the rest of the limit has to cover it too, or the request fails with `403 credit_limit_exceeded`.
* NagaAI charges once, when the clip is ready, for the seconds you asked for. For a clip more than half a second shorter, NagaAI charges its real length, rounded up to whole seconds.
* NagaAI does not charge a job that ends `failed`.

## Troubleshooting

### 400 listing supported values

The message names the values the model accepts for the field you sent. Read the `enum` of that parameter in the model's `video_generation` operation in `GET /v1/models` and send one of them.

### 402 or 403 `credit_limit_exceeded` when you start

NagaAI holds the full price of the clip before it starts the job. Top up the balance, or raise the key's credit limit. Running jobs hold their price too, so wait for them to finish if the balance is just short.

### 403 `inappropriate_content`

NagaAI's moderation blocked the prompt. Rewrite it and send the request again.

### 409 on the content URL

The job is not `completed` yet. Keep polling `GET /v1/videos/{id}` and download the file after the status changes.

### Status `failed`

The job has no clip and costs nothing. `error.type` says why. `moderation` means the provider's filter rejected the prompt or a frame image. `timeout` means the clip took too long. `upstream_error` means the provider could not produce it, so submit the job again or try another model.

### 404 on a job

The `id` is unknown, or the job belongs to another account.


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