Calling the MiniMax H3 video API from curl and Python
Updated 2026-10-02
This is the practical call sequence for the MiniMax H3 family (minimax/h3, minimax/h3-max, minimax/h3-max-turbo) through VideoRouter's POST /videos endpoint: create, poll, download, then the parts that bite in production, namely host pinning and error handling. Everything below uses the base URL https://videorouter.sh/api/v1 and a key of the form llmr_sk_live_....
How the job lifecycle works
Video generation is always asynchronous. POST /videos returns immediately with a job id and a status of queued. The job moves through in_progress to either completed or failed. You are billed once, at creation, from the requested duration. Polling GET /videos/{id} is free, and a job that fails upstream is not billed. That billing shape drives how you should write the client: creation is the expensive call, so it is the one you must not repeat carelessly.
Create, poll, download with curl
API=https://videorouter.sh/api/v1
KEY=llmr_sk_live_...
job=$(curl -s "$API/videos" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "minimax/h3",
"prompt": "a paper airplane gliding over a city at dawn, slow tracking shot",
"duration_secs": 5,
"aspect_ratio": "16:9"
}')
id=$(echo "$job" | jq -r .id)
while true; do
job=$(curl -s "$API/videos/$id" -H "Authorization: Bearer $KEY")
status=$(echo "$job" | jq -r .status)
[ "$status" = "completed" ] || [ "$status" = "failed" ] && break
sleep 5
done
echo "$job" | jq -r 'if .status == "completed" then .data[0].url else .error end'
A five-second poll interval is plenty because generation takes far longer than that. When the job completes, data[0].url holds the video. Download it to your own storage right away rather than treating the returned URL as permanent.
The same flow in Python
import time
import requests
API = "https://videorouter.sh/api/v1"
HEADERS = {"Authorization": "Bearer llmr_sk_live_...", "Content-Type": "application/json"}
class VideoError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status, self.code = status, code
def create(model, prompt, **fields):
r = requests.post(f"{API}/videos", headers=HEADERS, timeout=30,
json={"model": model, "prompt": prompt, **fields})
if r.status_code >= 400:
err = r.json().get("error", {})
raise VideoError(r.status_code, err.get("code"), err.get("message"))
return r.json()
def wait(job_id, every=5, limit=1200):
deadline = time.time() + limit
while time.time() < deadline:
job = requests.get(f"{API}/videos/{job_id}", headers=HEADERS, timeout=30).json()
if job["status"] in ("completed", "failed"):
return job
time.sleep(every)
return None # still running; do not resubmit
job = create("minimax/h3", "a paper airplane gliding over a city", duration_secs=5)
done = wait(job["id"])
if done and done["status"] == "completed":
open("clip.mp4", "wb").write(requests.get(done["data"][0]["url"]).content)
Two details are deliberate. create turns the OpenAI-style error envelope into an exception carrying the code, so callers can branch on 429 versus 402 without parsing strings. And wait returns None on timeout instead of raising or retrying, because a slow job is still running and still billed.
Image-to-video and the other fields
The fields are the same across the three H3 ids. Add start_image_url (a public https URL or a data:image/...;base64, URI) to animate a starting frame, or input_references for reference-to-video on the hosts that support it. duration_secs is snapped to a value the model supports, so read the billed seconds from the response instead of assuming your request was honoured. Combining aspect_ratio and resolution requests a size; an unsupported combination is ignored and the model default is used, not rejected, so check the output dimensions. The variant guide covers which id to choose.
Pinning a host
With no host specified, the request goes to the cheapest healthy host and fails over automatically. You have three ways to constrain that:
"model": "minimax/h3/fal"prefers that host first but keeps the rest of the pool as a fallback."provider": {"only": ["fal", "atlascloud"]}restricts routing to hosts you reviewed, and still fails over among them."provider": {"only": ["fal"], "allow_fallbacks": false}makes exactly one attempt on one host.
Pin only for a reason such as data terms or run-to-run consistency, because every pin narrows your failover options. The response names the host that took the job in provider; log it. The reliability guide goes deeper on timeouts and hedging, and the price table shows what hosts cost for each variant.
Errors you will see
| Status | Meaning | What to do |
|---|---|---|
| 400 | Bad request, such as a missing prompt or an unknown model id | Fix the request. Never retry unchanged. |
| 401 | Key missing, malformed, revoked or expired | Check the key. |
| 402 | Prepaid balance at or below zero, or the key's monthly spend cap reached (the code tells you which) | Top up or raise the cap. |
| 403 | Model not in the key's allow-list, or missing scope | Change key settings. |
| 429 | Rate limit on the key | Wait the number of seconds in Retry-After. |
| 500, 502, 503, 504 | upstream_error: every candidate host failed. Not billed. | Back off and retry creation a few times. |
Every error uses the OpenAI-style envelope {"error": {"message", "type", "code"}}. A job that reaches failed carries the provider's message in error and data is null. Read it before retrying, since a content-policy rejection will fail the same way again.
Before you ship
- Store the job id the moment creation returns, before anything else can fail.
- Download results promptly and keep your own copy.
- Set a monthly spend cap on the key so a bug becomes a 402, not a bill.
- Keep the model id in config so you can move between H3, H3 Max and H3 Max Turbo.
The quickstart has the shortest possible call, and you can get a key at videorouter.sh/signup.
Frequently asked questions
What is the endpoint and model id for MiniMax H3?
Send POST https://videorouter.sh/api/v1/videos with model set to minimax/h3, minimax/h3-max or minimax/h3-max-turbo. The call returns a job id; you poll GET /videos/{id} for the result.
How do I know when the video is ready?
Poll until status is completed or failed. Polling is free, and the file URL is in data[0].url once the job completes.
Am I billed for failed jobs?
Jobs that fail upstream are not billed. A job that completes is billed even if you discard the result, and billing happens once at creation from the requested duration.
How do I force a specific host?
Use provider.only with allow_fallbacks set to false. The minimax/h3/fal suffix is only a preference and still allows fallback to other hosts.
Keep reading
- MiniMax H3 vs H3 Max vs H3 Max Turbo — Which Variant for Which Job
- MiniMax Video API Use Cases: What to Build with the H3 Family
- MiniMax vs Kling vs Seedance API: A Decision Framework
- MiniMax API Failover and Reliability: Pinning, Timeouts, Retries
VideoRouter puts it next to dozens of other video and image models behind one API key, so you can compare providers, prices and fail over automatically. Compare providers on VideoRouter →