
MiniMax H3 is hiapi's native-2K video model, and it does more than plain text-to-video: the same model ID also does first/last-frame control and multimodal reference-to-video, switched purely by which input fields you send. This guide covers the minimal working request, a complete Python script, and the production details — callbacks, mode constraints, and the exact errors you'll hit.
Goal: send a prompt to POST /v1/tasks, poll until the task finishes, and download a 4-15 second 2K MP4 from data.output[0].url.
You need one thing:
Everything else — model routing, task polling, output storage — goes through the same unified async task API that every hiapi model uses, so this flow transfers directly if you swap in a different model id.
Create the task:
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-<your-key>" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-h3",
"input": {
"prompt": "A paper kite drifting gently above a green field under soft daylight, stable camera, no text."
}
}'
Three things worth knowing before you send it:
model is the bare id minimax-h3 — no vendor prefix, no version suffix.input.prompt is the only required field. Leave everything else out and you get the defaults: 5 seconds, native 2K resolution, 16:9 aspect ratio, no watermark.input.duration is an integer number of seconds from 4 to 15. Send 99 and the API answers invalid input: duration: maximum: got 99, want 15.The create response is a small envelope; the field you need is data.taskId. Poll it:
curl -s https://api.hiapi.ai/v1/tasks/<taskId> \
-H "Authorization: Bearer sk-<your-key>"
Read data.status. "success" and "fail" are the only terminal states — anything else means keep polling. On success, the clip lives at data.output[0].url.
Output URLs are signed and carry an expireAt. Download the MP4 to your own storage as soon as the task succeeds; don't hotlink the temp URL or store it for later.
import time
import requests
API = "https://api.hiapi.ai/v1/tasks"
KEY = "sk-<your-key>"
HEADERS = {"Authorization": f"Bearer {KEY}"}
payload = {
"model": "minimax-h3",
"input": {
"prompt": "A paper kite drifting gently above a green field under soft daylight, stable camera, no text.",
"duration": 6,
},
}
resp = requests.post(API, json=payload, headers=HEADERS, timeout=60)
resp.raise_for_status()
task_id = resp.json()["data"]["taskId"]
print("task created:", task_id)
deadline = time.time() + 600 # video tasks can take a few minutes
while time.time() < deadline:
task = requests.get(f"{API}/{task_id}", headers=HEADERS, timeout=30).json()["data"]
status = task.get("status")
if status == "success":
video_url = task["output"][0]["url"] # signed URL with expireAt
clip = requests.get(video_url, timeout=120).content
with open("minimax-h3-clip.mp4", "wb") as f:
f.write(clip)
print("saved minimax-h3-clip.mp4")
break
if status == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
time.sleep(5)
else:
raise TimeoutError("task did not finish within 10 minutes")
minimax-h3 picks its mode from which input fields you populate — don't mix fields from different modes in one request:
| Mode | Fields | Notes |
|---|---|---|
| Text-to-video | prompt, duration, aspect_ratio | Default mode; just send a prompt |
| First/last-frame control | first_frame_image, last_frame_image | last_frame_image requires first_frame_image to also be set |
| Multimodal reference | image_urls, video_urls, audio_urls | Up to 5 images, 3 videos (2-15s each), 3 audio clips; audio alone isn't valid — pair it with at least one image or video reference |
First/last-frame example:
{
"model": "minimax-h3",
"input": {
"prompt": "The camera slowly pushes forward as morning light transitions naturally into golden sunset.",
"duration": 6,
"first_frame_image": "https://example.com/first-frame.jpg",
"last_frame_image": "https://example.com/last-frame.jpg"
}
}
Multimodal reference example:
{
"model": "minimax-h3",
"input": {
"prompt": "Keep the reference subject consistent and follow the camera rhythm of the reference clip for a polished product showcase.",
"duration": 8,
"image_urls": ["https://example.com/product.jpg"],
"video_urls": ["https://example.com/camera-motion.mp4"],
"audio_urls": ["https://example.com/rhythm.mp3"]
}
}
All three modes take public HTTPS URLs, not file uploads — host any local files on a CDN or object storage first.
Callbacks instead of polling. For anything beyond ad hoc scripts, add a top-level callback object to the create request instead of polling:
{
"model": "minimax-h3",
"input": { "prompt": "A paper kite drifting gently above a green field under soft daylight." },
"callback": { "url": "https://your-server.example.com/hooks/hiapi", "when": "final" }
}
callback.when only supports "final" — you get one call when the task reaches a terminal state, covering both success and failure. Treat delivery as at-least-once: key your handler on taskId and make it idempotent, and keep a slow polling loop as a fallback in case a callback is missed.
Errors you'll actually see:
| Response | Meaning | What to do |
|---|---|---|
401 permission_denied | Key is invalid or can't use this model | Check the key in the dashboard; the error includes a request_id for support |
400 INVALID_REQUEST | Input failed validation | The message names the exact field, e.g. duration: maximum: got 99, want 15 — fix and resend |
400 INVALID_REQUEST (frame combo) | last_frame_image sent without first_frame_image | Set first_frame_image, or drop last_frame_image if you only need a starting frame |
400 MODEL_UNAVAILABLE | Model id doesn't exist or was removed | Double-check the bare model id against the model page |
503 TEMPORARILY_UNAVAILABLE | Transient upstream issue at create time | Retry with backoff — no task was created, so retrying is safe |
task status: "fail" | Generation itself failed | Read data.error.code / message; failed tasks are safe to resubmit |
Retries are cheap before create, careful after. A 400/503 at create time means no task exists yet — retry freely. Once you have a taskId, poll or wait for the callback instead of blind-resubmitting, or you can end up paying for duplicate generations.
How long can the generated video be?
duration accepts integers from 4 to 15 seconds, defaulting to 5. Values outside that range are rejected at create time with a 400 that names the limit.
What resolution does minimax-h3 generate at?
Native 2K only — it's currently the sole supported value for input.resolution.
Can I control the first and last frame of the clip?
Yes. Set first_frame_image for the opening frame, and optionally last_frame_image for the closing frame — but last_frame_image requires first_frame_image to be set too.
Can I combine frame control with reference images?
No. Frame-control fields (first_frame_image, last_frame_image) and reference-media fields (image_urls, video_urls, audio_urls) are separate modes — mixing them isn't supported.
Do I have to poll, or can I get pushed a result?
Both work. Polling GET /v1/tasks/<taskId> is simplest for scripts; for production, add callback: {"url": ..., "when": "final"} and receive one POST when the task reaches a terminal state.
How long is the output URL valid?
It's a signed URL with an expireAt timestamp. Download the file immediately after status turns "success" and store it yourself.
What does it cost? Video models are priced per generation depending on duration and settings — see the current numbers on the pricing page.
Key Takeaways