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

# Video API

> Generate Seedance videos, poll async jobs and retry safely

Orvado generates video with Seedance 2.5 and Seedance 2.0. A video takes much longer than an image, so we recommend the asynchronous flow: create a job, poll it, then download the MP4.

| Endpoint | Use it to |
| - | - |
| `POST /v1/async/videos` | Create a video job. Returns at once with a job ID. Recommended |
| `GET /v1/async/videos/{id}` | Check a job and get the video link |
| `POST /v1/videos/generations` | Create a video and wait for it in one call |

## Create a job

```bash theme={null}
curl https://orvado.dev/v1/async/videos \
  -H "Authorization: Bearer $ORVADO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-video" \
  -d '{
    "model": "seedance-2.5",
    "prompt": "slow dolly shot of a perfume bottle on plum velvet, soft studio light",
    "duration": 5,
    "resolution": "720p",
    "aspect_ratio": "9:16"
  }'
```

The answer comes back with status `202`:

```json theme={null}
{
  "id": "at_...",
  "object": "video.task",
  "model": "seedance-2.5",
  "status": "queued",
  "created_at": 1792560000,
  "billing_status": "held",
  "result": null,
  "error": null
}
```

### Request fields

| Field | Required | Notes |
| - | - | - |
| `model` | Yes | `seedance-2.5` or `seedance-2.0` |
| `prompt` | Yes | What happens in the video |
| `duration` | No | Whole seconds, default `5`. Seedance 2.5: 4 to 30. Seedance 2.0: 5 to 15 |
| `resolution` | No | `480p`, `720p` (default), `1080p` or `4k`. `4k` is Seedance 2.0 only |
| `aspect_ratio` | No | `16:9` (default), `9:16`, `1:1`, `4:3`, `3:4` or `21:9` |

## Poll the job

```bash theme={null}
curl https://orvado.dev/v1/async/videos/JOB_ID \
  -H "Authorization: Bearer $ORVADO_KEY"
```

Check every 5 to 10 seconds until `status` is final:

| `status` | What to do |
| - | - |
| `queued`, `running` | Wait and poll again |
| `completed` | Download `result.videos[0].url` |
| `failed`, `cancelled` | Not charged: `billing_status` is `refunded`. Read `error.code`, change the prompt or settings and create a new job with a new `Idempotency-Key` |
| anything else | Keep polling the same ID. Do not create a new job |

A finished job:

```json theme={null}
{
  "id": "at_...",
  "status": "completed",
  "billing_status": "settled",
  "result": {
    "videos": [{ "url": "https://..." }]
  },
  "error": null
}
```

### When the model refuses a prompt

The model can refuse a prompt, for example when it detects copyrighted audio or other restricted content. The job ends with status `failed`, `billing_status` `refunded` and an `error.code` such as `upstream_generation_failed`. You are not charged. Change the prompt and create a new job with a new `Idempotency-Key`. Reusing the old key returns the same failed job.

> [!WARNING]
>
> Video links stay available for at least 24 hours. Download the MP4 as soon as the job completes and keep it in your own storage.

## Retry safely with Idempotency-Key

Send an `Idempotency-Key` header with every create request, for example your own order ID. If the connection drops or you are not sure the job was created, send the same request with the same key. You get the same job back and are not charged again.

Use a new key only for a new video. Reusing a key with a different body returns `409 idempotency_conflict`. Keys are up to 256 characters, without leading or trailing spaces. A key works across both create endpoints: a job started with `POST /v1/videos/generations` can be picked up with `POST /v1/async/videos` and the same key.

## Wait in one call

`POST /v1/videos/generations` takes the same body and headers, waits for the video and returns:

```json theme={null}
{
  "created": 1792560000,
  "data": [{ "url": "https://..." }]
}
```

The call waits up to 600 seconds. If the video is still not ready, it returns `504` with the job ID:

```json theme={null}
{
  "error": {
    "code": "async_video_wait_timeout",
    "message": "The task is still tracked. Retrieve its status or retry with the same Idempotency-Key.",
    "task_id": "at_..."
  }
}
```

Then poll `GET /v1/async/videos/{task_id}`, or repeat the call with the same `Idempotency-Key`. Do not send it without the key, or you start a second paid video. Set your client timeout to at least 620 seconds. For production, the async flow is still the better choice: no long open connections, and you control the polling.

## Price and billing

The price is per second of video, by model and resolution. See [Models and pricing](/models-and-pricing#video-models).

* The full price, per-second rate times `duration`, is held from your balance when the job is created.
* If the job fails, the hold is returned in full. `billing_status` shows `held`, then `settled` or `refunded`.
* Example: Seedance 2.5, 720p, 5 seconds costs 5 × $0.305 = $1.525.

## Video errors

Video errors use this shape: `{"error": {"code", "message", "task_id"}}`.

| Status | `code` | What to do |
| - | - | - |
| `400` | `invalid_async_video_request` | Fix the body: model, duration, resolution or aspect ratio |
| `403` | `insufficient_quota` | Top up, or raise the key limit in **Tokens** |
| `403` | `model_not_allowed` | This key cannot use the model. Change the key settings in **Tokens** |
| `404` | `async_task_not_found` | Check the job ID. Jobs are visible only to the account that created them |
| `409` | `idempotency_conflict` | The key was used for a different request. Use a new key for a new video |
| `502` | `video_generation_failed` | Not charged. The model refused or failed the job. Change the prompt and create a new job with a new key |
| `504` | `async_video_wait_timeout` | The job is still running. Poll `task_id` |


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