---
url: https://docs.freetheai.org/api/videos.md
description: >-
  POST /v1/videos - generate a video as a task in the OpenAI Videos format, then
  download the MP4. Provider video models are priced per second.
---

# Videos

Generates a video from a prompt in the OpenAI Videos format. Videos take minutes, so they run as tasks: create the task, poll it, and download the file when it completes. Paid video models from providers are priced per second of video; see [freetheai.org/models](https://freetheai.org/models?kind=video).

`POST https://api.freetheai.org/v1/videos`, `GET https://api.freetheai.org/v1/videos/{id}`, `GET https://api.freetheai.org/v1/videos/{id}/content`

## Create the task

::: code-group

```bash [curl]
curl https://api.freetheai.org/v1/videos \
  -H "Authorization: Bearer $FTAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "MODEL_ID", "prompt": "Waves rolling onto a beach at sunset", "seconds": 8}'
```

```python [Python]
import os, time
import requests

base = "https://api.freetheai.org/v1"
headers = {"Authorization": f"Bearer {os.environ['FTAI_KEY']}"}

task = requests.post(f"{base}/videos", headers=headers,
                     json={"model": "MODEL_ID", "prompt": "Waves rolling onto a beach at sunset", "seconds": 8}).json()
while task["status"] in ("queued", "in_progress"):
    time.sleep(10)
    task = requests.get(f"{base}/videos/{task['id']}", headers=headers).json()

if task["status"] == "completed":
    video = requests.get(f"{base}/videos/{task['id']}/content", headers=headers)
    open("video.mp4", "wb").write(video.content)
```

```ts [TypeScript]
import { writeFile } from 'node:fs/promises'

const base = 'https://api.freetheai.org/v1'
const headers = { 'Authorization': `Bearer ${process.env.FTAI_KEY}`, 'Content-Type': 'application/json' }

let task = await (await fetch(`${base}/videos`, {
  method: 'POST', headers, body: JSON.stringify({ model: 'MODEL_ID', prompt: 'Waves rolling onto a beach at sunset', seconds: 8 }),
})).json()
while (task.status === 'queued' || task.status === 'in_progress') {
  await new Promise(resolve => setTimeout(resolve, 10000))
  task = await (await fetch(`${base}/videos/${task.id}`, { headers })).json()
}
if (task.status === 'completed') {
  const video = await fetch(`${base}/videos/${task.id}/content`, { headers })
  await writeFile('video.mp4', Buffer.from(await video.arrayBuffer()))
}
```

:::

| Field | Type | Description |
| --- | --- | --- |
| `model` | string | A video model ID from [`GET /v1/models`](/api/models) (`"type": "video"`). |
| `prompt` | string | What to show. |
| `seconds` | integer | Optional. Length of the video (for example 4, 8, or 12); 4 when left out. |
| `size` | string | Optional. For example `1280x720`, when the model supports it. |

The create call answers at once with the task (`"object": "video"`, `"status": "queued"`, and a `poll_url`).

## Get the video

Poll `GET /v1/videos/{id}` every 10 seconds or so. `status` moves from `queued` to `in_progress` to `completed` or `failed`; `progress` counts to 100. A completed task carries `content_url`; download the MP4 from `GET /v1/videos/{id}/content`. A failed one carries `error` with a `code` and a `message`.

The request holds the price of the seconds you ask for. You are charged for the seconds delivered, never more than you asked for; a failed or expired task costs nothing.

## Errors

| Status | Meaning |
| --- | --- |
| `400` | A field has a value the endpoint does not take (the message names it). |
| `401` | Missing or invalid API key. |
| `402` | Not enough wallet balance, or the key's budget is used ([paid usage errors](/errors#billing)). |
| `404` | No model has that ID, none of the providers you follow serve it, or the task ID is not yours. |
| `503` | The provider is busy. Retry; nothing is charged. |
