---
title: "Video API"
description: "Text to video: live video models, create, poll and download, per-second pricing, holds and limits."
canonical_url: "https://minirouter.sh/docs/videos"
markdown_url: "https://minirouter.sh/docs/videos.md"
last_updated: "2026-10-29"
---

# Video API

Turn a text prompt into an MP4: create a video, poll it, then download it.

## Generate a video

### 1. Create a video

Set `MINIROUTER_API_KEY` to a funded [API key](https://minirouter.sh/key). This starts a 4-second clip, holds credit and saves its `id`.

```sh
curl --fail-with-body --retry 0 https://api.minirouter.sh/v1/videos \
  -H "Authorization: Bearer $MINIROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "model": "google/veo-3.1-lite-generate-001",
  "prompt": "A paper boat drifting down a rainy street at dusk",
  "seconds": 4,
  "size": "1280x720"
}' | tee video.json
VIDEO_ID=$(jq -r .id video.json)
```

### 2. Poll until it finishes

Repeat every 10 seconds until `status` is `completed` or `failed`.

```sh
curl --fail-with-body https://api.minirouter.sh/v1/videos/$VIDEO_ID \
  -H "Authorization: Bearer $MINIROUTER_API_KEY"
```

### 3. Download the MP4

Downloads are free for 24 hours after completion.

```sh
curl --fail-with-body https://api.minirouter.sh/v1/videos/$VIDEO_ID/content \
  -H "Authorization: Bearer $MINIROUTER_API_KEY" \
  --output video.mp4
```

## Use the OpenAI SDK

Point the client at MiniRouter and turn off retries: a retried create starts a second video.

**Node.js**

```ts
// npm install openai@7.4.0
import OpenAI from 'openai';
import { writeFile } from 'node:fs/promises';
import { setTimeout as sleep } from 'node:timers/promises';

const client = new OpenAI({
  baseURL: 'https://api.minirouter.sh/v1',
  apiKey: process.env.MINIROUTER_API_KEY,
  maxRetries: 0,
});
let video = await client.videos.create({
  model: 'google/veo-3.1-lite-generate-001',
  prompt: 'A paper boat drifting down a rainy street at dusk',
  seconds: '4', size: '1280x720',
});
while (video.status === 'queued' || video.status === 'in_progress') {
  await sleep(10_000);
  video = await client.videos.retrieve(video.id);
}
if (video.status === 'failed') throw new Error(video.error?.message);
const content = await client.videos.downloadContent(video.id);
await writeFile('video.mp4', Buffer.from(await content.arrayBuffer()));
```

**Python**

```python
# pip install openai==2.53.0
import os
from openai import OpenAI

client = OpenAI(
    base_url='https://api.minirouter.sh/v1',
    api_key=os.environ['MINIROUTER_API_KEY'],
    max_retries=0,
)
video = client.videos.create_and_poll(
    model='google/veo-3.1-lite-generate-001',
    prompt='A paper boat drifting down a rainy street at dusk',
    seconds='4',
    size='1280x720',
    poll_interval_ms=10_000,
)
if video.status == 'failed':
    raise RuntimeError(video.error.message)
client.videos.download_content(video.id).write_to_file('video.mp4')
```

Only create, retrieve and download are supported. Values from `video.controls` work even where the SDK types omit them; TypeScript needs a cast.

## Choose a model

Text-to-video models in the live catalog. Open a model for its full price table, or copy its ID.

### Alibaba
| Model ID | Sizes, durations and price |
| --- | --- |
| [`alibaba/wan-v2.5-t2v-preview`](https://minirouter.sh/models/alibaba/wan-v2.5-t2v-preview) | 480p, 720p, 1080p · 5 or 10 seconds · from $0.0525/s |
| [`alibaba/wan-v2.6-t2v`](https://minirouter.sh/models/alibaba/wan-v2.6-t2v) | 720p, 1080p · 2–15 seconds · from $0.105/s |
| [`alibaba/wan-v2.7-t2v`](https://minirouter.sh/models/alibaba/wan-v2.7-t2v) | 720p, 1080p · 2–15 seconds · from $0.105/s |

### ByteDance
| Model ID | Sizes, durations and price |
| --- | --- |
| [`bytedance/seedance-v1.0-pro`](https://minirouter.sh/models/bytedance/seedance-v1.0-pro) | 480p, 720p, 1080p · 2–12 seconds · from $0.025515/s |
| [`bytedance/seedance-v1.0-pro-fast`](https://minirouter.sh/models/bytedance/seedance-v1.0-pro-fast) | 480p, 720p, 1080p · 2–12 seconds · from $0.010185/s |
| [`bytedance/seedance-v1.5-pro`](https://minirouter.sh/models/bytedance/seedance-v1.5-pro) | 480p, 720p, 1080p · 4–12 seconds · audio · from $0.012705/s |

### Google
| Model ID | Sizes, durations and price |
| --- | --- |
| [`google/veo-3.1-generate-001`](https://minirouter.sh/models/google/veo-3.1-generate-001) | 720p, 1080p, 4K · 4, 6 or 8 seconds · audio · from $0.21/s |
| [`google/veo-3.1-fast-generate-001`](https://minirouter.sh/models/google/veo-3.1-fast-generate-001) | 720p, 1080p, 4K · 4, 6 or 8 seconds · audio · from $0.105/s |
| [`google/veo-3.1-lite-generate-001`](https://minirouter.sh/models/google/veo-3.1-lite-generate-001) | 720p, 1080p · 4, 6 or 8 seconds · audio · from $0.0315/s |

### Kling AI
| Model ID | Sizes, durations and price |
| --- | --- |
| [`klingai/kling-v2.5-turbo-t2v`](https://minirouter.sh/models/klingai/kling-v2.5-turbo-t2v) | 720p, 1080p · 5 or 10 seconds · from $0.0441/s |
| [`klingai/kling-v2.6-t2v`](https://minirouter.sh/models/klingai/kling-v2.6-t2v) | 720p, 1080p · 5 or 10 seconds · audio · from $0.0441/s |
| [`klingai/kling-v3.0-t2v`](https://minirouter.sh/models/klingai/kling-v3.0-t2v) | 720p, 1080p · 3–15 seconds · audio · from $0.1764/s |

### MiniMax
| Model ID | Sizes, durations and price |
| --- | --- |
| [`minimax/minimax-h3`](https://minirouter.sh/models/minimax/minimax-h3) | 768p, 2K · 4–15 seconds · from $0.084/s |
| [`minimax/minimax-h3-max`](https://minirouter.sh/models/minimax/minimax-h3-max) | 480p, 768p · 5–15 seconds · from $0.0525/s |

### xAI
| Model ID | Sizes, durations and price |
| --- | --- |
| [`spacexai/grok-imagine-video`](https://minirouter.sh/models/spacexai/grok-imagine-video) | 480p, 720p · 1–15 seconds · from $0.0525/s |
| [`spacexai/grok-imagine-video-1.5`](https://minirouter.sh/models/spacexai/grok-imagine-video-1.5) | 480p, 720p, 1080p · 1–15 seconds · from $0.084/s |
| [`spacexai/grok-imagine-video-1.5-lite`](https://minirouter.sh/models/spacexai/grok-imagine-video-1.5-lite) | 480p, 720p, 1080p · 1–15 seconds · from $0.021/s |

### List models in code

Video models are the `GET /v1/models` entries with `"type": "video"`.

```sh
curl -s https://api.minirouter.sh/v1/models | jq '.data[]
  | select(.type == "video")
  | {id, controls: .video.controls, prices: .video.pricing.prices}'
```

| Field | Meaning |
| --- | --- |
| `video.controls.sizes` | Accepted `size` values, each with a price `tier` and `aspect_ratio`. |
| `video.controls.seconds` | Accepted durations. |
| `video.controls.audio` | `true` if the model can add sound. |
| `video.controls.max_prompt_chars` | Longest accepted prompt. |
| `video.pricing.prices` | `usd_per_second` by `tier` and `audio`. |

## Request body

| Field | Rule |
| --- | --- |
| `model` | Required. A video model `id`. |
| `prompt` | Required. Up to `max_prompt_chars` characters. |
| `seconds` | Required. One of `video.controls.seconds`, as a number or string. |
| `size` | Required. One of `video.controls.sizes`, such as `1280x720`. |
| `audio` | Optional, default `false`. `true` adds sound where the model supports it, at the audio price. |

Send JSON or multipart fields, up to 256 KiB. Video is text to video only: `image_url`, file parts and any other field return 400.

## Read the status

Create and poll return the video object. `cost` is a MiniRouter extension.

```json
{
  "id": "req_5f2c9e8a1b7d4c3e9f6a0b2d8c4e1f7a",
  "object": "video",
  "model": "google/veo-3.1-lite-generate-001",
  "status": "in_progress",
  "progress": 50,
  "created_at": 1793145600,
  "completed_at": null,
  "expires_at": null,
  "seconds": "4",
  "size": "1280x720",
  "error": null,
  "cost": {
    "status": "pending",
    "usd": null
  }
}
```

### Video status

| status | Meaning |
| --- | --- |
| `queued` | Accepted, not started yet. |
| `in_progress` | Generating. `progress` is 0 while queued, 50 while generating and 100 when completed. |
| `completed` | Ready to download until `expires_at`. |
| `failed` | `error` says why. Nothing is charged. |

### Cost status

| cost.status | Meaning |
| --- | --- |
| `pending` | Credit is held. `usd` is `null`. A completed video stays pending until the provider reports its cost. |
| `settled` | `usd` is the final charge. |
| `released` | The hold was returned. `usd` is `"0"`. |

## Pricing and holds

Prices are per second of output, by size tier and `audio`, and include MiniRouter's 5% fee.

1. **Hold**: At create: (`seconds` + 1) × the per-second price. The extra second covers clips that run long. The minimum hold is $0.00002.
2. **Generate**: A failed or cancelled video releases the hold.
3. **Settle**: What the provider bills, at the listed price and never more than the hold. The rest returns.

For example, 4 seconds at $0.0315 a second holds $0.1575. If the provider bills 4 seconds, you pay $0.126 and $0.0315 returns.

Video is paid from credit; provider keys (BYOK) are not used. Key spend limits apply to the hold.

## Limits

- **24** Hours content is kept after completion
- **2** Videos in progress per account
- **45** Minutes before a running video is cancelled
- **$0** Charged for a failed video

A video counts toward the 2 until its cost settles. Polls and downloads count toward your key's [rate limit](https://minirouter.sh/docs/rate-limits). Only the account that created a video can read it.

## Errors

| Code | Cause |
| --- | --- |
| [`invalid_request`](https://minirouter.sh/errors/invalid_request) | 400. A field is missing, unsupported or not in `video.controls`, or the provider refused the prompt. Nothing is charged. |
| [`insufficient_credits`](https://minirouter.sh/errors/insufficient_credits) | 402. Your balance cannot cover the hold. |
| [`rate_limited`](https://minirouter.sh/errors/rate_limited) | 429. Too many requests on this key, polls included. |
| [`videos_unavailable`](https://minirouter.sh/errors/videos_unavailable) | 503. Video is paused, or your account already has 2 videos in progress. Nothing is charged. |
| [`video_capacity`](https://minirouter.sh/errors/video_capacity) | 503. Provider or download capacity is full. Retry after `Retry-After`. |
| [`video_request_failed`](https://minirouter.sh/errors/video_request_failed) | 502. The create was not confirmed and may still run. Its `x-minirouter-request-id` is the video `id`: poll it before creating another. |
| [`video_not_found`](https://minirouter.sh/errors/video_not_found) | 404. No video with this `id` on your account. |
| [`video_not_ready`](https://minirouter.sh/errors/video_not_ready) | 409. Not completed yet. Failed videos have no content. |
| [`video_expired`](https://minirouter.sh/errors/video_expired) | 410. More than 24 hours since completion. |
| [`video_content_unavailable`](https://minirouter.sh/errors/video_content_unavailable) | 502. The download failed. Retry; downloads are free. |

Other codes are in [Errors](https://minirouter.sh/docs/errors).

## Choose your path

- [Billing](https://minirouter.sh/docs/billing): Deposits, refunds and low-balance behavior.
- [Key limits](https://minirouter.sh/docs/rate-limits): Per-key request and spend limits, which also cap holds.
- [All models](https://minirouter.sh/docs/models): Model IDs, types and the catalog endpoint.
