> ## 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](/docs/api/files). Images are the synchronous case — see [Images](/docs/api/images).

## The media job object

<ResponseField name="id" type="string">`med_…`.</ResponseField>
<ResponseField name="object" type="string">Always `media_job`.</ResponseField>
<ResponseField name="kind" type="string">`video`. Other async media kinds share the object and the routes below.</ResponseField>
<ResponseField name="status" type="string">`queued` → `processing` → `completed` | `failed`.</ResponseField>
<ResponseField name="model" type="string | null">The model rendering it.</ResponseField>
<ResponseField name="input" type="object">The request as accepted.</ResponseField>
<ResponseField name="result" type="object | null">On `completed`: `{ files: [{ file_id, content_type, width, height, duration_seconds }] }`. Dimensions are `null` where the model did not report them.</ResponseField>
<ResponseField name="error" type="object | null">On `failed`: `{ code, message }` — the reason, safe to repeat, so a refusal and an outage read differently.</ResponseField>
<ResponseField name="cost_micro_usd" type="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.</ResponseField>
<ResponseField name="session_id" type="string | null">The session whose agent submitted it; `null` for a job submitted through this API.</ResponseField>
<ResponseField name="created_at" type="string">Submission time.</ResponseField>
<ResponseField name="completed_at" type="string | null">When it reached a terminal status.</ResponseField>

## Generate a video

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

<ParamField body="model" type="string" required>A model id from [`GET /v1/media/models?kind=video`](/docs/api/media#search-models). Video models publish no price, so there is no default to pick for you.</ParamField>
<ParamField body="prompt" type="string" required>What to render.</ParamField>
<ParamField body="image_urls" type="string[]">One public `http(s)` image to animate from. A private or loopback host is `validation_failed`.</ParamField>
<ParamField body="seconds" type="integer">Target duration, 1–60. Models honour it as they can.</ParamField>
<ParamField body="aspect_ratio" type="string">Such as `16:9` or `9:16`.</ParamField>
<ParamField body="seed" type="integer">Repeat an earlier result.</ParamField>

<CodeGroup>
  ```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...
  ```
</CodeGroup>

<ResponseExample>
  ```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
  }
  ```
</ResponseExample>

## 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](/docs/api/clips) 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`](/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](/docs/api/events#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](/docs/api/pagination).

<ParamField query="kind" type="string">`video` (or another async kind).</ParamField>
<ParamField query="status" type="string">`queued`, `processing`, `completed` or `failed`.</ParamField>
<ParamField query="session_id" type="string">Jobs an agent submitted from one session.</ParamField>
<ParamField query="limit" type="number">Page size, 1–100.</ParamField>
<ParamField query="after" type="string">The `next_cursor` of the previous page.</ParamField>

## Billing

A job is billed **what it actually cost**, on the media tier, when it completes; nothing is quoted beforehand. At submit the organization's prepaid balance is checked against a small floor — `insufficient_credits` if it is below it — and the debit settles under the job id at completion, exactly once. A job that fails, is refused, or is abandoned is not billed.

## Errors

| Code                     | When                                                                                                      |
| ------------------------ | --------------------------------------------------------------------------------------------------------- |
| `validation_failed`      | Missing `Idempotency-Key`, an unknown `model`, a private-host `image_urls` entry, `seconds` outside 1–60. |
| `insufficient_credits`   | The prepaid balance is below the admission floor.                                                         |
| `provider_error`         | The model would not take the job. Nothing is queued or billed.                                            |
| `not_found`              | No such `med_` in this organization.                                                                      |
| `feature_not_configured` | This deploy has no generation provider or no persistence configured.                                      |
