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

# Video generation

> Create video from a text prompt with an asynchronous job API — submit, poll, download.

This section is about *creating* video from a text prompt. To send an existing
video to a model for analysis, see [Video input](/capabilities/video-input).

The gateway generates **video** too. Unlike image generation, video is
**asynchronous**: a generation can take from tens of seconds to several
minutes, so you submit a *job* and poll for its result instead of holding one HTTP
request open. Authentication is the same API key as every other route (as
`Authorization: Bearer` or `x-api-key`), and the same per-client
rate limits and spend caps apply.

1. `POST /v1/videos/generations` — submit a job. Answers **202** with a job object containing an `id`.
2. `GET /v1/videos/generations/{id}` — poll the job until its `status` is terminal (`succeeded` or `failed`).

## Quickstart

```bash theme={"dark"}
# 1) Submit — returns 202 with the job id
JOB=$(curl -s https://api.aireserve.com/v1/videos/generations \
  -H "Authorization: Bearer $AUDACITY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "wan-2.6",
    "prompt": "A drone shot over a misty pine forest at sunrise",
    "duration_seconds": 5,
    "size": "1280x720"
  }')
JOB_ID=$(echo "$JOB" | jq -r .id)

# 2) Poll every few seconds until the job is terminal
while :; do
  JOB=$(curl -s "https://api.aireserve.com/v1/videos/generations/$JOB_ID" \
    -H "Authorization: Bearer $AUDACITY_API_KEY")
  STATUS=$(echo "$JOB" | jq -r .status)
  echo "status: $STATUS"
  case "$STATUS" in succeeded|failed) break ;; esac
  sleep 3
done

# 3) Download the finished clip (mp4)
curl -o clip.mp4 "$(echo "$JOB" | jq -r .result.video.url)"
```

## Request parameters

| Field              | Required | Description                                                                                                                                                                                                                                                                                       |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`            | yes      | Video model ID — see the table below                                                                                                                                                                                                                                                              |
| `prompt`           | yes      | Text description of the desired video; 1–32,000 characters                                                                                                                                                                                                                                        |
| `duration_seconds` | no       | Clip length in seconds — each model accepts a specific set of values (see the models table below: `wan-2.6` accepts 5, 10, or 15; `seedance-2.0` accepts integers 4–15). An unsupported value is rejected synchronously with a 400 listing the allowed values. Omit it to use the model's default |
| `size`             | no       | Dimensions as a `"WxH"` string. The gateway maps it to the nearest resolution tier and aspect ratio the model supports — e.g. `1280x720` becomes 720p at 16:9                                                                                                                                     |
| `user`             | no       | End-user identifier forwarded for abuse attribution                                                                                                                                                                                                                                               |

## The job object

Both endpoints return the same shape — `object` is always
`"video.generation.job"` and `status` moves strictly forward:

| Status      | Meaning                                                                 |
| ----------- | ----------------------------------------------------------------------- |
| `queued`    | Accepted, not yet dispatched to the provider                            |
| `running`   | Generating. Jobs advance when you poll — keep polling every few seconds |
| `succeeded` | Terminal — `result` is populated                                        |
| `failed`    | Terminal — `error` carries the reason; the job stays readable by id     |

A finished job looks like this:

```json theme={"dark"}
{
  "id": "3f8a1e2c-9d4b-4c6a-b7e1-2a5c8d9f0e13",
  "object": "video.generation.job",
  "kind": "video",
  "model": "wan-2.6",
  "status": "succeeded",
  "created_at": 1752000000,
  "completed_at": 1752000041,
  "result": {
    "video": {
      "url": "https://…/output.mp4",
      "width": 1280,
      "height": 720,
      "duration": 5
    }
  },
  "error": null
}
```

* On success, `result.video.url` is an MP4 hosted on the provider's CDN — **download promptly and persist on your side**; the URL is not permanent.
* Extra metadata in `result` varies by model (for example `wan-2.6` echoes width/height/duration; `seedance-2.0` omits them).
* On failure, `error` is a human-readable string and `result` stays `null`.

## Models & pricing

| Model          | Resolutions                                   | Durations      | Approximate pricing                                                     |
| -------------- | --------------------------------------------- | -------------- | ----------------------------------------------------------------------- |
| `wan-2.6`      | 720p, 1080p (16:9, 9:16, 1:1, 4:3, 3:4)       | 5, 10, or 15 s | \~$0.10/s at 720p, ~$0.15/s at 1080p                                    |
| `seedance-2.0` | 480p, 720p, 1080p, 4k (16:9, 9:16, 1:1, 21:9) | 4–15 s         | \~$0.14/s at 480p, ~$0.30/s at 720p, \~$0.68/s at 1080p, ~$1.56/s at 4k |

Video is billed **per second of finished video** at the tier actually
generated — a 5-second `wan-2.6` clip at 720p is roughly \$0.50. Only videos that
finish successfully are billed. Each job's cost is metered against your key exactly like
chat and image traffic, appears in your usage reporting, and counts toward the same spend
caps.

## Polling from Python

A production-shaped loop with a hard deadline: poll every few seconds, treat 5xx poll
errors as transient (the job keeps running server-side), and give up after a sensible
timeout. A typical 5-second `wan-2.6` clip completes in under a minute; longer
clips and higher tiers take proportionally longer.

```python theme={"dark"}
import os, time, requests

BASE = "https://api.aireserve.com"
HEADERS = {"Authorization": f"Bearer {os.environ['AUDACITY_API_KEY']}"}

job = requests.post(
    f"{BASE}/v1/videos/generations",
    headers=HEADERS,
    json={
        "model": "wan-2.6",
        "prompt": "A drone shot over a misty pine forest at sunrise",
        "duration_seconds": 5,
        "size": "1280x720",
    },
    timeout=30,
)
job.raise_for_status()
job = job.json()

deadline = time.monotonic() + 600          # give up after 10 minutes
while job["status"] in ("queued", "running"):
    if time.monotonic() > deadline:
        raise TimeoutError(f"video job {job['id']} did not finish in time")
    time.sleep(3)
    resp = requests.get(
        f"{BASE}/v1/videos/generations/{job['id']}", headers=HEADERS, timeout=30
    )
    if resp.status_code >= 500:
        continue                            # transient — retry the next poll
    resp.raise_for_status()
    job = resp.json()

if job["status"] == "succeeded":
    url = job["result"]["video"]["url"]
    with open("clip.mp4", "wb") as f:       # download promptly — URLs expire
        f.write(requests.get(url, timeout=120).content)
else:
    print("generation failed:", job["error"])
```

<Info>
  **SDK note.** The SDKs do not yet ship a video-generation helper — call the
  HTTP endpoints directly as above. Typed helpers in all five languages are coming in an
  upcoming SDK release.
</Info>

## Errors

Submission errors reuse the familiar codes: 400 `invalid_request_error` for a
malformed body, 401 `invalid_api_key`, 402 `usage_cap_exceeded` when
a spend cap is reached, and 429 `rate_limit_exceeded` with a
`Retry-After` header. Three video-specific cases:

* **400 at submit** — `duration_seconds` is not one of the values the model supports (for example `wan-2.6` only accepts 5, 10, or 15). The message lists the allowed values; no job is created.
* **502 at submit** — the provider rejected the job synchronously (for example a prompt rejected by upstream validation). The response carries the provider's message and a `job_id`; the failed job remains readable by id.
* **404 `job_not_found` on poll** — the job id is unknown or belongs to a different workspace's key.

<Note>
  **Content policy.** Upstream video providers apply safety filtering to
  prompts and outputs. A prompt that violates the provider's content policy fails the
  generation — the job lands in `failed` with the provider's message in
  `error` (or, if rejected at submit, in the 502 response).
</Note>
