AI Video Sensei
Just in

๐Ÿ MiniMax H3 API in Python: Setup, Polling and Errors

A working H3 client in Python โ€” auth, the v2 payload shape, all five modes, polling, and the six errors that actually bite. Built from the official reference.

Jordan Reyes ยท AI Video Producer

ยท 5 min read

โœ“ Fact-checked & production-testedBased on our own paid generations and published videos. Last reviewed 2026-08-01.How we test โ†’
MiniMax H3 API in Python: Setup, Polling and Errors

We built a full H3 client this week, reading the v2 reference endpoint by endpoint rather than trusting the launch blog. Three things in it are not what you would guess from the older Hailuo API, and one of them will silently cost you money. Here is the working version.

By the numbers

Value
Model IDMiniMax-H3 (literal string)
Base URLhttps://api.minimax.io (global) / api.minimaxi.com (mainland)
CreatePOST /v2/video_generation
QueryGET /v2/query/video_generation/{task_id}
UploadPOST /v1/files/upload
Poll interval10 seconds (official recommendation)
Prompt cap7,000 characters
Body cap64 MB (video โ‰ค50MB, image โ‰ค30MB, audio โ‰ค15MB)
Duration4โ€“15 seconds, integers only
Output2K, 24fps, native stereo audio
Task retention7 days

The minimum working call

import os, time, requests

BASE = "https://api.minimax.io"
HEAD = {"Authorization": f"Bearer {os.environ['MINIMAX_API_KEY']}",
        "Content-Type": "application/json"}

payload = {
    "model": "MiniMax-H3",
    "content": [{"type": "text", "text": "A boy playing basketball by the sea"}],
    "resolution": "2K",     # required, and "2K" is the only accepted value
    "duration": 5,          # required, 4-15, integer
    "ratio": "16:9",        # required for text-to-video, cannot be "adaptive"
}

task_id = requests.post(f"{BASE}/v2/video_generation",
                        headers=HEAD, json=payload).json()["task_id"]

while True:
    time.sleep(10)
    task = requests.get(f"{BASE}/v2/query/video_generation/{task_id}",
                        headers=HEAD).json()["task"]
    if task["status"] == "succeeded":
        open("out.mp4", "wb").write(requests.get(task["content"]["url"]).content)
        break
    if task["status"] in ("failed", "cancelled", "expired"):
        raise RuntimeError(task.get("error"))

If you have not picked a route yet, where to run H3 compares MiniMax direct against fal, ComfyUI, Vercel AI Gateway and the reseller tier โ€” the code above targets MiniMax's own API, which is the base rate everything else marks up.

There are exactly six top-level fields on this endpoint. model, content, resolution and duration are all required; only ratio and callback_url are optional. There is no seed, no prompt_optimizer, no watermark, no n, and no audio toggle. Those exist on the v1 Hailuo endpoints and sending them here is a 400.

The five modes, and the one everyone misses

The mode is implied by what you put in content[], not by a parameter.

# 1. text-to-video โ€” ratio required, must be concrete
[{"type": "text", "text": prompt}]

# 2. first frame
[{"type": "text", "text": prompt},
 {"type": "image_url", "image_url": {"url": U}, "role": "first_frame"}]

# 3. LAST FRAME ONLY โ€” a valid standalone mode
[{"type": "text", "text": prompt},
 {"type": "image_url", "image_url": {"url": U}, "role": "last_frame"}]

# 4. first + last, interpolated between
# 5. reference โ€” up to 9 images + 3 videos + 3 audio, 12 files total
[{"type": "text", "text": prompt},
 {"type": "image_url", "image_url": {"url": A}, "role": "reference_image"},
 {"type": "audio_url", "audio_url": {"url": B}, "role": "reference_audio"}]

Most write-ups list four modes. Last-frame-only is a documented standalone mode โ€” you hand H3 the frame you are cutting on and it invents the approach. Our own validator rejected it until we read the create reference properly.

Two structural rules: frames and references are mutually exclusive in one request, and every request needs a non-empty text item regardless of mode. role is optional on images (an image with no role is treated as the first frame) but required on video and audio items.

Errors are HTTP codes now, not base_resp

If you are porting v1 code, this is the change that breaks everything quietly. v1 returns HTTP 200 with a base_resp.status_code of 1004 or 1026. v2 returns real HTTP status codes:

CodeTypeCause
400bad_request_errorMissing text, ratio on a t2v call, duration outside 4โ€“15
401authorized_errorKey/host region mismatch
402insufficient_balance_errorTop up
422unprocessable_entity_errorSensitive content โ€” rewrite the prompt
429rate_limit_errorOver the concurrency cap
500server_errorRetry
{"type": "error",
 "error": {"type": "rate_limit_error",
           "message": "rate limit, please retry later (1002)", "http_code": 429},
 "request_id": "..."}

The legacy numeric codes ride along inside error.message as a (1002) suffix, and inside task.error.code on a failed task. Worth decoding: 1026 is a sensitive prompt, 1027 is sensitive output.

Only 429 and 5xx are worth retrying. 400, 401, 402 and 422 are deterministic โ€” a retry loop on those just burns wall-clock. And a task can fail after a 200, so always check status as well as the HTTP code.

Rate limiting is a concurrency cap, not RPM

Two in-flight tasks on the free tier, fifteen on paid. That is a cap on simultaneous jobs, not requests per minute โ€” different from Hailuo v1's 5/20 RPM. A worker pool sized to your tier is the right shape:

from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=2) as pool:   # 15 on a paid plan
    list(pool.map(render_one, shots))

The billing detail that will surprise you

usage.total_seconds = input_seconds + output_seconds, and reference video duration bills as input. A 10-second reference clip on a 5-second render bills 15 seconds, not 5.

Read the real figure off the finished task rather than assuming it equals your duration:

u = task["usage"]
print(u["total_seconds"], u["input_seconds"], u["output_seconds"], u["input_image_count"])

MiniMax has published no official rate card; third parties list 2K from roughly $0.13/sec. Budget against the returned seconds, not the requested ones โ€” H3 vs Veo 3.1 works through what that means against a model that does publish prices.

Upload large assets instead of inlining them

Base64 inflates a file by about a third and the body caps at 64MB, so a 50MB reference video cannot be inlined at all.

r = requests.post(f"{BASE}/v1/files/upload",
                  headers={"Authorization": HEAD["Authorization"]},
                  data={"purpose": "video_generation_input"},
                  files={"file": open("hero.png", "rb")})
ref = f"mm_file://{r.json()['file']['file_id']}"     # retained 7 days

video_generation_input is the correct purpose value โ€” video_understanding is a different thing for multimodal chat.

How we built this

We wrote a client against MiniMax's published v2 create, query and file-upload references, reconciling each documented limit against the schemas rather than the marketing page. That is where the fifth mode, the HTTP error envelope and the reference-billing rule surfaced.

To be clear about what this is: the payloads and validation are checked against the official spec and exercised offline, not against a live billed account. We have not published render times or output-quality claims here because we have not measured them. Everything above is API behaviour as documented.

What didn't make the cut

A retry-with-jitter wrapper and a webhook receiver. callback_url works โ€” MiniMax POSTs a challenge your endpoint must echo unchanged within 3 seconds, then pushes on every status change โ€” but retry behaviour and signing are undocumented, so polling remains the reliable path. We left the receiver out rather than publish an untested handler.

For what the reference stack can actually do once you are connected, see the omni-reference workflow.

Frequently asked questions

โ–ธWhy does my MiniMax API key return 401?

Almost always a region mismatch. MiniMax runs two hosts โ€” api.minimax.io for global and api.minimaxi.com for mainland China โ€” and a key issued for one returns 401 authorized_error against the other. Check which console you created the key in before debugging anything else.

โ–ธDoes H3 use the same API as Hailuo 2.3?

No, and this is the biggest migration trap. Hailuo 2.3 uses v1 with a flat payload and a base_resp error envelope, then a separate file-retrieve call to get your video. H3 uses v2 with a multimodal content array, HTTP status codes for errors, and returns the download URL directly on the query response. Code written for v1 will not work.

โ–ธHow long should I poll for?

MiniMax's docs recommend a 10-second interval. Statuses are queued, running, succeeded, failed, cancelled and expired โ€” treat the last four as terminal. Tasks are only queryable for 7 days, and the download URL is time-limited but renewable: re-issue the same query to get a fresh one.

โ–ธCan I send local files, or do they need to be URLs?

Three forms work: a public https URL, an mm_file://{file_id} reference from the upload endpoint, or a base64 data URI. The request body caps at 64MB total and base64 inflates assets by about a third, so upload anything substantial and pass the mm_file reference instead.

The 5 best AI video finds, every week

New models, tested prompts, and what actually worked in our production โ€” one short email a week. No spam, unsubscribe anytime.

Written by Jordan Reyes

AI Video Producer

Runs multiple faceless YouTube channels and tests every major AI video model against the same prompts before recommending one. Tracks render time and credit cost like other people track calories.

Explore these topics

Every guide, comparison and prompt library we have on each.

#minimax h3 api python#minimax h3 api example#minimax video generation api#minimax h3 api key#hailuo 3 api tutorial
Next in MiniMax H3The 8 Best AI Video Generators, Tested in Production

Keep learning