> Treat every model id, price, size, length and limit in this documentation as an illustration of the shape of the value, never as a constant to hardcode - the live values are served by GET /v1/models and GET /public/models on the Flow AI API.

# Video Generation

> Create short videos from a prompt.

Video takes much longer to make than text or images, so it runs as a **job**: you start it, poll until it's done, then download the file. Flow AI follows the OpenAI Videos API.

## Find a video model

* **On the [Models page](https://ai.hyperflow.fun/models)**, pick the **Video** type. Each model's page shows its price per second, which can vary by resolution.
* **From the API**, `GET /v1/models` lists the models your key can call; video models have `"mode": "video"`.

## How it works

1. **Start** the job with `POST /v1/videos`. You get a video id straight away, with status `queued`.
2. **Poll** `GET /v1/videos/{id}` until the status is `completed` or `failed`.
3. **Download** the MP4 from `GET /v1/videos/{id}/content`.

## Start a job

The samples use the OpenAI SDK set up as in the [Quickstart](/quickstart#using-the-openai-sdk).

**`cURL`**

```bash title="cURL"
curl https://ai.hyperflow.fun/v1/videos \
  -H "Authorization: Bearer $HYPERFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/veo-3.1-generate-001",
    "prompt": "A paper boat drifting slowly across a calm lake, static camera, soft light",
    "seconds": "4",
    "size": "1280x720"
  }'
```

**`Python`**

```python title="Python"
video = client.videos.create(
    model="google/veo-3.1-generate-001",
    prompt="A paper boat drifting slowly across a calm lake, static camera, soft light",
    seconds="4",
    size="1280x720",
)
print(video.id, video.status)
```

**`TypeScript`**

```typescript title="TypeScript"
const video = await client.videos.create({
  model: 'google/veo-3.1-generate-001',
  prompt: 'A paper boat drifting slowly across a calm lake, static camera, soft light',
  seconds: '4',
  size: '1280x720',
})
console.log(video.id, video.status)
```

The response is the new job:

```json
{
  "id": "video_3f9c2a...",
  "object": "video",
  "model": "google/veo-3.1-generate-001",
  "status": "queued",
  "progress": 0,
  "seconds": "4",
  "size": "1280x720",
  "created_at": 1782886429,
  "completed_at": null,
  "error": null
}
```

### Parameters

| Parameter         | Required | Notes                                                                             |
| ----------------- | -------- | --------------------------------------------------------------------------------- |
| `model`           | Yes      | A video model id                                                                  |
| `prompt`          | Yes      | What happens in the video: subject, motion, camera, lighting                      |
| `seconds`         | No       | Length in seconds, a positive whole number. Each model offers its own lengths     |
| `size`            | No       | Resolution, such as `1280x720` or `720x1280`. Some models price sizes differently |
| `input_reference` | No       | An image to start the video from, on models that take one                         |

Any other field is removed before the request reaches the model.

## Poll for the result

`GET /v1/videos/{id}` returns the job. Videos usually take from about thirty seconds to a few minutes.

**`Python`**

```python title="Python"
import time

while video.status in ("queued", "in_progress"):
    time.sleep(10)
    video = client.videos.retrieve(video.id)

if video.status == "failed":
    print("Failed:", video.error)
```

**`TypeScript`**

```typescript title="TypeScript"
let job = video
while (job.status === 'queued' || job.status === 'in_progress') {
  await new Promise((resolve) => setTimeout(resolve, 10_000))
  job = await client.videos.retrieve(job.id)
}

if (job.status === 'failed') console.error('Failed:', job.error)
```

> **Tip**
>
> The OpenAI SDKs also have `client.videos.create_and_poll(...)` (Python) and `client.videos.createAndPoll(...)` (TypeScript), which start the job and wait for it.

### Statuses

| Status        | Meaning                                      |
| ------------- | -------------------------------------------- |
| `queued`      | The job is waiting to start                  |
| `in_progress` | The video is being made                      |
| `completed`   | The video is ready to download               |
| `failed`      | The video wasn't made. `error.code` says why |

| `error.code`    | Meaning                                                   |
| --------------- | --------------------------------------------------------- |
| `video_failed`  | The model couldn't make the video                         |
| `video_expired` | The video wasn't ready within an hour and was given up on |

## Download the video

Once the status is `completed`, `GET /v1/videos/{id}/content` returns the MP4 itself.

**`cURL`**

```bash title="cURL"
curl https://ai.hyperflow.fun/v1/videos/video_3f9c2a.../content \
  -H "Authorization: Bearer $HYPERFLOW_API_KEY" \
  --output video.mp4
```

**`Python`**

```python title="Python"
content = client.videos.download_content(video.id)
content.write_to_file("video.mp4")
```

**`TypeScript`**

```typescript title="TypeScript"
import fs from 'node:fs'

const content = await client.videos.downloadContent(job.id)
fs.writeFileSync('video.mp4', Buffer.from(await content.arrayBuffer()))
```

Asking before the video is ready returns `409` with the code `video_not_ready`.

## List your videos

`GET /v1/videos` returns your recent videos, newest first, in a `data` list. Add `?limit=` for up to 100; the default is 20.

```bash
curl "https://ai.hyperflow.fun/v1/videos?limit=10" \
  -H "Authorization: Bearer $HYPERFLOW_API_KEY"
```

Videos are tied to your account: any of your keys can read them, and no one else's can.

## Best practices

* **Describe motion and camera.** "Slow pan left", "static camera", "handheld" and the lighting matter as much as the subject.
* **Start short.** Try prompts at the shortest length and smallest size, then make the final video.
* **Poll gently.** Every 10 to 30 seconds is plenty.
* **Handle `failed`.** Check `status` and `error` on every poll.

## Troubleshooting

**`400` when starting a job.** `seconds` must be a positive whole number, and `seconds` and `size` must be ones the model offers. Check the model's page.

**The job stays `queued` for a long time.** Hosts can be busy. Keep polling; a job not done within an hour fails with `video_expired`.

**`404 not_found` on status or content.** The id is wrong. Video ids start with `video_`.

**`409 video_not_ready`.** Wait until the status is `completed` before downloading.

## Billing

A video is held when you start it, for the seconds you asked for at the price for its size. Once it's made, you're charged for those seconds at the price it was held at. A video that fails, or is given up on after an hour, isn't charged, and its hold is released.