
Hailuo 2.3 is MiniMax's latest text-to-video model, and it has one standout trait: motion that actually obeys physics. Fabric ripples, liquids splash, animals move with believable weight — which is why it's a popular pick for product shots and cinematic b-roll. On hiapi you call it through the same unified async task API as every other video model, with the model id hailuo-2.3/text-to-video.
This guide gives you a request that works on the first try: the exact input schema (verified against the live API), a copy-paste curl call, a complete Python script, and the production details — callbacks, idempotent retries, and the error responses you'll actually see.
Goal: submit a text prompt, get back an .mp4 clip of 6 or 10 seconds.
Prerequisites:
sk-... and go in the Authorization: Bearer header.curl and Python (requests).Video generation is billed per second of output — see pricing for the current rate, and start with "duration": "6" while you iterate on prompts.
hailuo-2.3/text-to-video accepts exactly three input fields:
| Field | Type | Required | Notes |
|---|---|---|---|
prompt | string | yes | Your scene description. This is also where motion is controlled — see below. |
duration | string | no | "6" or "10". A string, not a number — 6 fails validation. |
prompt_optimizer | boolean | no | Let the model expand and refine your prompt before generating. |
Two things trip people up:
duration is a string enum. Send "duration": 6 and you get 400 INVALID_REQUEST: duration: got number, want string. Send "7" and you get value must be one of '6', '10'.resolution, aspect_ratio, size, seed, or negative_prompt for this model. Any extra key returns 400: additional properties '<field>' not allowed. If you're porting code from another model (say, one that takes size), strip those fields first — input schemas on the task API vary per model.Where are the motion physics parameters? There aren't any — and that's by design. Hailuo 2.3's physics simulation is native model behavior, steered entirely through prompt language. Describe the motion you want ("ice cubes tumbling", "cape snapping in the wind", "slow dolly-in") and the model handles momentum, gravity, and collision on its own. Setting "prompt_optimizer": true helps when your prompt is short: the model rewrites it with more concrete motion and camera detail before generating.
Create the task:
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "hailuo-2.3/text-to-video",
"input": {
"prompt": "A glass of iced tea tipping over in slow motion, splash arcing across a wooden table, ice cubes tumbling, macro shot, shallow depth of field",
"duration": "6",
"prompt_optimizer": true
}
}'
The response includes a task id (trimmed):
{ "data": { "taskId": "task_01jz..." } }
Poll it until it reaches a terminal state:
curl -s https://api.hiapi.ai/v1/tasks/task_01jz... \
-H "Authorization: Bearer sk-YOUR_KEY"
While the job is running, data.status reports an in-progress state. The two terminal states are success and fail. On success (trimmed):
{
"data": {
"taskId": "task_01jz...",
"status": "success",
"output": [ { "url": "https://.../clip.mp4" } ]
}
}
Download data.output[0].url immediately. Output URLs are signed and expire — persist the bytes to your own storage, never hot-link the task URL.
import time
import requests
API_BASE = "https://api.hiapi.ai/v1"
API_KEY = "sk-YOUR_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def create_task(prompt: str, duration: str = "6") -> str:
resp = requests.post(
f"{API_BASE}/tasks",
headers=HEADERS,
json={
"model": "hailuo-2.3/text-to-video",
"input": {
"prompt": prompt,
"duration": duration, # "6" or "10" — a string, not a number
"prompt_optimizer": True,
},
},
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"]["taskId"]
def wait_task(task_id: str, timeout_s: int = 900, poll_interval: int = 5) -> dict:
deadline = time.time() + timeout_s
while time.time() < deadline:
resp = requests.get(f"{API_BASE}/tasks/{task_id}", headers=HEADERS, timeout=30)
resp.raise_for_status()
task = resp.json()["data"]
if task["status"] == "success":
return task
if task["status"] == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
time.sleep(poll_interval) # anything else = still running
raise TimeoutError(f"task {task_id} not finished after {timeout_s}s")
task_id = create_task(
"A snow leopard sprinting across a rocky ridge at dawn, loose stones "
"scattering under its paws, fur rippling in the wind, cinematic tracking shot"
)
print("task created:", task_id)
task = wait_task(task_id)
video_url = task["output"][0]["url"]
# Output URLs expire — download right away and store the bytes yourself.
video = requests.get(video_url, timeout=120).content
with open("hailuo-clip.mp4", "wb") as f:
f.write(video)
print(f"saved hailuo-clip.mp4 ({len(video) / 1e6:.1f} MB)")
Run it with pip install requests, swap in your key, and you'll have an .mp4 on disk when the task completes. Video tasks take longer than image tasks — give your poll loop a generous deadline (the script above allows 15 minutes) rather than assuming completion in seconds.
For anything beyond a script, register a webhook when you create the task instead of polling:
{
"model": "hailuo-2.3/text-to-video",
"input": { "prompt": "...", "duration": "10" },
"callback": { "url": "https://your-app.example.com/hooks/hiapi", "when": "final" }
}
With "when": "final" hiapi calls your endpoint once, when the task reaches a terminal state — no poll traffic, no missed completions between intervals. Polling is still the right choice for CLIs, notebooks, and environments that can't expose an HTTPS endpoint. If your handler never fires, work through why your hiapi task callback isn't firing — it's almost always URL reachability or a non-2xx handler response.
Persist the taskId the moment the create call returns. If your process crashes mid-generation, resume by polling the stored id — don't re-create the task, or you'll pay for a second render. Only re-create when the original create request itself failed before returning a task id.
| Symptom | Response | Fix |
|---|---|---|
| Missing/invalid key | 401 with error.code: "permission_denied" | Check the Authorization: Bearer sk-... header and that the key has access to this model. |
duration: got number, want string | 400 INVALID_REQUEST | Quote it: "duration": "6". |
additional properties 'X' not allowed | 400 INVALID_REQUEST | Remove the field — this model only takes prompt, duration, prompt_optimizer. |
task not found | 404 on GET | Wrong task id, or you're querying with a different account's key. |
status: "fail" in polling | terminal task state | Read data.error.code / data.error.message; content-policy rejections and upstream capacity issues land here. Retry with a reworded prompt if it's a content flag. |
| Task seems stuck | non-terminal status for a long time | See when a hiapi /v1/tasks job hangs or times out for a diagnosis checklist before you bail. |
Can I set the resolution or aspect ratio for hailuo-2.3 text-to-video?
No. The input schema accepts only prompt, duration, and prompt_optimizer; sending resolution, aspect_ratio, or size returns a 400. Framing (wide shot, portrait composition, macro) is controlled through the prompt itself.
Is duration in seconds? Why does 6 fail?
Yes — clips are 6 or 10 seconds. But the API expects a string enum, so send "6" or "10". The number 6 fails schema validation with got number, want string.
What does prompt_optimizer do, and should I turn it on?
When true, the model rewrites your prompt with richer motion and camera detail before generating. Turn it on for short or vague prompts; consider leaving it false when you've hand-tuned exact wording and want maximum prompt fidelity.
How do I control Hailuo's motion physics via the API? Through the prompt — there is no separate motion parameter. Name the physical interactions you want (splashes, collisions, wind, weight shifts) and the camera move (tracking shot, dolly-in, handheld). The model's physics simulation takes it from there.
How long does a generation take, and how should I wait?
Expect minutes, not seconds — budget a 10–15 minute poll deadline or use a callback with "when": "final" so hiapi notifies you at the terminal state.
Does Hailuo 2.3 support image-to-video too?
Yes, as separate model ids on the same task API: hailuo-2.3/image-to-video and the cheaper/faster hailuo-2.3-fast/image-to-video. The input schema differs from text-to-video, so probe it before reusing this request body.
How much does a 6-second clip cost? Video is billed per second of output and rates change — check the pricing page for the current per-second price.
Key Takeaways