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

# Video generation

> Generate videos from a text prompt with an asynchronous task API, billed by the output tokens generated.

Videos take minutes to generate, so they run as **tasks**: you submit a task, get its ID back at once, and poll the task until it finishes. The endpoints follow the task API used by Seedance-compatible SDKs:

* `POST https://api.omnirelay.ai/api/v3/contents/generations/tasks` submits a task.
* `GET https://api.omnirelay.ai/api/v3/contents/generations/tasks/{id}` reads it.

Use your OmniRelay API key as a Bearer token. The same key rules apply as for text models: key expiry, [IP allowlist](https://omnirelay.ai/docs/overview/rate-limits.md#key-ip-allowlist), [model limits](https://omnirelay.ai/docs/overview/authentication.md#limit-a-key-to-some-models), budgets and [rate and concurrency limits](https://omnirelay.ai/docs/overview/rate-limits.md). A running task counts as one call in flight. You can also try video models in the console playground.

## Models

| Model ID | Input | Resolutions | Duration |
| - | - | - | - |
| `doubao-seedance-2-0-260128` | Text prompt | `480p`, `720p` | 4–10 seconds |

Video models appear in your own [`GET /v1/models`](https://omnirelay.ai/docs/llms/listing-models#video-models) list with `type: "video"` and the price for your workspace.

## Pricing

Videos are billed by the **output tokens generated**, at a list price of **USD 25 per million output tokens**. The token count follows the frame size and length:

```
output tokens = width × height × (24 × seconds + 1) / 1024
```

A 5-second 480p clip in 16:9 (864 × 496) is 50,638 tokens, about **USD 1.27**. A 10-second 720p clip costs about USD 5.2–5.7 depending on the aspect ratio. Your discount applies as for text models.

When you submit, OmniRelay holds the cost of the largest frame at the requested resolution and duration, plus 10%. When the task succeeds, you pay for the tokens actually generated and the rest of the hold returns to your balance. **Failed, expired and cancelled tasks are not charged.** The task response reports the charge in `cost_microusd`. See [Usage and billing](https://omnirelay.ai/docs/general/usage-billing.md#video-models).

## Submit a task

```bash cURL theme={null}
curl https://api.omnirelay.ai/api/v3/contents/generations/tasks \
  -H "Authorization: Bearer $OMNIRELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "doubao-seedance-2-0-260128",
    "content": [{"type": "text", "text": "A paper boat drifting on a calm pond at sunrise, slow camera pan"}],
    "resolution": "480p",
    "ratio": "16:9",
    "duration": 5
  }'
```

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

BASE = "https://api.omnirelay.ai/api/v3/contents/generations/tasks"
HEADERS = {"Authorization": f"Bearer {os.environ['OMNIRELAY_API_KEY']}"}

task = requests.post(BASE, headers=HEADERS, json={
    "model": "doubao-seedance-2-0-260128",
    "content": [{"type": "text", "text": "A paper boat drifting on a calm pond at sunrise, slow camera pan"}],
    "resolution": "480p",
    "ratio": "16:9",
    "duration": 5,
}).json()

while True:
    result = requests.get(f"{BASE}/{task['id']}", headers=HEADERS).json()
    if result["status"] in ("succeeded", "failed", "expired", "cancelled"):
        break
    time.sleep(5)

print(result["status"], result.get("content", {}).get("video_url"))
```

```javascript JavaScript theme={null}
const BASE = "https://api.omnirelay.ai/api/v3/contents/generations/tasks";
const headers = { Authorization: `Bearer ${process.env.OMNIRELAY_API_KEY}`, "Content-Type": "application/json" };

const task = await (await fetch(BASE, {
  method: "POST",
  headers,
  body: JSON.stringify({
    model: "doubao-seedance-2-0-260128",
    content: [{ type: "text", text: "A paper boat drifting on a calm pond at sunrise, slow camera pan" }],
    resolution: "480p",
    ratio: "16:9",
    duration: 5,
  }),
})).json();

let result;
do {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  result = await (await fetch(`${BASE}/${task.id}`, { headers })).json();
} while (!["succeeded", "failed", "expired", "cancelled"].includes(result.status));

console.log(result.status, result.content?.video_url);
```

The submit response is `{"id": "<task id>"}`. Keep the ID: it is also the request ID in your usage log.

## Parameters

Only these fields are accepted. Any other field is refused with `400 invalid_parameter`, because every accepted field can change the price.

| Field | Required | Values |
| - | - | - |
| `model` | Yes | A video model ID |
| `content` | Yes | Exactly one item, `{"type": "text", "text": "..."}`, 1–4000 characters |
| `resolution` | No | `480p` (default) or `720p` |
| `ratio` | No | `16:9` (default), `4:3`, `1:1`, `3:4`, `9:16`, `21:9`, `adaptive` |
| `duration` | No | Whole seconds, 4–10 (default 5) |
| `seed` | No | Integer, −1 to 4294967295 |
| `generate_audio` | No | Boolean; the clip has a soundtrack unless set to `false` |
| `watermark`, `camera_fixed` | No | Boolean |

Image-to-video (a first or last frame) is not available yet.

## Read a task

`GET /api/v3/contents/generations/tasks/{id}` returns the task. Only the user who created the task can read it; any other ID returns `404`.

```json theme={null}
{
  "id": "3f0c9a2e-6b1d-4c35-9a7e-2d8c5b1e4f60",
  "model": "doubao-seedance-2-0-260128",
  "status": "succeeded",
  "created_at": 1791994620,
  "updated_at": 1791994785,
  "content": { "video_url": "https://…/video.mp4?…" },
  "duration": 5,
  "ratio": "16:9",
  "resolution": "480p",
  "framespersecond": 24,
  "seed": 9846,
  "generate_audio": true,
  "usage": { "completion_tokens": 50638, "total_tokens": 50638 },
  "cost_microusd": 1265950
}
```

| `status` | Meaning |
| - | - |
| `queued`, `running` | Still generating. Poll again in a few seconds; most tasks finish in 2–4 minutes. |
| `succeeded` | Done. `content.video_url` is an MP4 link and `cost_microusd` is the charge. |
| `failed` | Not generated, for example blocked by content moderation. `error.code` and `error.message` say why. Not charged. |
| `expired`, `cancelled` | Not generated. Not charged. |

> [!WARNING]
>
> `content.video_url` is a signed link that **expires 48 hours after the video is generated**. Download the file if you need to keep it; OmniRelay does not store a copy.

You do not have to poll for billing to complete: if nobody reads a task, OmniRelay still settles it when it finishes.

## Failures and charges

| Situation | Response | Charge |
| - | - | - |
| Invalid field or value | `400` with `error.param` | None; nothing sent |
| Balance, key budget or member budget too small for the hold | `402` | None; nothing sent |
| Too many calls in flight or per minute | `429` | None |
| The task could not be confirmed when submitting (timeout, dropped connection, provider error) | `502`/`504` with `video_submit_held` | Held for review; charged only if the video was generated |
| The task finishes as `failed`, `expired` or `cancelled` | `200` with that status | None; the hold is released |

Contact support with the task ID before resubmitting a held task, so the same video is not paid for twice.

## Limits

* Text-to-video only, one text prompt per task.
* Resolution up to 720p, duration up to 10 seconds.
* Request body up to 64 KiB.


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