MiniMax H3 Max has two model ids on HiAPI: minimax-h3-max/text-to-video and minimax-h3-max/image-to-video. Both generate 0.92–15 second videos at 480P, 768P or 1080P, billed per requested second ($0.072, $0.115 and $0.229). Start with the minimal request below, then add callbacks and idempotency for an application integration. Use this prompt → Select 6 seconds, 768P, and 16:9 manually.
What you'll build
A script that submits a text-to-video job to minimax-h3-max/text-to-video, waits for it to finish, and prints the resulting video URL for download. The examples below follow the current text-to-video and image-to-video API references.
Prerequisite: an API key. Grab one from the hiapi dashboard and export it:
export HIAPI_API_KEY="sk-..."
Every request authenticates with Authorization: Bearer sk-....
The minimal request
Both H3 Max ids run on hiapi's unified async task endpoint. You create a task, then poll (or get a callback) until it finishes.
curl
a red panda sipping tea on a rainy Tokyo balcony, cinematic lighting
curl --fail-with-body --connect-timeout 10 --max-time 60 -X POST "https://api.hiapi.ai/v1/tasks" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-h3-max/text-to-video",
"input": {
"prompt": "a red panda sipping tea on a rainy Tokyo balcony, cinematic lighting",
"duration": 6,
"resolution": "768P",
"aspect_ratio": "16:9"
}
}'
A successful submission returns data.taskId; save it before polling. This is the response shape:
{"code": 200, "data": {"taskId": "tk-hiapi-..."}, "message": "success"}
If submission times out or its response is lost, check task history before another POST. Once you have the ID, keep querying that task until success or fail:
curl --fail-with-body --connect-timeout 10 --max-time 60 "https://api.hiapi.ai/v1/tasks/tk-hiapi-..." \
-H "Authorization: Bearer $HIAPI_API_KEY"
When data.status is success, select the entry with type: "video" in data.output and read its url. Download it before that entry's expireAt deadline (Unix seconds). Stop on fail and inspect data.error.
Use this prompt → For this text-only example, select 6 seconds and 768P.
Input fields that actually work
Use the current documented input fields:
| Field | Type | Notes |
|---|---|---|
prompt | string | required; up to 50,000 characters |
duration | number | 0.92–15 seconds, decimals allowed; default 5 |
resolution | enum | "480P" | "768P" | "1080P"; default "768P" |
aspect_ratio | enum | text-to-video only: "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"; default "16:9" |
first_frame_image | string | image-to-video: public image URL for the opening frame |
last_frame_image | string | image-to-video: public image URL for the ending frame; can also be used alone |
target_audio_url | string | optional; replaces the soundtrack with your own audio (at least 2 seconds) |
prompt_expansion_mode | enum | "disabled", "balanced" (default), "quality" |
To guide the opening, ending, or both, switch to minimax-h3-max/image-to-video, which takes frame URLs instead of aspect_ratio (the frame sets the ratio):
{
"model": "minimax-h3-max/image-to-video",
"input": {
"prompt": "a red panda sipping tea on a rainy Tokyo balcony, cinematic lighting",
"duration": 6,
"resolution": "768P",
"first_frame_image": "https://your-domain.com/images/opening.jpg",
"last_frame_image": "https://your-domain.com/images/ending.jpg"
}
}
Replace the image URL placeholders with accessible files. For a portrait clip from text, set "aspect_ratio": "9:16" on the text-to-video id; for image-to-video, use a portrait frame and the output follows it.
Python
import os
import time
import urllib.request
import urllib.error
import json
API_KEY = os.environ["HIAPI_API_KEY"]
BASE = "https://api.hiapi.ai/v1/tasks"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def request(method, url, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, headers=HEADERS, method=method)
with urllib.request.urlopen(req, timeout=30) as response:
result = json.load(response)
if not isinstance(result, dict) or result.get("code") != 200:
raise ValueError(f"Unexpected API response: {result!r}")
if not isinstance(result.get("data"), dict):
raise ValueError("Missing data object")
return result["data"]
payload = {
"model": "minimax-h3-max/text-to-video",
"input": {
"prompt": "a red panda sipping tea on a rainy Tokyo balcony, cinematic lighting",
"duration": 6,
"resolution": "768P",
"aspect_ratio": "16:9"
}
}
try:
created = request("POST", BASE, payload)
task_id = created.get("taskId")
if not isinstance(task_id, str) or not task_id.strip():
raise ValueError("Missing taskId")
except (OSError, ValueError) as exc:
raise SystemExit(
f"Submission outcome is unconfirmed: {exc}. "
"Check task history before another POST; do not blindly resubmit."
)
print("taskId:", task_id, flush=True)
deadline = time.monotonic() + 600
while time.monotonic() < deadline:
try:
task = request("GET", f"{BASE}/{task_id}")
except urllib.error.HTTPError as exc:
if exc.code in (400, 401, 403, 404):
raise SystemExit(
f"Query rejected ({exc.code}). Correct URL/auth, then GET existing task {task_id}."
)
print(f"Query error: {exc}; keep querying task {task_id}", flush=True)
time.sleep(5)
continue
except (OSError, ValueError) as exc:
print(f"Query error: {exc}; keep querying task {task_id}", flush=True)
time.sleep(5)
continue
status = task.get("status")
if status not in ("queued", "handling", "archiving", "success", "fail"):
raise RuntimeError(f"Unexpected task status {status!r}; keep task ID {task_id}")
if status == "success":
output = task.get("output")
if not isinstance(output, list):
raise RuntimeError(f"Invalid output; keep task ID {task_id}")
videos = [item["url"] for item in output
if isinstance(item, dict) and item.get("type") == "video"
and isinstance(item.get("url"), str) and item["url"]]
if not videos:
raise RuntimeError(f"No video URL; keep task ID {task_id}")
print(videos[0])
break
if status == "fail":
raise RuntimeError(f"Task {task_id} failed: {task!r}")
time.sleep(5)
else:
raise TimeoutError(f"Keep querying existing task {task_id}; do not POST again")
Production patterns
A one-off script can poll in a loop. A production integration needs three more things.
Use a callback instead of polling. Add a callback object to the create-task request and hiapi will POST to it once the task reaches a terminal state — no polling loop, no wasted requests:
{
"model": "minimax-h3-max/text-to-video",
"callback": {"url": "https://your-app.example.com/hiapi/callback", "when": "final"},
"input": {"prompt": "...", "duration": 6, "resolution": "768P", "aspect_ratio": "16:9"}
}
when currently only supports "final" (fires on both success and failure) and defaults to it if omitted. Polling is simpler for a script you run by hand; a callback is the right call once you're submitting more than a handful of jobs per minute, because callbacks reduce query traffic and simplify notification handling. Polling the existing task does not create another generation charge.
Set an Idempotency-Key header. The create-task endpoint accepts an optional Idempotency-Key header (up to 255 bytes). Retrying a request with the same key under the same account creates the task only once — a replay returns the original taskId instead of billing a second render. Use this any time a request might be retried after a timeout, so a network hiccup doesn't turn into a duplicate charge:
curl --fail-with-body --connect-timeout 10 --max-time 60 -X POST "https://api.hiapi.ai/v1/tasks" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Idempotency-Key: order-42-video-render" \
-H "Content-Type: application/json" \
-d '{"model": "minimax-h3-max/text-to-video", "input": {"prompt": "...", "duration": 6, "resolution": "768P", "aspect_ratio": "16:9"}}'
Handle the two real error shapes. An invalid or missing API key returns HTTP 401:
{"error": {"code": "permission_denied", "message": "...", "request_id": "...", "type": "hiapi_error"}}
An invalid field (bad enum value, wrong type, out-of-range duration) returns HTTP 400:
{"code": 400, "data": null, "error_code": "INVALID_REQUEST", "message": "..."}
Branch on these explicitly — permission_denied means fix your key, INVALID_REQUEST means fix your payload, and neither is worth retrying without a change.
Related docs
- H3 Max text-to-video and image-to-video model pages — full parameter reference and live pricing
- Create Task — the request envelope, callback, and idempotency semantics used above
- Get Task Detail — the polling response shape
- Authentication and Quickstart — if this is your first call to hiapi
- hiapi pricing — current per-second rate for H3 Max at 480P, 768P and 1080P
FAQ
Is MiniMax H3 Max a chat or language model? It is a video model: supply a prompt, optionally guide the opening or ending with images, and receive a video output.
Does MiniMax H3 Max support image-to-video?
Yes, through minimax-h3-max/image-to-video. Use first_frame_image, last_frame_image, or both alongside prompt; the frame sets the aspect ratio.
What resolutions and aspect ratios are supported?
resolution is "480P", "768P" or "1080P". Text-to-video accepts 21:9, 16:9, 4:3, 1:1, 3:4, and 9:16. Image-to-video has no aspect_ratio field; the frame image determines the ratio.
How much does MiniMax H3 Max cost? HiAPI charges per requested second: $0.072 at 480P, $0.115 at 768P and $0.229 at 1080P, on both ids. A 6-second clip is $0.432, $0.69 or $1.374. See the H3 and H3 Max pricing guide for batch budgets.
Should I poll or use a callback?
Poll for scripts and low-volume testing. Switch to callback.url once you're generating enough clips that a polling loop would waste requests or add latency to your own workflow.
Why did I get a 401 with permission_denied?
Your Authorization: Bearer sk-... header is missing, malformed, or the key is invalid/revoked. Regenerate a key from the dashboard and confirm the header is set exactly as shown above.
Why did I get a 400 with INVALID_REQUEST?
One of your input fields doesn't match the schema — usually an out-of-range duration or a resolution/aspect_ratio value outside the enum. The error message names the offending field.
Can I use this from Node.js instead of Python or curl?
Yes — it's the same POST /v1/tasks / GET /v1/tasks/{taskId} pair with any HTTP client; only the request-building syntax changes.
Copy the red-panda example and Use this prompt →. For more scene ideas, see four H3 and H3 Max prompt examples.





