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

# Videos

> Generate video with POST /v1/videos, poll the job, and download the MP4.

Video generation is a job. `POST https://api.naga.ac/v1/videos` answers `202` at once with a job `id`. A clip takes from half a minute to several minutes, so you poll `GET /v1/videos/{id}` until `status` is `completed` or `failed`, then download the file from `GET /v1/videos/{id}/content`. The request and job fields follow the OpenRouter Videos API.

`GET /v1/videos/models` lists the video models on sale, with the values each accepts and its price per second.

```python Python theme={null}
import os
import time

import requests

BASE = "https://api.naga.ac/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['NAGAAI_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={null}
import { writeFile } from "node:fs/promises";

const BASE = "https://api.naga.ac/v1";
const headers = { Authorization: `Bearer ${process.env.NAGAAI_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={null}
curl https://api.naga.ac/v1/videos \
  -H "Authorization: Bearer $NAGAAI_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"}'

# Repeat until "status" is "completed" or "failed".
curl https://api.naga.ac/v1/videos/$JOB_ID \
  -H "Authorization: Bearer $NAGAAI_API_KEY"

curl -L -o boat.mp4 https://api.naga.ac/v1/videos/$JOB_ID/content \
  -H "Authorization: Bearer $NAGAAI_API_KEY"
```

## Parameters

Every value is checked against the model's entry in `GET /v1/videos/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/videos/models` |
| `prompt` | 1 to 8,192 characters |
| `duration` | Seconds, one of the model's `supported_durations`. Without it, the model's default |
| `resolution` | One of `supported_resolutions`, for example `720p`. Without it, the model's default |
| `aspect_ratio` | One of `supported_aspect_ratios`, 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 |
| `generate_audio` | `true` for a soundtrack, on models whose `generate_audio` is `true`. 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 whose `seed` is `false` |

NagaAI checks prompts with its own moderation first. A blocked prompt fails with `403 inappropriate_content`.

## The job

| `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/videos/models` lists `per_second` for each pair the model sells.

* 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`.


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