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

# Video

> Generate video over REST — a queued job you read back, with the finished file on it.

A video renders for minutes, so `POST /v1/media/videos` does not wait: it answers `202` with a **media job** in `queued`, and you read the job back until it is `completed` or `failed`. The finished clip is a [file](https://vetta.sh/docs/api/files.md). Images are the synchronous case — see [Images](https://vetta.sh/docs/api/images.md). The same job is what an agent's [`generate_video`](#in-a-session) tool submits; the [catalogue](https://vetta.sh/docs/api/media.md) is shared, and it is its own — a video model is never an agent's [`model`](https://vetta.sh/docs/concepts/model-router).

## The media job object

**id** `string`

`med_…`.

**object** `string`

Always `media_job`.

**kind** `string`

`video`. Other async media kinds share the object and the routes below.

**status** `string`

`queued` → `processing` → `completed` | `failed`.

**model** `string | null`

The model rendering it.

**input** `object`

The request as accepted.

**result** `object | null`

On `completed`: `{ files: [{ file_id, content_type, width, height, duration_seconds }] }`. Dimensions are `null` where the model did not report them.

**error** `object | null`

On `failed`: `{ code, message }` — the reason, safe to repeat, so a refusal and an outage read differently.

**cost\_micro\_usd** `integer | null`

What was debited, in integer micro-USD, once the job finished: `null` while it runs, `0` on a failure — a job that fails is not billed.

**session\_id** `string | null`

The session whose agent submitted it; `null` for a job submitted through this API.

**created\_at** `string`

Submission time.

**completed\_at** `string | null`

When it reached a terminal status.

## Generate a video

`POST /v1/media/videos` → `202 Accepted` — scope `agents:write`, [`Idempotency-Key`](https://vetta.sh/docs/api/overview#idempotency) **required**.

**model** `string`

A model id from [`GET /v1/media/models?kind=video`](https://vetta.sh/docs/api/media#search-models). Omitted, the first of the platform's video defaults the catalogue publishes — `black-forest-labs/flux-3-max`, `minimax/hailuo-3`, `bytedance/seedance-2.5`, then `alibaba/wan-3.0/text-to-video`; for an edit (`video_url`), only one the catalogue says takes a video. Otherwise the request is refused and you name one.

**prompt** `string` *required*

What to render.

**image\_urls** `string[]`

One image, used as the **first frame** the video animates from: a stored image's `fil_` id — say a still [rendered as a character](https://vetta.sh/docs/api/characters.md#rendering-as-a-character) — or a public `http(s)` URL. A `fil_` this organization does not hold is `not_found`; one that is not an image, a private or loopback host, and a `data:` or `file:` URL are `validation_failed`. The job's `input` keeps the `fil_` you sent.

**character** `string`

Put a trained [character](https://vetta.sh/docs/api/characters.md) on video, by `chr_` id or name. It must be `ready`, and `image_urls` must be one `fil_` still this organization rendered as that character — anything else is `validation_failed` — because the face on screen is the still's. Only a model whose catalogue entry says it animates a still is used; a named one that does not is `validation_failed`. The character is recorded on the job, never sent to the model.

**seconds** `integer`

Target duration, 1–60. Models honour it as they can — each renders a set of lengths of its own, and clamps to what it supports.

**aspect\_ratio** `string`

Such as `16:9` or `9:16`. On a model that publishes the ratios it takes, one it does not is clamped to the nearest it does.

**seed** `integer`

Repeat an earlier result, on models that honour a seed.

**The default model, `black-forest-labs/flux-3-max`,** renders 5 to 20 seconds (a shorter or longer `seconds` is clamped into that), at `21:9`, `2:1`, `16:9`, `4:3`, `1:1`, `3:4` or `9:16` (with a first frame and no `aspect_ratio`, the frame's own shape), with **audio generated with the picture** — speech written into the prompt is spoken. It is priced per generated second at 720p: **$0.0425 per second until 7 November 2026 (00:00 UTC), then $0.17 per second**, so a 5-second clip costs about $0.21, then $0.85. The price is the one in force when the job is submitted.

That is the whole request. A model priced per generated second renders at 720p. Not on it today: a `resolution` or `size`, a **last** frame, style or subject reference images beyond the first frame, an audio on/off switch, a completion callback URL, and any per-model or per-provider passthrough. A body that carries one of them is `validation_failed`, not quietly dropped. Completion is read by polling the job or, for an agent's job, from the session's events; a job submitted here does not fire a [webhook](https://vetta.sh/docs/api/webhooks.md) — [clip jobs](https://vetta.sh/docs/api/clips.md) do, video jobs do not yet.

```bash cURL theme={"system"}
curl -fsSL https://api.vetta.sh/v1/media/videos \
  -H "authorization: Bearer sk_live_..." \
  -H "idempotency-key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{ "model": "example/reel", "prompt": "waves at dusk, slow pan", "seconds": 4 }'
```

```ts SDK theme={"system"}
let job = await client.media.videos.generate({ model: "example/reel", prompt: "waves at dusk", seconds: 4 });
while (job.status === "queued" || job.status === "processing") {
  await new Promise((r) => setTimeout(r, 5_000));
  job = await client.media.get(job.id);
}
```

```bash CLI theme={"system"}
vetta media generate --kind video --model example/reel --prompt "waves at dusk" --seconds 4
vetta media show med_01J...
```

```json 202 Accepted theme={"system"}
{
  "id": "med_01J...",
  "object": "media_job",
  "kind": "video",
  "status": "queued",
  "model": "example/reel",
  "input": { "model": "example/reel", "prompt": "waves at dusk, slow pan", "seconds": 4 },
  "result": null,
  "error": null,
  "cost_micro_usd": null,
  "session_id": null,
  "created_at": "2026-09-06T12:00:00.000Z",
  "completed_at": null
}
```

## Retrieve a job

`GET /v1/media/{id}` → `200 OK` — scope `agents:read`. Poll it; a job left rendering for over thirty minutes is failed and not billed. Any `med_` reads here, a [clip job](https://vetta.sh/docs/api/clips.md) included — the same object `GET /v1/media/clips/{id}` answers.

```json Response theme={"system"}
{
  "id": "med_01J...",
  "object": "media_job",
  "kind": "video",
  "status": "completed",
  "model": "example/reel",
  "input": { "model": "example/reel", "prompt": "waves at dusk, slow pan", "seconds": 4 },
  "result": {
    "files": [
      { "file_id": "fil_01J...", "content_type": "video/mp4", "width": null, "height": null, "duration_seconds": null }
    ]
  },
  "error": null,
  "cost_micro_usd": 412000,
  "session_id": null,
  "created_at": "2026-09-06T12:00:00.000Z",
  "completed_at": "2026-09-06T12:03:41.000Z"
}
```

Read the clip with [`GET /v1/files/{file_id}?download=true`](https://vetta.sh/docs/api/files#retrieve-a-file). The file carries `source.type = video_generation` and `source.job_id` = this job.

A job an agent submitted with `generate_video` carries its `session_id`, and that session's [event stream](https://vetta.sh/docs/api/events.md#event-types) announces it: `media.job.queued`, then `media.job.completed` or `media.job.failed`. A job submitted here has no session, so poll it.

## List jobs

`GET /v1/media` → `200 OK`, cursor-paginated — scope `agents:read`. See [Pagination](https://vetta.sh/docs/api/pagination.md).

**kind** `string`

`video` (or another async kind).

**status** `string`

`queued`, `processing`, `completed` or `failed`.

**session\_id** `string`

Jobs an agent submitted from one session.

**limit** `number`

Page size, 1–100.

**after** `string`

The `next_cursor` of the previous page.

## Billing

A job is billed **what it cost**, on the media tier, when it completes. Nothing is held beforehand, because the price is only known once the model accepts the job; instead, a submit on an empty balance is refused with `insufficient_credits` (402) before it reaches a model. On a model priced per generated second, that is the clip's length at the resolution it rendered; on one that reports a cost, what it reported. The debit settles under the job id at completion, exactly once, and the request's idempotency key reaches the model's provider where it takes one, so a retried submit is not rendered twice — `black-forest-labs/flux-3-max`'s takes none, so there only this API's own replay (the same `Idempotency-Key`) guards against a second render. A job that fails, is refused, or is abandoned is not billed.

## In a session

An agent submits with the [`generate_video`](https://vetta.sh/docs/capabilities/tools#generation-tools) tool — the same job, the same catalogue, the same fields (`model?`, `prompt`, `image_urls?`, `character?`, `seconds?`, `aspect_ratio?`, `seed?`). The tool **does not block**: it returns the job's handle in the same turn and the agent carries on; when the job finishes, the file is saved and the session is woken with its `file_id`. Three differences from calling the route yourself:

* **`model` may be left to the agent's configuration.** `tools.configs.generate_video.config.models` on the agent names the model that runs when the agent does not choose one. It is a default, not an allow-list; with none pinned and none named, the platform's video default runs, as here.
* **Completion is an event, not a poll.** The session's [event stream](https://vetta.sh/docs/api/events.md#event-types) announces `media.job.queued`, then `media.job.completed` or `media.job.failed`; the job also reads back here with its `session_id` set.
* **The bill lands on the agent.** The debit carries the `session_id` and settles against the agent's [budget](https://vetta.sh/docs/concepts/budgets.md) as the `media` component; a job through this route carries no session and settles against the organization.

## Errors

| Code | When |
| - | - |
| `validation_failed` | Missing `Idempotency-Key`, an unknown `model`, a private-host `image_urls` entry or a stored file that is not an image, more than one of them, a `character` without a still rendered as it or on a model that animates no still, `seconds` outside 1–60, a field the request does not carry. |
| `provider_error` | The model would not take the job. Nothing is queued or billed. The model's reason follows the status when it can be repeated (`the media provider answered 400: …`). |
| `provider_error`, model busy or unavailable | The model is busy or isn't available right now, and the message says which. Nothing is queued or billed, and no other model is tried: wait and retry, or name another `model`. |
| `not_found` | No such `med_`, or an `image_urls` `fil_`, in this organization. |
| `feature_not_configured` | This deploy has no generation provider or no persistence configured. |


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