# MiniMax H3 video generation

Source: https://platform.lyrouter.com/ai_gateway/docs/video-minimax/en
Last updated: 2026-10-19

Generate videos with the MiniMax H3 models: text to video, first-and-last-frame image to video, and omni-reference video from reference images, reference videos and reference audio. Besides regular video generation, `minimax/minimax-h3` offers two endpoints that only it supports: video upscaling (`POST /v1/videos/regenerations`, which regenerates a 768P video at 2K) and context enhancement (`POST /v1/videos/context`, whose result is an enhanced prompt in text). All three kinds of task run asynchronously and are tracked with the same query endpoint. The endpoints follow the shape of OpenAI's Videos API, and the gateway converts the common parameters into MiniMax's format. For the task lifecycle in general, see [Asynchronous tasks](https://platform.lyrouter.com/ai_gateway/docs/async/en).

## Supported models

| Model ID | Highlights | Duration and resolution | Billing basis |
| --- | --- | --- | --- |
| `minimax/minimax-h3` | Standard: text to video, first-and-last-frame image to video, omni reference (reference images / videos / audio); supports video upscaling and context enhancement | 4–15 s; 480P, 768P, 2K | Video per second (input + output), tiered by resolution; upscaling has its own tier; reference images are a separate item; context enhancement is billed per input and output token |
| `minimax/minimax-h3-max` | **Taken offline by the provider and currently unavailable; use `minimax/minimax-h3` instead.** Fast: text to video, first-and-last-frame image to video, omni reference; no multiple reference images, no upscaling or context enhancement | 5–15 s; 480P, 768P | Video per second, tiered by 480P and 768P; reference images are a separate item |

> [!warning]
> `minimax/minimax-h3-max` has been taken offline by the provider: calling it now returns HTTP 200, but the task object is empty and has no task ID, so it cannot be polled; you are not charged. Use `minimax/minimax-h3` instead.

A model must be enabled in the workspace's model marketplace before you can call it; the model marketplace shows the current prices.

## Endpoints

| Method | Path | Purpose |
| --- | --- | --- |
| POST | `/v1/videos` | Submit a video generation task; returns a task ID right away |
| GET | `/v1/videos/{id}` | Query the task status; works for all three kinds of task |
| GET | `/v1/videos/{id}/content` | Download the video (302 redirect to the video URL) |
| POST | `/v1/videos/regenerations` | Video upscaling: regenerates a 768P video produced by H3 at 2K; `minimax/minimax-h3` only |
| POST | `/v1/videos/context` | Context enhancement: turns multimodal context into a richer prompt; the result is text; `minimax/minimax-h3` only |
| DELETE | `/v1/videos/{id}` | Delete a finished task; a task in progress cannot be canceled |

The full URL is `https://api.lyrouter.com` followed by the path in the table. The three submit endpoints share the same model name and the same task IDs, and every task they return is queried with `GET /v1/videos/{id}`.

## Authentication

Send your API key in a request header:

```text
Authorization: Bearer <API Key>
```

`Bearer` is case-sensitive. You can use `x-api-key: <API Key>` instead; when both are present, only `Authorization` is read. Create API keys on the workspace's **API Keys** page. Your API key is used only to authenticate with LYRouter and is never forwarded to the upstream provider. See [Base URLs and authentication](https://platform.lyrouter.com/ai_gateway/docs/call-gateway/en).

Any API key in the same workspace can query and download that workspace's tasks; a key from another workspace gets 404.

## Headers

| Header | Required | Description |
| --- | --- | --- |
| `Content-Type` | Required when submitting | `application/json`. `POST /v1/videos` also accepts `multipart/form-data`; see "Submitting a form" below. Upscaling and context enhancement accept only JSON |
| `X-Gateway-Strict-Parameters` | No | When `true`, a request containing parameters that would be dropped returns 400 instead, without submitting a task or charging; see [Gateway headers and extensions](https://platform.lyrouter.com/ai_gateway/docs/gateway-headers/en) |

## Submit a task

`POST /v1/videos`

Submits a video generation task. The gateway validates the parameters, converts them into MiniMax's format and submits the task to the provider, then returns a task object with `status` `queued` right away; the video is generated in the background. The submit request itself incurs no cost.

### Request body

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | `minimax/minimax-h3` or `minimax/minimax-h3-max` (taken offline by the provider and currently unavailable; use `minimax/minimax-h3` instead). It must be a specific model ID: a virtual model `@vmodel/<name>` or `lyrouter/auto` returns 403, and a failure does not fall back to the workspace's Default Model |
| `prompt` | string | Yes | The prompt |
| `seconds` | integer | Yes | Video duration in seconds; must be a positive integer. 4–15 for H3 and 5–15 for H3-Max (H3-Max has been taken offline by the provider and is currently unavailable). MiniMax requires a duration, and the gateway does not fill in a default |
| `size` | string | Yes | Resolution: a tier name (`480P`, `768P`, `2K`) or `WxH` (the short edge must be exactly 480 or 768); see "`size`: the short edge must land exactly on a tier" below. H3-Max does not support `2K` (H3-Max has been taken offline by the provider and is currently unavailable). MiniMax requires a resolution |
| `first_frame` | string | No | URL of the first-frame image (image to video); 1 image |
| `last_frame` | string | No | URL of the last-frame image; 1 image |
| `reference_image` | string \| string[] | No | URLs of reference images: a string for one image, an array for several; up to 9 |
| `input_reference` | string \| string[] | No | URLs of reference **videos**; up to 3 |
| `reference_audio` | string \| string[] | No | URLs of reference audio; up to 3 |
| `watermark` | boolean | No | Whether to add a watermark to the video |

- **Image to video and omni reference are mutually exclusive**: once you send `reference_image`, `input_reference` or `reference_audio`, you cannot also send `first_frame` / `last_frame`, and vice versa. The provider enforces this.
- These parameters exist in the gateway contract (other models use them), but MiniMax has no equivalent, so sending one returns 400 `<parameter name> is not supported for this model`: `generate_audio` (sound comes from reference audio), `output_format` (no container choice), `omni_reference_task_type` (no omni task types), `source_video` (belongs to the upscaling endpoint only).
- Other fields not in the table are dropped, and the `X-Gateway-Unsupported-Parameters` response header lists their names; with `X-Gateway-Strict-Parameters: true`, the request returns 400 instead. This includes MiniMax's native `duration`, `resolution`, `ratio` and `content`; use `seconds`, `size` and the material fields above instead.
- This endpoint does not support `n`; each request generates one video.

### Submitting a form

`POST /v1/videos` also accepts `multipart/form-data`, which is what the official OpenAI SDK's `videos.create()` uses. In a form, the gateway reads fields such as `model`, `prompt`, `seconds`, `size`, `input_reference` and `watermark`:

- **`input_reference` accepts only a URL; an uploaded file raises no error but is ignored**: pass a publicly reachable `https://` URL. Currently, when you upload a file, the task is generated as if there were no reference video and is charged as usual.
- `watermark` accepts only the literal values `true` or `false`.
- Submit `first_frame`, `last_frame`, `reference_image` and `reference_audio` in a JSON request body. Currently these fields in a form are replaced with a placeholder value and forwarded to the provider; with `reference_audio`, for example, the submission returns 200 and the task then fails with the `error.message` `content[1].audio_url: invalid param: audio url must be http(s):// or data:...;base64` (failed tasks are not charged).

### Request examples

The examples read the API key from the `LYROUTER_API_KEY` environment variable; to set it, see [Your first model call](https://platform.lyrouter.com/ai_gateway/docs/quickstart/en).

**Request examples**

cURL:
```bash
curl https://api.lyrouter.com/v1/videos \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "minimax/minimax-h3",
    "prompt": "Epic space-opera trailer: a female captain alone at the observation window",
    "size": "768P",
    "seconds": 5
  }'
```

Python:
```python
import os
import httpx

resp = httpx.post(
    "https://api.lyrouter.com/v1/videos",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    json={
        "model": "minimax/minimax-h3",
        "prompt": "Epic space-opera trailer: a female captain alone at the observation window",
        "size": "768P",
        "seconds": 5,
    },
    timeout=60,
)
resp.raise_for_status()
dropped = resp.headers.get("X-Gateway-Unsupported-Parameters")
if dropped:
    print("These parameters did not take effect:", dropped)
task = resp.json()
print(task["id"], task["status"])
```

Node.js:
```js
const resp = await fetch("https://api.lyrouter.com/v1/videos", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LYROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "minimax/minimax-h3",
    prompt: "Epic space-opera trailer: a female captain alone at the observation window",
    size: "768P",
    seconds: 5,
  }),
});
if (!resp.ok) throw new Error(await resp.text());
const dropped = resp.headers.get("X-Gateway-Unsupported-Parameters");
if (dropped) console.log("These parameters did not take effect:", dropped);
const task = await resp.json();
console.log(task.id, task.status);
```

### Response

A successful submission returns HTTP 200 and a task object with `status` `queued`; for the fields, see "Retrieve a task" below. The submit response has no `usage`, because the cost is settled when the task completes. Below, the 400 is a `size` of `1280x720` (a short edge of 720 is not one of MiniMax's tiers), the 403 is a model that is not enabled in this workspace (or a virtual model or `lyrouter/auto`), and the 429 means the provider account has reached its limit of concurrent tasks.

**Response examples**

200:
```json
{
  "id": "aigwt_minimax_videos_Qm3Tz8VbN1sLk0pYc4dReA",
  "object": "video",
  "model": "MiniMax-H3",
  "status": "queued",
  "progress": 0,
  "created_at": 1791950400,
  "expires_at": 1792555200,
  "seconds": 5,
  "size": "768P"
}
```

400:
```json
{
  "error": {
    "message": "this model takes discrete resolution tiers, not arbitrary sizes: 1280x720 (short side 720). Use a size whose short side is 480 or 768, or pass the tier name directly as size (e.g. \"768P\", \"2K\")",
    "type": "invalid_request_error",
    "code": "invalid_parameter"
  }
}
```

403:
```json
{
  "error": {
    "message": "Model is not available in the model plaza",
    "type": "access_denied",
    "code": "model_not_available"
  }
}
```

429:
```json
{
  "error": {
    "message": "upstream concurrency full",
    "type": "rate_limit_error",
    "code": "upstream_concurrency_full"
  }
}
```

- Keep the returned `id`. If a query hits a network error or times out, keep querying with the same ID rather than resubmitting: resubmitting creates a new task and charges again.
- The concurrency limit behind 429 `upstream_concurrency_full` is shared per provider account, so switching to another API key does not help; resubmit later.

## Retrieve a task

`GET /v1/videos/{id}`

Reads the task's current status; video generation, video upscaling and context enhancement tasks all use this endpoint. The gateway polls the provider for progress in the background, and this endpoint only returns the status the gateway has recorded, so querying more often does not get you the result sooner.

### Path parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The task ID returned at submission, starting with `aigwt_`. The provider's own task ID cannot be queried and returns 404 |

### Request examples

**Request examples**

cURL:
```bash
curl https://api.lyrouter.com/v1/videos/$TASK_ID \
  -H "Authorization: Bearer $LYROUTER_API_KEY"
```

Python:
```python
import os
import httpx

task_id = "aigwt_minimax_videos_Qm3Tz8VbN1sLk0pYc4dReA"
resp = httpx.get(
    f"https://api.lyrouter.com/v1/videos/{task_id}",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    timeout=60,
)
resp.raise_for_status()
task = resp.json()
print(task["status"], task.get("usage"), task.get("error"))
```

Node.js:
```js
const taskId = "aigwt_minimax_videos_Qm3Tz8VbN1sLk0pYc4dReA";
const resp = await fetch(`https://api.lyrouter.com/v1/videos/${taskId}`, {
  headers: { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` },
});
if (!resp.ok) throw new Error(await resp.text());
const task = await resp.json();
console.log(task.status, task.usage, task.error);
```

### Response

When the task exists, the response is always HTTP 200, even if the task itself failed; rely on `status` for the outcome. A task ID that does not exist, is older than 7 days or belongs to another workspace returns 404.

**Response examples**

200 completed:
```json
{
  "id": "aigwt_minimax_videos_Qm3Tz8VbN1sLk0pYc4dReA",
  "object": "video",
  "model": "MiniMax-H3",
  "status": "completed",
  "progress": 100,
  "created_at": 1791950400,
  "completed_at": 1791950507,
  "expires_at": 1792555200,
  "seconds": 5,
  "size": "768P",
  "usage": { "duration_seconds": 5 }
}
```

200 context enhancement:
```json
{
  "id": "aigwt_minimax_videos-context_Zp8Rk2Vt5Lq9Wx3Ny6Bh1A",
  "object": "video",
  "model": "MiniMax-H3",
  "status": "completed",
  "progress": 100,
  "created_at": 1791950400,
  "completed_at": 1791950421,
  "expires_at": 1792555200,
  "seconds": 5,
  "output_text": "integrated_multimodal_description: [Shot 1] Cinematic, a medium shot featuring a slow push-in on ...",
  "usage": { "completion_tokens": 1820 }
}
```

200 failed:
```json
{
  "id": "aigwt_minimax_videos_Fj4Ns7Kd2Pw9Lm5Qx8Rt3Y",
  "object": "video",
  "model": "MiniMax-H3",
  "status": "failed",
  "progress": 0,
  "created_at": 1791950400,
  "completed_at": 1791950482,
  "expires_at": 1792555200,
  "seconds": 5,
  "size": "768P",
  "error": {
    "code": "upstream_error",
    "message": "upstream task failed"
  }
}
```

404:
```json
{
  "error": {
    "message": "video task not found",
    "type": "invalid_request_error",
    "code": "not_found"
  }
}
```

| `status` | Meaning | What to do |
| --- | --- | --- |
| `queued` | Accepted; generation has not started | Keep polling |
| `in_progress` | The provider is generating | Keep polling |
| `completed` | Finished | For video tasks, call `/content` to download; for context enhancement tasks, read `output_text` |
| `failed` | Failed | Read `error`; you are not charged |

| Field | Description |
| --- | --- |
| `id` | The gateway's task ID, in the format `aigwt_<provider>_<task type>_<random string>`, such as `aigwt_minimax_videos_…`, `aigwt_minimax_videos-regenerations_…` or `aigwt_minimax_videos-context_…`. It is not the provider's task ID; when you report a problem to the provider, use `error.provider_request_id` |
| `object` | Always `video`, including for context enhancement tasks |
| `model` | The upstream model name actually called, such as `MiniMax-H3` |
| `status` | One of `queued`, `in_progress`, `completed` or `failed` |
| `progress` | `0` before completion and `100` after; it does not show intermediate progress, so rely on `status` |
| `created_at` / `completed_at` | Submission time and end time (Unix seconds). `completed_at` appears only after the task ends |
| `expires_at` | When the task record expires, 7 days after submission. After that, queries return 404 |
| `seconds` / `size` | Echo the values you submitted; absent when not submitted |
| `output_text` | Context enhancement tasks only: the enhanced prompt |
| `error` | Present only when the task is `failed`: `code` and `message`, plus `provider_request_id` (the provider's request identifier) and `retryable`, each present only when it has a value |
| `usage` | Present after the task completes: `duration_seconds` (billed seconds: for video generation it includes the seconds of reference video; for upscaling it counts only the source video's seconds) for video generation and upscaling, `completion_tokens` (output tokens) for context enhancement |

- For failed tasks, `error.code` is the provider's error code; when the provider gives no recognizable code, it is `upstream_error`. `error.retryable` `false` means retrying unchanged will not succeed, `true` means you can retry, and a missing field means the gateway cannot tell. For all values, see [Asynchronous tasks](https://platform.lyrouter.com/ai_gateway/docs/async/en).
- **Polling interval**: start querying about 10 seconds after submitting, then gradually widen to once every 30 seconds.
- **Waiting limit**: a task still unfinished 24 hours after submission will never produce a result and is not charged, so you can stop polling. At that point the query may still show `queued` or `in_progress`; to see the task's actual state, search by task ID on the workspace's **Multimedia generation** page in the console (State shows Expired).

## Download the video

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

Call it after a video generation or upscaling task becomes `completed`. It returns 302, and the `Location` response header is the signed video URL provided by the provider. Let the client follow the redirect to download: add `-L` in cURL, set `follow_redirects=True` in Python `httpx`; Node.js `fetch` follows redirects by default. A context enhancement task produces text in `output_text` and has no video to download, so do not call this endpoint for it: currently, calling it on a completed context enhancement task returns HTTP 500 with an HTML page as the body, not a JSON error.

### Request examples

**Request examples**

cURL:
```bash
# Download to a local file
curl -L https://api.lyrouter.com/v1/videos/$TASK_ID/content \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -o video.mp4

# Get only the video URL without downloading
curl -s -o /dev/null -w '%{redirect_url}\n' \
  https://api.lyrouter.com/v1/videos/$TASK_ID/content \
  -H "Authorization: Bearer $LYROUTER_API_KEY"
```

Python:
```python
import os
import httpx

task_id = "aigwt_minimax_videos_Qm3Tz8VbN1sLk0pYc4dReA"
with httpx.stream(
    "GET",
    f"https://api.lyrouter.com/v1/videos/{task_id}/content",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    follow_redirects=True,
    timeout=600,
) as video:
    video.raise_for_status()
    with open("video.mp4", "wb") as f:
        for chunk in video.iter_bytes():
            f.write(chunk)
```

Node.js:
```js
import { writeFile } from "node:fs/promises";

const taskId = "aigwt_minimax_videos_Qm3Tz8VbN1sLk0pYc4dReA";
const video = await fetch(`https://api.lyrouter.com/v1/videos/${taskId}/content`, {
  headers: { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` },
});
if (!video.ok) throw new Error(`download failed: ${video.status}`);
await writeFile("video.mp4", Buffer.from(await video.arrayBuffer()));
```

### Response

**Response examples**

302:
```text
Status: 302
Location: <signed video URL provided by the provider>
```

409:
```json
{
  "error": {
    "message": "video is not ready yet",
    "type": "invalid_request_error",
    "code": "not_ready"
  }
}
```

404:
```json
{
  "error": {
    "message": "video task not found",
    "type": "invalid_request_error",
    "code": "not_found"
  }
}
```

- The video URL is fixed when the task completes, and every later call redirects to the same URL. How long the URL stays valid is up to the provider, usually 24 hours; once it expires, it cannot be fetched again through the gateway, so download it or copy it to your own storage as soon as you get it.
- Calling it before the task completes (or after it failed) returns 409. These 4xx responses also count toward the API key's error rate, and frequent calls can trip the [circuit breaker](https://platform.lyrouter.com/ai_gateway/docs/rate-errors/en), so query until the status is `completed` before downloading.

## Video upscaling

`POST /v1/videos/regenerations`

Regenerates a **768P video produced by H3** at 2K; only `minimax/minimax-h3` supports it. It works only on H3's own output that meets the 768P spec; it is not a general-purpose upscaler. Submitting returns the same kind of task object: query it with `GET /v1/videos/{id}` and download it with `/content` once it completes. Accepts only a JSON request body.

### Request body

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | `minimax/minimax-h3` |
| `prompt` | string | Yes | The **exact final prompt that was sent to the model** when the 768P video was generated. If context enhancement was used then, pass the prompt **after** enhancement |
| `source_video` | string | Yes | URL of the 768P video to upscale |
| `size` | string | No | Target resolution; defaults to `2K` |
| `first_frame` / `last_frame` / `reference_image` / `input_reference` / `reference_audio` | — | No | The reference materials used to generate the source video, attached exactly as they were; written as in "Submit a task" |
| `watermark` | boolean | No | Whether to add a watermark to the video |

- **Do not send `seconds`**: the duration comes from the source video, and sending it returns 400 `seconds is not supported for video regeneration (the source video determines the duration)`.
- `generate_audio`, `output_format` and `omni_reference_task_type` likewise return 400 `<parameter name> is not supported for this model`.
- The source video must have an audio track, 24 fps, width and height both divisible by 32, an area between 589,824 and 1,032,192 pixels, and 107–362 frames in total (in steps of 17, roughly 4–15 s).
- The provider requires the `prompt` and reference materials to match exactly what was used to generate the source video. If they do not match, there is **no error**; you just do not get the expected result, and the gateway cannot check this for you.

### Request examples

**Request examples**

cURL:
```bash
curl https://api.lyrouter.com/v1/videos/regenerations \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "minimax/minimax-h3",
    "prompt": "The exact final prompt that produced the 768P video",
    "source_video": "https://example.com/h3-768p.mp4",
    "size": "2K"
  }'
```

Python:
```python
import os
import httpx

resp = httpx.post(
    "https://api.lyrouter.com/v1/videos/regenerations",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    json={
        "model": "minimax/minimax-h3",
        "prompt": "The exact final prompt that produced the 768P video",
        "source_video": "https://example.com/h3-768p.mp4",
        "size": "2K",
    },
    timeout=60,
)
resp.raise_for_status()
print(resp.json()["id"])
```

Node.js:
```js
const resp = await fetch("https://api.lyrouter.com/v1/videos/regenerations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LYROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "minimax/minimax-h3",
    prompt: "The exact final prompt that produced the 768P video",
    source_video: "https://example.com/h3-768p.mp4",
    size: "2K",
  }),
});
if (!resp.ok) throw new Error(await resp.text());
console.log((await resp.json()).id);
```

### Response

A successful submission returns HTTP 200 and a task object whose `id` starts with `aigwt_minimax_videos-regenerations_`; after completion, `usage.duration_seconds` is the billed seconds. The 400 below is a request without `source_video`.

**Response examples**

200:
```json
{
  "id": "aigwt_minimax_videos-regenerations_Hk2Lp9WqR4tYv7Xz1Bc3Nd",
  "object": "video",
  "model": "MiniMax-H3",
  "status": "queued",
  "progress": 0,
  "created_at": 1791950400,
  "expires_at": 1792555200,
  "size": "2K"
}
```

400:
```json
{
  "error": {
    "message": "source_video is required for video regeneration (the 768P video to upscale)",
    "type": "invalid_request_error",
    "code": "invalid_parameter"
  }
}
```

## Context enhancement

`POST /v1/videos/context`

Turns a prompt and its multimodal context into a structured, semantically richer prompt; only `minimax/minimax-h3` supports it. **This endpoint returns text only; it does not generate a video.** It is also an asynchronous task: after submitting, query it with `GET /v1/videos/{id}`; once it completes, the enhanced prompt is in the task object's `output_text`, and there is nothing to download from `/content`. The typical use is two steps: call this endpoint to get `output_text`, then submit it as the `prompt` of `POST /v1/videos`. Accepts only a JSON request body.

> [!warning]
> This endpoint currently does not reject Seedance models: a Seedance model ID generates a regular video (with no `output_text`) that is charged as a video. Call this endpoint only with `minimax/minimax-h3`.

### Request body

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | `minimax/minimax-h3` |
| `prompt` | string | Yes | The original prompt |
| `seconds` | integer | Yes | Planned video duration in seconds, 4–15. When omitted, the request returns 400 with the plain-text body `binding: expr_path=duration, cause=missing required parameter` (the response header is currently still `Content-Type: application/json`) |
| `size` | string | No | Written as in "Submit a task". When omitted, the gateway sends `2K` to the provider |
| `first_frame` / `last_frame` / `reference_image` / `input_reference` / `reference_audio` | — | No | Multimodal context; written and limited as in "Submit a task" |

`generate_audio`, `output_format`, `omni_reference_task_type` and `source_video` return 400 `<parameter name> is not supported for this model`.

### Request examples

**Request examples**

cURL:
```bash
curl https://api.lyrouter.com/v1/videos/context \
  -H "Authorization: Bearer $LYROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "minimax/minimax-h3",
    "prompt": "A cat walking down a neon street in the rain",
    "seconds": 5
  }'
```

Python:
```python
import os
import httpx

resp = httpx.post(
    "https://api.lyrouter.com/v1/videos/context",
    headers={"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"},
    json={
        "model": "minimax/minimax-h3",
        "prompt": "A cat walking down a neon street in the rain",
        "seconds": 5,
    },
    timeout=60,
)
resp.raise_for_status()
print(resp.json()["id"])
```

Node.js:
```js
const resp = await fetch("https://api.lyrouter.com/v1/videos/context", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LYROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "minimax/minimax-h3",
    prompt: "A cat walking down a neon street in the rain",
    seconds: 5,
  }),
});
if (!resp.ok) throw new Error(await resp.text());
console.log((await resp.json()).id);
```

### Response

A successful submission returns HTTP 200 and a task object whose `id` starts with `aigwt_minimax_videos-context_`. For the completed result, see "200 context enhancement" under "Retrieve a task" above: `output_text` is the enhanced prompt and `usage.completion_tokens` is the output token count. The 400 below is the error the provider returns when this endpoint is called with `minimax/minimax-h3-max` (taken offline); the gateway passes it through as is, without an `error.code`.

**Response examples**

200:
```json
{
  "id": "aigwt_minimax_videos-context_Zp8Rk2Vt5Lq9Wx3Ny6Bh1A",
  "object": "video",
  "model": "MiniMax-H3",
  "status": "queued",
  "progress": 0,
  "created_at": 1791950400,
  "expires_at": 1792555200,
  "seconds": 5
}
```

400:
```json
{
  "type": "error",
  "error": {
    "type": "bad_request_error",
    "message": "invalid params, model MiniMax-H3-Max does not support resolution 2K, supported resolutions: 480P, 768P (2013)",
    "http_code": "400"
  },
  "request_id": "<provider request identifier>"
}
```

## Delete a task

`DELETE /v1/videos/{id}`

For a finished task (`completed` or `failed`), returns HTTP 200 and the task object with `"deleted": true`; the task record is not deleted, and you can still query and download it until `expires_at`. Calling it on a task in progress returns 409 `cancel_not_supported`: the provider offers no cancel endpoint, so wait for the task to end. The official OpenAI SDK's `videos.delete()` reads this `deleted` field.

## MiniMax-specific parameters and limits

### `size`: the short edge must land exactly on a tier

| Notation | What the gateway does |
| --- | --- |
| Tier name (`480P`, `768P`, `2K`) | Passed through |
| `WxH` | Looks the **short edge** up in the tier table: it must be **exactly** `480` or `768`, otherwise the gateway returns 400 listing the legal forms. The aspect ratio is matched to the nearest supported one |

- `1280x720` has a short edge of 720, which is not a tier, so it is rejected; for 768P write `1366x768` in landscape or `768x1366` in portrait.
- `2K` has no fixed short edge; write it only as the tier name `"2K"`.
- With `WxH`, the aspect ratio must also be one of 21:9, 16:9, 4:3, 1:1, 3:4 or 9:16 (within 2%), otherwise the request returns 400 `size implies an aspect ratio this model does not support: <value>`.
- The tier sets the resolution tier; the actual pixel size of the output is decided by the provider, so go by the downloaded video. Measured: text to video and omni reference with `480P` or `854x480` produce 1038×576 (a short edge of 576, not 480); `768P` produces 1344×768; first-and-last-frame image to video with a 1:1 image and `854x480` produces 768×768; upscaling to `2K` produces 2560×1440.

### Three input scenarios and the aspect ratio

| Scenario | What you send | Aspect ratio |
| --- | --- | --- |
| Text to video | `prompt` only | Must be an explicit ratio, not adaptive: the gateway derives it from `size`; when `size` is a tier name, the video is generated at 16:9 |
| Image to video | `first_frame` (+ `last_frame`) | Set by the input image; `size` only sets the resolution |
| Omni reference to video | `reference_image` / `input_reference` / `reference_audio` | Derived from `size` when it is `WxH`; adaptive when it is a tier name. **With a reference video the ratio cannot be adaptive**: write `size` as `WxH` (such as `854x480` or `1366x768`); with a tier name the task fails |

### Input material limits

| Material | Limits |
| --- | --- |
| Images | JPG, JPEG, PNG, WEBP, HEIC, HEIF; sides [256, 5760] px; aspect ratio [0.4, 2.5]; ≤ 30 MB; at most 1 first frame, 1 last frame, 9 reference images |
| Video | MP4, MOV (H.264 / H.265 video, AAC / MP3 audio); ≤ 50 MB; at most 3 files; [2, 15] s each and ≤ 15 s in total; sides [256, 5760] px; aspect ratio [0.4, 2.5]; frame rate [23.976, 60] |
| Audio | WAV, MP3; ≤ 15 MB; at most 3 files; [2, 15] s each and ≤ 15 s in total |
| Request body | ≤ 64 MB in total; use public URLs for large files |

The provider checks the size and aspect ratio of reference materials: when one does not comply, the provider rejects it with error code 2013 and names the offending item (such as `content[N].image_url`); you are not charged. H3-Max does not support multiple reference images; the provider rejects such requests and suggests using H3 instead (H3-Max has been taken offline by the provider and is currently unavailable).

### Billing

| Task | Metering | Tiers |
| --- | --- | --- |
| Video generation (`minimax-h3`) | Per second | Three tiers: 480P / 768P / 2K |
| Video generation (`minimax-h3-max`; taken offline by the provider and currently unavailable, use `minimax-h3` instead) | Per second | Two tiers: 480P / 768P |
| Video upscaling (`/v1/videos/regenerations`) | Per second | A tier of its own |
| Context enhancement (`/v1/videos/context`) | Per token | Input and output billed separately |

- **Billed seconds are input plus output**, which is `usage.duration_seconds` on the task object: when you supply a reference video, its seconds count too (for example, a 4-second reference video plus 4 seconds of output gives a `duration_seconds` of 8). Reference audio is free. Video upscaling does not add them up: `duration_seconds` counts only the source video's seconds (measured: a 4-second source video is billed as 4).
- **Reference images are a separate item** (the two models charge different rates for them): for `minimax-h3`, the first 5 reference images are free, and each one after that is charged. The image count is not in `usage`; you can see the cost in the logs.
- Context enhancement is billed on input and output tokens separately. The task object's `usage` lists only `completion_tokens` (output), but the input tokens are charged as well.
- The price is locked at submission; when the task becomes `completed`, it is settled on the metering the provider actually returns; `failed` tasks (including timeouts) are not charged. To see a task's cost in the logs, see [Asynchronous tasks](https://platform.lyrouter.com/ai_gateway/docs/async/en).

## Complete example

Expands the prompt with context enhancement first, then generates a 768P video with the enhanced prompt, polls until it ends and downloads it. The cURL example requires `jq`; save it as a script file and run it with `bash` (it uses `exit` on errors).

**Complete example**

cURL:
```bash
BASE=https://api.lyrouter.com/v1
AUTH="Authorization: Bearer $LYROUTER_API_KEY"

# Submit a task (request body read from stdin) and print the task ID; without a task ID, print the response and fail
submit() {
  local resp id
  resp=$(curl -s "$BASE/$1" -H "$AUTH" -H "Content-Type: application/json" -d @-)
  id=$(echo "$resp" | jq -r '.id // empty')
  if [ -z "$id" ]; then echo "Submission failed: $resp" >&2; return 1; fi
  echo "task: $id" >&2
  echo "$id"
}

# Poll a task until it ends, then print the final task object; fail on a query error or after waiting 2 hours
wait_task() {
  local delay=10 give_up_at=$(( $(date +%s) + 2 * 3600 )) task status
  while [ "$(date +%s)" -le "$give_up_at" ]; do
    sleep $delay
    delay=$(( delay * 3 / 2 > 30 ? 30 : delay * 3 / 2 ))
    task=$(curl -s "$BASE/videos/$1" -H "$AUTH")
    status=$(echo "$task" | jq -r '.status')
    case "$status" in
      completed|failed) echo "$task"; return 0 ;;
      queued|in_progress|"") ;;  # An empty value is usually a network glitch: keep the task ID and query again
      *) echo "Query error: $task" >&2; return 1 ;;
    esac
  done
  echo "$1 is still running; query the same ID again later" >&2; return 1
}

# 1. Context enhancement: get the enhanced prompt; on failure, print the reason and exit
CTX_ID=$(echo '{"model":"minimax/minimax-h3","prompt":"A cat walking down a neon street in the rain","seconds":5}' \
  | submit videos/context) || exit 1
CTX=$(wait_task "$CTX_ID") || exit 1
PROMPT=$(echo "$CTX" | jq -r '.output_text // empty')
if [ -z "$PROMPT" ]; then echo "$CTX" | jq '.error'; exit 1; fi

# 2. Generate the video with the enhanced prompt
VIDEO_ID=$(jq -n --arg p "$PROMPT" \
    '{model:"minimax/minimax-h3",prompt:$p,size:"768P",seconds:5}' \
  | submit videos) || exit 1
TASK=$(wait_task "$VIDEO_ID") || exit 1

# 3. Download on success; print the reason on failure
if [ "$(echo "$TASK" | jq -r '.status')" = "completed" ]; then
  curl -sL "$BASE/videos/$VIDEO_ID/content" -H "$AUTH" -o video.mp4
else
  echo "$TASK" | jq '.error'
fi
```

Python:
```python
import os
import time
import httpx

BASE = "https://api.lyrouter.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['LYROUTER_API_KEY']}"}

def wait_task(client: httpx.Client, task: dict, max_wait: int = 2 * 3600) -> dict:
    """Poll from 10 seconds, widening gradually to 30 seconds, until the task ends."""
    if not task.get("id"):
        raise RuntimeError(f"No task ID returned: {task}")
    print("task:", task["id"])
    delay, give_up_at = 10, time.time() + max_wait
    while task["status"] in ("queued", "in_progress"):
        if time.time() > give_up_at:
            raise TimeoutError(f"{task['id']} is still running; query the same ID again later")
        time.sleep(delay)
        delay = min(delay * 1.5, 30)
        try:
            resp = client.get(f"{BASE}/videos/{task['id']}")
            resp.raise_for_status()
        except httpx.TransportError:
            continue  # Network glitch: keep the task ID and query again; do not resubmit
        task = resp.json()
    if task["status"] == "failed":
        err = task.get("error", {})
        raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
    return task

with httpx.Client(headers=HEADERS, timeout=60) as client:
    # 1. Context enhancement: get the enhanced prompt
    resp = client.post(f"{BASE}/videos/context", json={
        "model": "minimax/minimax-h3",
        "prompt": "A cat walking down a neon street in the rain",
        "seconds": 5,
    })
    resp.raise_for_status()
    prompt = wait_task(client, resp.json())["output_text"]

    # 2. Generate the video with the enhanced prompt
    resp = client.post(f"{BASE}/videos", json={
        "model": "minimax/minimax-h3",
        "prompt": prompt,
        "size": "768P",
        "seconds": 5,
    })
    resp.raise_for_status()
    task = wait_task(client, resp.json())

    # 3. Download: the endpoint returns 302, so follow the redirect
    with client.stream("GET", f"{BASE}/videos/{task['id']}/content",
                       follow_redirects=True, timeout=600) as video:
        video.raise_for_status()
        with open("video.mp4", "wb") as f:
            for chunk in video.iter_bytes():
                f.write(chunk)
    print("Saved video.mp4, usage:", task.get("usage"))
```

Node.js:
```js
import { writeFile } from "node:fs/promises";

const BASE = "https://api.lyrouter.com/v1";
const headers = { Authorization: `Bearer ${process.env.LYROUTER_API_KEY}` };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function submit(path, body) {
  const resp = await fetch(`${BASE}${path}`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!resp.ok) throw new Error(await resp.text());
  return resp.json();
}

// Poll from 10 seconds, widening gradually to 30 seconds, until the task ends
async function waitTask(task) {
  if (!task.id) throw new Error(`No task ID returned: ${JSON.stringify(task)}`);
  console.log("task:", task.id);
  let delay = 10_000;
  while (task.status === "queued" || task.status === "in_progress") {
    await sleep(delay);
    delay = Math.min(delay * 1.5, 30_000);
    let resp;
    try {
      resp = await fetch(`${BASE}/videos/${task.id}`, { headers });
    } catch {
      continue; // Network glitch: keep the task ID and query again; do not resubmit
    }
    if (!resp.ok) throw new Error(await resp.text());
    task = await resp.json();
  }
  if (task.status === "failed") {
    throw new Error(`${task.error?.code}: ${task.error?.message}`);
  }
  return task;
}

// 1. Context enhancement: get the enhanced prompt
const ctx = await waitTask(await submit("/videos/context", {
  model: "minimax/minimax-h3",
  prompt: "A cat walking down a neon street in the rain",
  seconds: 5,
}));

// 2. Generate the video with the enhanced prompt
const task = await waitTask(await submit("/videos", {
  model: "minimax/minimax-h3",
  prompt: ctx.output_text,
  size: "768P",
  seconds: 5,
}));

// 3. Download: fetch follows the 302 redirect by default
const video = await fetch(`${BASE}/videos/${task.id}/content`, { headers });
if (!video.ok) throw new Error(`download failed: ${video.status}`);
await writeFile("video.mp4", Buffer.from(await video.arrayBuffer()));
console.log("saved video.mp4", task.usage);
```

## Common errors

Error responses take the form `{"error":{"message":"...","type":"...","code":"..."}}`.

| Status | `error.message` | `error.code` | Cause and fix |
| --- | --- | --- | --- |
| 400 | `this model takes discrete resolution tiers, not arbitrary sizes: ...` | `invalid_parameter` | The short edge of `size` is not 480 or 768. Use a tier name (such as `768P` or `2K`) or a size whose short edge is exactly 480 / 768 |
| 400 | `size implies an aspect ratio this model does not support: <value>` | `invalid_parameter` | The aspect ratio derived from `size` is not one of the supported ratios |
| 400 | `seconds must be a positive whole number of seconds` | `invalid_parameter` | `seconds` is not a positive integer |
| 400 | `<parameter name> is not supported for this model` | `invalid_parameter` | The request sent `generate_audio`, `output_format` or `omni_reference_task_type`, which MiniMax does not support, or sent `source_video` to `/v1/videos`. Remove it from the request |
| 400 | `seconds is not supported for video regeneration (the source video determines the duration)` | `invalid_parameter` | Video upscaling does not accept `seconds`; remove it |
| 400 | `source_video is required for video regeneration (the 768P video to upscale)` | `invalid_parameter` | Video upscaling is missing the source video |
| 400 | `This model does not support the videos_regenerations operation` (or `videos_context`) | `unsupported_model_operation` | The selected model does not support video upscaling or context enhancement; use `minimax/minimax-h3`. Currently not every unsupported model returns this; see the notes below the table |
| 400 | `unsupported parameters for this model: <parameter name>` | `unsupported_parameters` | The request has `X-Gateway-Strict-Parameters: true` and contains parameters that would be dropped |
| 403 | `Model is not available in the model plaza` | `model_not_available` | The model is not enabled in this workspace, or a virtual model or `lyrouter/auto` was used. Use a specific model ID that is enabled |
| 404 | `video task not found` | `not_found` | The task ID does not exist, is older than 7 days, belongs to another workspace, or is the provider's task ID |
| 409 | `video is not ready yet` | `not_ready` | `/content` was called before the task completed (or after it failed). Query until the status is `completed` first |
| 409 | `cancelling an in-flight video task is not supported: ...` | `cancel_not_supported` | `DELETE` was called on a task in progress. Wait for the task to end |
| 429 | `upstream concurrency full` | `upstream_concurrency_full` | The provider account has reached its limit of concurrent tasks. Switching API keys does not help; resubmit later |

- Calling video upscaling or context enhancement with another model currently does not always return `unsupported_model_operation`: Seedance 2.5 on video upscaling returns 400 `source_video is not supported for this model` (`invalid_parameter`); a Seedance model on context enhancement is accepted and generates a regular video that is charged as usual; `minimax/minimax-h3-max` (taken offline) on video upscaling is accepted and a task is created, and on context enhancement gets the provider's 400 passed through (`invalid params, model MiniMax-H3-Max does not support resolution 2K, …`, without an `error.code`). Call these two endpoints only with `minimax/minimax-h3`.
- In a form submission, `input_reference` accepts only a URL. Currently an uploaded file **raises no error**: the file is ignored, and the task is generated as if there were no reference video and charged as usual. Only a non-URL text value (such as `video.mp4`) returns a 400 plain-text error: `input_reference must be a URL; uploading a file is not supported by this gateway (pass a publicly reachable https:// URL instead)`. These plain-text errors from the gateway currently still carry the response header `Content-Type: application/json`, so check whether the body is JSON before parsing it.
- A duration out of range, mixing image to video with omni reference, non-compliant reference materials and similar problems are rejected by the provider, and the status code and error text are set by the provider; problems found only after submission come back as failed tasks, as described under "Retrieve a task" above. The full list is in [Errors and request identifiers](https://platform.lyrouter.com/ai_gateway/docs/errors/en).

## FAQ

### The error says the short edge must be 480 or 768. What should I do?

**Answer:** MiniMax resolutions are discrete tiers, not arbitrary sizes. Write the tier name directly (such as `"768P"` or `"2K"`), or use a size whose short edge is exactly 480 / 768 (such as `1366x768`). `2K` can only be written as the tier name.

### Can H3-Max do video upscaling or context enhancement?

**Answer:** No. These two endpoints support only `minimax/minimax-h3`. Currently the gateway does not reject H3-Max outright: video upscaling is accepted and a task is created; context enhancement gets the provider's 400 passed through (`invalid params, model MiniMax-H3-Max does not support resolution 2K, …`, without an `error.code`). H3-Max also does not support 2K or a 4-second duration. Moreover, H3-Max has been taken offline by the provider, so even regular video generation is currently unavailable (a call returns HTTP 200, but the task object is empty and has no task ID); use `minimax/minimax-h3` instead.

### The upscaled video does not look right. What should I do?

**Answer:** Check three things: the source video is a 768P video produced by H3; `prompt` is the exact final prompt that was sent to the model when the source video was generated (if context enhancement was used, pass the enhanced prompt); and the reference materials match the ones used then. When these do not match, neither the gateway nor the provider reports an error; you just do not get the expected result.

### Context enhancement completed. Why can't I download a video?

**Answer:** Context enhancement produces only text, found in the task object's `output_text`; there is no video to download, and calling `/content` on it currently returns HTTP 500. Submit `output_text` as the `prompt` of `POST /v1/videos` to generate a video.

### I sent a reference video. Why are the billed seconds longer than the video?

**Answer:** MiniMax bills input plus output seconds: the seconds of the reference video also count toward `usage.duration_seconds`. Reference audio is free, and the first 5 reference images are free, with each one after that charged.

### I sent a reference video and the task failed with `aspect ratio "adaptive" is not supported`. What should I do?

**Answer:** The full error is `h3submit: aspect ratio "adaptive" is not supported for task ref2va`. With a reference video (`input_reference`), MiniMax requires an explicit aspect ratio, but when `size` is a tier name (such as `480P`) the gateway sends an adaptive ratio. Change `size` to `WxH` with a short side of 480 or 768, such as `854x480` or `1366x768`, and submit again. Failed tasks like this are not charged.

### The video URL has expired. Can I download it again?

**Answer:** No. The video URL is fixed when the task completes, and calling `/content` again returns the same URL, so after it expires you need to submit a new task. Download the video or copy it to your own storage right after the task completes.

### The query keeps returning `in_progress`. What should I do?

**Answer:** Video generation usually takes several minutes. A task still unfinished 24 hours after submission will never produce a result and is not charged, so you can stop polling and check the task's state by task ID on the **Multimedia generation** page in the console. Do not resubmit the same request while you wait: each submission creates a new task that is charged separately.
