MiniMax H3 video generation
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.
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 |
minimax/minimax-h3-maxhas 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. Useminimax/minimax-h3instead.
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:
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.
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 |
Submit a task
/v1/videosSubmits 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_referenceorreference_audio, you cannot also sendfirst_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-Parametersresponse header lists their names; withX-Gateway-Strict-Parameters: true, the request returns 400 instead. This includes MiniMax's nativeduration,resolution,ratioandcontent; useseconds,sizeand 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_referenceaccepts only a URL; an uploaded file raises no error but is ignored: pass a publicly reachablehttps://URL. Currently, when you upload a file, the task is generated as if there were no reference video and is charged as usual.watermarkaccepts only the literal valuestrueorfalse.- Submit
first_frame,last_frame,reference_imageandreference_audioin a JSON request body. Currently these fields in a form are replaced with a placeholder value and forwarded to the provider; withreference_audio, for example, the submission returns 200 and the task then fails with theerror.messagecontent[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.
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
}'
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"])
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.
{
"id": "aigwt_minimax_videos_Qm3Tz8VbN1sLk0pYc4dReA",
"object": "video",
"model": "MiniMax-H3",
"status": "queued",
"progress": 0,
"created_at": 1791950400,
"expires_at": 1792555200,
"seconds": 5,
"size": "768P"
}
{
"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"
}
}
{
"error": {
"message": "Model is not available in the model plaza",
"type": "access_denied",
"code": "model_not_available"
}
}
{
"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_fullis shared per provider account, so switching to another API key does not help; resubmit later.
Retrieve a task
/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
curl https://api.lyrouter.com/v1/videos/$TASK_ID \
-H "Authorization: Bearer $LYROUTER_API_KEY"
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"))
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.
{
"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 }
}
{
"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 }
}
{
"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"
}
}
{
"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.codeis the provider's error code; when the provider gives no recognizable code, it isupstream_error.error.retryablefalsemeans retrying unchanged will not succeed,truemeans you can retry, and a missing field means the gateway cannot tell. For all values, see Asynchronous tasks. - 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
queuedorin_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
/v1/videos/{id}/contentCall 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
# 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"
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)
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
Status: 302
Location: <signed video URL provided by the provider>
{
"error": {
"message": "video is not ready yet",
"type": "invalid_request_error",
"code": "not_ready"
}
}
{
"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, so query until the status is
completedbefore downloading.
Video upscaling
/v1/videos/regenerationsRegenerates 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 400seconds is not supported for video regeneration (the source video determines the duration). generate_audio,output_formatandomni_reference_task_typelikewise 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
promptand 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
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"
}'
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"])
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.
{
"id": "aigwt_minimax_videos-regenerations_Hk2Lp9WqR4tYv7Xz1Bc3Nd",
"object": "video",
"model": "MiniMax-H3",
"status": "queued",
"progress": 0,
"created_at": 1791950400,
"expires_at": 1792555200,
"size": "2K"
}
{
"error": {
"message": "source_video is required for video regeneration (the 768P video to upscale)",
"type": "invalid_request_error",
"code": "invalid_parameter"
}
}
Context enhancement
/v1/videos/contextTurns 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.
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 withminimax/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
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
}'
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"])
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.
{
"id": "aigwt_minimax_videos-context_Zp8Rk2Vt5Lq9Wx3Ny6Bh1A",
"object": "video",
"model": "MiniMax-H3",
"status": "queued",
"progress": 0,
"created_at": 1791950400,
"expires_at": 1792555200,
"seconds": 5
}
{
"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
/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 |
1280x720has a short edge of 720, which is not a tier, so it is rejected; for 768P write1366x768in landscape or768x1366in portrait.2Khas 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 400size 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
480Por854x480produce 1038×576 (a short edge of 576, not 480);768Pproduces 1344×768; first-and-last-frame image to video with a 1:1 image and854x480produces 768×768; upscaling to2Kproduces 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_secondson 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 aduration_secondsof 8). Reference audio is free. Video upscaling does not add them up:duration_secondscounts 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 inusage; you can see the cost in the logs. - Context enhancement is billed on input and output tokens separately. The task object's
usagelists onlycompletion_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;failedtasks (including timeouts) are not charged. To see a task's cost in the logs, see Asynchronous tasks.
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).
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
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"))
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 400source_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 anerror.code). Call these two endpoints only withminimax/minimax-h3. - In a form submission,
input_referenceaccepts 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 asvideo.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 headerContent-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.
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.