# Video generation

> Asynchronous video generation on MirAPI: submit a task, poll for status, and download the MP4 across the unified, OpenAI/Sora, Kling, and Jimeng routes, billed per second of output.

Video generation is **asynchronous**: a clip takes longer than a chat reply, so instead of blocking, the API returns a task that you submit, poll, and download.

:::tip[Pick the right model]
Video endpoints only accept models with video output. Replace `deepseek-chat` in the examples below with a video model's exact `id` from your catalogue.
:::

## Choose a model

Video models are listed in `GET /v1/models` alongside chat models. Filter for entries whose `output_modalities` includes `video`, and check their `pricing` — video is billed per second of output, not per token. The `vendor/` prefix in the console is for browsing only; it is not part of the model `id` you send to the API.

## The three steps

1. `POST /v1/video/generations` — submit the job and receive a `task_id`.
2. `GET /v1/video/generations/{task_id}` — poll until `status` is `completed` or `failed`.
3. Download the MP4 from the `url` in the completed task.

```python
import os
import time
import requests

BASE = "https://api.mirapi.ai/v1"
headers = {"Authorization": f"Bearer {os.environ['MIRAPI_API_KEY']}"}

# Step 1: submit
resp = requests.post(
    f"{BASE}/video/generations",
    headers=headers,
    json={
        "model": "deepseek-chat",
        "prompt": "A golden retriever playing fetch on a sunny beach",
        "duration": 5,
        "width": 1280,
        "height": 720,
        "fps": 30,
    },
)
resp.raise_for_status()
task_id = resp.json()["task_id"]

# Step 2: poll
while True:
    task = requests.get(f"{BASE}/video/generations/{task_id}", headers=headers).json()
    if task["status"] in ("completed", "failed"):
        break
    time.sleep(10)

if task["status"] == "failed":
    raise SystemExit(f"Video failed: {task.get('error')}")

# Step 3: download
video = requests.get(task["url"])
open("output.mp4", "wb").write(video.content)
```

```bash
# Step 1
curl -X POST https://api.mirapi.ai/v1/video/generations \
  -H "Authorization: Bearer $MIRAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "deepseek-chat", "prompt": "A city skyline at dusk", "duration": 5}'

# Step 2 — repeat until "completed"
curl https://api.mirapi.ai/v1/video/generations/abcd1234efgh \
  -H "Authorization: Bearer $MIRAPI_API_KEY"

# Step 3 — download the URL from the completed task
curl -o output.mp4 "https://…"
```

The completed task's `url` is a temporary link — download it right away, before it expires.

## Request parameters

The unified route takes a JSON body with these fields:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | yes | Video-capable model `id` from the catalogue. |
| `prompt` | string | yes | Text description of the clip. |
| `image` | string | no | First-frame image for image-to-video: HTTP(S) URL or base64 data URI. |
| `duration` | number | no | Target length in seconds. |
| `width` | integer | no | Output width in pixels. |
| `height` | integer | no | Output height in pixels. |
| `fps` | integer | no | Frame rate. |
| `seed` | integer | no | Seed for reproducible output (not guaranteed by every model). |
| `n` | integer | no | Number of videos to generate. |
| `response_format` | string | no | Response format (e.g. `url`). |
| `user` | string | no | End-user identifier for abuse tracking. |
| `metadata` | object | no | Model-specific extras (`negative_prompt`, `style`, `quality_level`, …) where the model accepts them. |

## Task status

`status` is one of `queued`, `in_progress`, `completed`, `failed`. A completed task includes the video `url`, `format` (e.g. `mp4`), and a `metadata` object with the actual `duration`, `width`, `height`, and `fps`; a failed task includes an `error` with a `code` and `message`.

```json
{
  "task_id": "abcd1234efgh",
  "status": "completed",
  "url": "https://…",
  "format": "mp4",
  "metadata": { "duration": 5, "width": 1280, "height": 720 }
}
```

Poll every 10–30 seconds — a job can take minutes, depending on the model and resolution.

## Image-to-video

Pass an optional `image` field (URL or base64 data URI) to control the first frame. The model keeps the composition, so describe the motion in `prompt` rather than re-describing the scene:

```python
resp = requests.post(
    f"{BASE}/video/generations",
    headers=headers,
    json={
        "model": "deepseek-chat",
        "prompt": "The photo comes to life and the person waves",
        "image": "https://example.com/first-frame.png",
        "duration": 5,
    },
)
```

## Endpoint routes

MirAPI exposes four route families for video. They all follow the same submit → poll → download flow, but differ in request format and response shape.

### OpenAI / Sora format

- `POST /v1/videos` — `multipart/form-data` with `model`, `prompt`, `seconds` (a string), and an optional `input_reference` image file. Returns an `id` (not `task_id`).
- `GET /v1/videos/{task_id}` — task status, including `progress` and `created_at`.
- `GET /v1/videos/{task_id}/content` — the finished file, returned as `video/mp4`.

### Unified format

- `POST /v1/video/generations` — JSON body (the request parameters above). Returns `task_id` and `status`.
- `GET /v1/video/generations/{task_id}` — status; a completed task carries `url`, `format`, and `metadata`.

### Kling format

- `POST /kling/v1/videos/text2video` and `POST /kling/v1/videos/image2video` — the same JSON body as the unified route; the image variant takes `image`.
- Poll via `GET /kling/v1/videos/text2video/{task_id}` and `GET /kling/v1/videos/image2video/{task_id}`.

### Jimeng format

- `POST /jimeng/` — the native Jimeng API. `Action` and `Version` are query parameters: `Action` is `CVSync2AsyncSubmitTask` (submit) or `CVSync2AsyncGetResult` (fetch result), and `Version` is the API version (e.g. `2022-08-31`). The JSON body carries `req_key`, `prompt`, and an optional `binary_data_base64` image array.

:::note
Only the OpenAI/Sora route uses multipart; the unified, Kling, and Jimeng routes accept JSON.
:::

## Billing

Video is billed **per second of generated output**, settled when the job completes. Failed jobs produce no output and are not charged.

- Balance exhausted → the request fails with `403`; top up in the console and the same key recovers immediately.
- Reconcile spend in the console billing log, `GET /api/usage/token` (account-level summary), or `GET /api/log/token` (per-request detail).

## Errors and troubleshooting

| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | Invalid parameters | Check the model `id`, and that `duration`/`width`/`height` are numeric. |
| `401` | Missing or invalid key | Verify `Authorization: Bearer`. |
| `403` | Key valid but rejected | Top up (empty balance), or check the model whitelist, quota, and IP allowlist. |
| `404` | Task or content not found | The task does not exist, or the video is not `completed` yet. |
| `429` | Rate limited | Back off exponentially (1s → 2s → 4s, cap ~30s); there is no `Retry-After` header. |
| `500` | Gateway or upstream failure | Retry idempotent requests safely. |

Every error message ends with a request ID — include it in support tickets.

- **Task stays `queued` or `in_progress` for a while** — normal; jobs can take minutes. Keep polling at a steady interval.
- **Generation `failed`** — read the `error` field, confirm the model has video output, and check that any reference image is reachable and in a supported format.
- **`404` on the content endpoint** — the task is not `completed` yet; poll the status endpoint first.

## Related links

- [Text-to-video tutorial](/docs/tutorials/text-to-video) — full submit/poll/download script with timeout and failure branches
- [Image-to-video tutorial](/docs/tutorials/image-to-video) — first-frame control and compatibility routes
- [Models & pricing](/docs/models) — find video-capable models and per-second pricing
- [Billing & top-ups](/docs/billing) — how balance, top-ups, and reconciliation work