Home › Guides › How to call the API

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:

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

StatusMeaningWhat to do
400Bad request, such as a missing prompt or an unknown model idFix the request. Never retry unchanged.
401Key missing, malformed, revoked or expiredCheck the key.
402Prepaid balance at or below zero, or the key's monthly spend cap reached (the code tells you which)Top up or raise the cap.
403Model not in the key's allow-list, or missing scopeChange key settings.
429Rate limit on the keyWait the number of seconds in Retry-After.
500, 502, 503, 504upstream_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

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

Using MiniMax is one part of the job.

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 →