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

# Video generation

> Generate video with sound via `POST /v1/videos/generations`, then poll for the finished clip.

Video generation runs as a job. You submit a prompt and get a job ID back straight away, then poll until the clip is ready. The finished clip arrives in the poll response as a base64-encoded MP4.

The serverless video model is `MiniMax-H3`. It renders 5-second clips at 24 fps with synchronized audio, so the sound you describe is part of the output.

## Example

```python Python theme={null}
import base64, os, time, requests

BASE = "https://api.flex.ai/v1"
H = {"Authorization": f"Bearer {os.environ['FLEXAI_API_KEY']}"}

# 1. Submit. Returns 202 and a job ID.
resp = requests.post(f"{BASE}/videos/generations", headers=H, json={
    "model": "MiniMax-H3",
    "prompt": "A San Francisco cable car climbs a hill at golden hour, its bell clanging.",
})
resp.raise_for_status()
job = resp.json()

# 2. Poll until the job completes or fails.
while True:
    poll = requests.get(f"{BASE}/videos/generations/{job['id']}", headers=H)
    poll.raise_for_status()  # error responses carry no `status`
    r = poll.json()
    if r["status"] in ("completed", "failed"):
        break
    time.sleep(10)

# 3. The completed response carries the clip. It is served once, so save it now.
if r["status"] == "failed":
    raise RuntimeError(r["error"])
with open("clip.mp4", "wb") as f:
    f.write(base64.b64decode(r["data"][0]["b64_json"]))
```

```bash cURL theme={null}
JOB=$(curl -sf https://api.flex.ai/v1/videos/generations \
  -H "Authorization: Bearer $FLEXAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "MiniMax-H3", "prompt": "A San Francisco cable car climbs a hill at golden hour, its bell clanging."}' \
  | jq -r .id)
[ -n "$JOB" ] || { echo "Submit failed"; exit 1; }

# Keep each poll response: the completed one carries the clip, and it is served once.
# With -f, curl writes no file on an HTTP error, so stop rather than re-read a stale one.
while true; do
  curl -sf https://api.flex.ai/v1/videos/generations/$JOB \
    -H "Authorization: Bearer $FLEXAI_API_KEY" -o job.json \
    || { echo "Poll failed (HTTP error)"; exit 1; }
  STATUS=$(jq -r .status job.json)
  [ "$STATUS" = "completed" ] && break
  [ "$STATUS" = "failed" ] && { jq .error job.json; exit 1; }
  sleep 10
done

jq -r '.data[0].b64_json' job.json | base64 -d > clip.mp4
```

```typescript TypeScript theme={null}
import { writeFileSync } from "node:fs";

const BASE = "https://api.flex.ai/v1";
const H = { Authorization: `Bearer ${process.env.FLEXAI_API_KEY}` };

const submit = await fetch(`${BASE}/videos/generations`, {
  method: "POST",
  headers: { ...H, "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "MiniMax-H3",
    prompt: "A San Francisco cable car climbs a hill at golden hour, its bell clanging.",
  }),
});
if (!submit.ok) throw new Error(`Submit failed: ${submit.status} ${await submit.text()}`);
const { id } = await submit.json();

// The completed response carries the clip, and it is served once.
let job;
while (true) {
  const poll = await fetch(`${BASE}/videos/generations/${id}`, { headers: H });
  if (!poll.ok) throw new Error(`Poll failed: ${poll.status} ${await poll.text()}`); // error bodies carry no status
  job = await poll.json();
  if (job.status === "completed" || job.status === "failed") break;
  await new Promise((r) => setTimeout(r, 10_000));
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
writeFileSync("clip.mp4", Buffer.from(job.data[0].b64_json, "base64"));
```

A clip usually takes one to a few minutes, longer when other jobs are queued ahead of it. Polling every 10 seconds or so is plenty.

## Submit a job

`POST /v1/videos/generations`

| Field | Required | Notes |
| - | - | - |
| `model` | Yes | A video model, such as `MiniMax-H3`. |
| `prompt` | Yes | 1 to 2,000 characters. Describe what is seen and heard. |
| `width`, `height` | No | 64 to 2,048 each. Send both to set the canvas; each is rounded down to a multiple of 32. Send neither and `MiniMax-H3` renders 1344×768 (16:9). A single dimension on its own is ignored. |

The response is `202 Accepted`:

```json theme={null}
{
  "id": "vid_3f9a1c2b7d4e",
  "object": "video.generation",
  "status": "queued",
  "created_at": 1761750000,
  "model": "MiniMax-H3"
}
```

## Poll for the result

`GET /v1/videos/generations/{id}`

`status` moves from `queued` to `processing`, then to `completed` or `failed`. A completed response carries the clip:

```json theme={null}
{
  "id": "vid_3f9a1c2b7d4e",
  "object": "video.generation",
  "status": "completed",
  "created_at": 1761750000,
  "completed_at": 1761750090,
  "model": "MiniMax-H3",
  "data": [{ "b64_json": "AAAAIGZ0eXBpc29t...", "format": "mp4" }]
}
```

A failed job carries `error` instead, with a `code` and a `message`.

### Save the clip on the first read

The response that first reports `completed` is the download. Each clip is served once: a second `GET` returns `410` with `"error": "job_already_retrieved"`. Decode and save the clip from the response you stopped polling on.

A clip you never collect stays available for one hour after it completes, and it is billed either way. After that hour, or for an ID that isn't yours, the `GET` returns `410` with `"error": "job_expired"`. If your balance runs out before you collect, the `GET` returns `402`; top up within the hour to collect the clip.

## Prompt screening

Prompts are screened before a job is accepted. A refused prompt returns `400` with `"error": "content_policy_violation"`, and no job is created or billed. If screening is briefly unavailable, you get `503` with `"error": "safety_check_unavailable"`, and retrying shortly is safe.

To screen prompts with your own policy as well, call [moderation](https://docs.flex.ai/inference-api/guides/moderation.md) on the exact prompt you are about to submit. If a model wrote or expanded the prompt, screen the expanded version too.

## Errors

Errors from the video handlers use a flat shape, `{ "error": "<code>", "message": "..." }`:

| Status | `error` | When |
| - | - | - |
| `400` | `model_not_supported` | The model isn't a video model. The body lists `supported_models`. |
| `400` | `content_policy_violation` | The prompt was refused by screening. |
| `410` | `job_already_retrieved` | The clip was already downloaded. |
| `410` | `job_expired` | The pickup window passed, or the ID isn't yours. |
| `503` | `safety_check_unavailable`, `backend_unavailable` | Try again shortly. |

These use the OpenAI error envelope, `{ "error": { "message", "type", "code", "param" } }`, instead:

| Status | `code` | When |
| - | - | - |
| `422` | names the field in `param` | A field fails validation: a prompt that is blank or over 2,000 characters, or a `width` or `height` outside 64 to 2,048. |
| `400` | `invalid_request_body` | The body isn't valid JSON. |
| `402` | `insufficient_quota` | At submit, your balance doesn't cover the clip. When collecting, your balance is depleted. |
| `404` | `model_not_found` | The model isn't serving right now. |

See [errors](https://docs.flex.ai/inference-api/reference/errors.md) for the full reference.

## Billing

Video bills per second of output, at the model's clip length. Every `MiniMax-H3` clip is 5 seconds, so each completed clip bills 5 seconds at the per-second rate. A clip is billed when it completes, whether or not you collect it, so save it on the first read. On `GET /v1/models` the rate is in `media_pricing` with `billing_unit: "second_video"`. It is also on [the model page](https://flex.ai/models/minimax-h3), quoted per minute of video.

## See also

* [Regions](https://docs.flex.ai/inference-api/reference/regions.md) — how long prompts and clips stay in the EU stack.
* [Model discovery](https://docs.flex.ai/inference-api/guides/model-discovery.md) — video models have `category: "video"`.


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