Ray 3.2 is a text-to-video model in the hiapi catalog, and you call it through the same unified async task API as every other video model — model id ray-3.2/text-to-video, submitted to POST /v1/tasks. Unlike some video models whose only knob is the prompt, Ray 3.2 gives you three real output controls — duration, resolution, and aspect_ratio — each strictly validated server-side.
This guide gets you one request that works on the first try: the exact input schema (verified against the live API on 2026-10-19), a copy-paste curl call, a complete Python submit/poll/download script, and the production details — callbacks, idempotent retries, and the error responses you'll actually see.
What you'll build, and what you need
Goal: submit a text prompt, poll the task, and download a silent .mp4 clip (5 or 10 seconds).
Prerequisites:
- A hiapi API key — grab one from your dashboard. Keys look like
sk-...and go in theAuthorization: Bearerheader. New to the task API? Start with the quickstart. - Any HTTP client. We'll show
curland Python (requests).
Ray 3.2 output is billed per clip, and the price scales hard with your choices — see pricing for current rates. Start at 540p / 5 seconds while you iterate on prompts; a single 1080p 10-second clip can cost many times a budget-sized image render, so don't leave your dev loop running at max settings.
The exact input schema (one required field, three strict enums)
Everything goes inside an input object. Verified field-by-field against the live API:
| Field | Type | Required | Allowed values | Default |
|---|---|---|---|---|
prompt | string | yes | Scene description, up to 6000 characters | — |
duration | integer | no | 5 or 10 | 5 |
resolution | string | no | 540p, 720p, 1080p | 540p |
aspect_ratio | string | no | 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 | 16:9 |
Three things worth internalizing before you write code:
durationis an integer, not a string. Send the bare number —"duration": 5. This is the opposite of some other task-API video models (hailuo-2.3, for example, wants"duration": "6"as a string). If you're porting a request body across models, re-check this field; it's the single most common cross-model mistake.- The enums are validated, not clamped. An out-of-range value is rejected with a
400, not silently rounded. There's no2k, no480p, no2:1. - There's no
prompt_optimizer,seed, ornegative_prompt. Framing is controlled entirely throughaspect_ratioplus the prompt. If you send a field the schema doesn't know, expect a 400 — input schemas on/v1/tasksvary per model, so don't assume a key that worked elsewhere carries over.
What the validation actually returns
These are real responses from the live endpoint (invalid values submitted deliberately, no task created, nothing billed):
// POST with "resolution": "2k"
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
"message":"invalid input: resolution: value must be one of '540p', '720p', '1080p'"}
// POST with "duration": 7
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
"message":"invalid input: duration: value must be one of 5, 10"}
// POST with "aspect_ratio": "2:1"
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
"message":"invalid input: aspect_ratio: value must be one of '16:9', '9:16', '1:1', '4:3', '3:4', '21:9'"}
The message names the exact field and the exact allowed set — so if you ever forget an enum, the API tells you. Note the unquoted 5, 10 for duration (numbers) versus the quoted '540p' strings for resolution: that quoting is your reminder of which field is an integer.
Minimal working example: curl
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": "ray-3.2/text-to-video",
"input": {
"prompt": "A street dancer spins under neon signs on a rainy night, puddles reflecting pink and blue light, her jacket swirling with the turn, the camera circling low around her, cinematic",
"duration": 5,
"resolution": "540p",
"aspect_ratio": "9:16"
}
}'
The create response carries a task id (trimmed):
{ "data": { "taskId": "task_01k..." } }
Poll it until it reaches a terminal state:
curl -s https://api.hiapi.ai/v1/tasks/task_01k... \
-H "Authorization: Bearer sk-YOUR_KEY"
While the job runs, data.status reports an in-progress state. The two terminal states are success and fail. On success (trimmed):
{
"data": {
"taskId": "task_01k...",
"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 rather than hot-linking the task URL. Ray 3.2 returns a silent MP4 (no audio track), so don't wait for an audio stream that never comes.
Complete Python script (submit → poll → download)
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: int = 5,
resolution: str = "540p", aspect_ratio: str = "9:16") -> str:
resp = requests.post(
f"{API_BASE}/tasks",
headers=HEADERS,
json={
"model": "ray-3.2/text-to-video",
"input": {
"prompt": prompt,
"duration": duration, # 5 or 10 — an INTEGER, not a string
"resolution": resolution, # "540p" | "720p" | "1080p"
"aspect_ratio": aspect_ratio, # "16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "21:9"
},
},
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) # any non-terminal status = still running
raise TimeoutError(f"task {task_id} not finished after {timeout_s}s")
task_id = create_task(
"A paper boat drifting down a rain-filled gutter, droplets rippling the surface, "
"autumn leaves swirling past, shot low to the water, soft overcast light"
)
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("ray32-clip.mp4", "wb") as f:
f.write(video)
print(f"saved ray32-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 minutes, not seconds — give your poll loop a generous deadline (15 minutes above) instead of assuming a fast return.
Production notes: callbacks, idempotency, errors
Prefer a callback over polling
For anything beyond a script, register a webhook at create time instead of polling:
{
"model": "ray-3.2/text-to-video",
"input": { "prompt": "...", "duration": 10, "resolution": "720p" },
"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 completions missed between intervals. Your handler then does a GET /v1/tasks/<id> (or reads the delivered body) to fetch output[0].url. Polling is still fine for CLIs and notebooks that can't expose an HTTPS endpoint.
Make retries idempotent
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. Because Ray 3.2 bills per clip and clips aren't cheap at higher resolution, a duplicate create is a real cost mistake, not just wasted time.
The errors you'll actually see
Every row below is a verified live response shape:
| Symptom | Response | Fix |
|---|---|---|
| Missing/invalid key | 401, nested {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"..."}} | Check the Authorization: Bearer sk-... header and that the key has access. Quote the request_id if you contact support. |
| Wrong enum value | 400 INVALID_REQUEST, flat {"code":400,"error_code":"INVALID_REQUEST","message":"invalid input: <field>: value must be one of ..."} | Read the field named in message; it lists the exact allowed set. |
duration sent as a string | 400 INVALID_REQUEST | Send the bare integer 5 or 10, not "5". |
| Unknown model id | 400, error_code: "MODEL_UNAVAILABLE", "model not available via /v1/tasks" | Use ray-3.2/text-to-video exactly — check the model page for the canonical id. |
| Bad task id on poll | 404, flat {"code":404,"data":null,"message":"task not found"} (note: no error_code on 404) | Wrong id, or you're querying with a different account's key. |
status: "fail" while polling | terminal task state, detail in data.error | Read data.error.code / data.error.message. Failed tasks are refunded; content-policy flags and upstream capacity issues surface here — retry with a reworded prompt for a content flag. |
Related resources
- Ray 3.2 Text-to-Video model page — playground, live status, and the full price table
- Ray 3.2 Image-to-Video API — same model family, but animates a reference frame instead of starting from text (different input schema — probe it before reusing this body)
- How to use the Hailuo 2.3 text-to-video API — a sibling recipe whose
durationis a string, a useful contrast to Ray's integer - hiapi pricing — per-clip video rates across resolutions and durations
FAQ
What's the model id, and which endpoint do I call?
ray-3.2/text-to-video, submitted to POST https://api.hiapi.ai/v1/tasks as the top-level model with parameters under input. It's asynchronous — you get a taskId back and poll GET /v1/tasks/<id> (or register a callback).
Is duration in seconds, and why does "5" fail?
Clips are 5 or 10 seconds, and the field is an integer: send 5 or 10. A quoted string, or any other number like 7, fails with 400 INVALID_REQUEST: duration: value must be one of 5, 10.
Can I set resolution and aspect ratio?
Yes — that's where Ray 3.2 differs from prompt-only video models. resolution is 540p, 720p, or 1080p; aspect_ratio is one of 16:9, 9:16, 1:1, 4:3, 3:4, 21:9. Both are optional (defaults 540p and 16:9) and both reject out-of-range values with a 400.
How much does one clip cost?
Ray 3.2 is billed per clip, and resolution is the biggest lever — a 1080p clip costs several times a 540p one at the same length, and a 10-second clip is roughly triple its 5-second counterpart. Rates change, so check the pricing page rather than hardcoding a number, and prototype at 540p/5.
Does the clip have audio? No — Ray 3.2 text-to-video returns a silent MP4. Add sound in post or with a separate audio model if you need it.
Does Ray 3.2 support image-to-video?
Yes, as a separate model id on the same task API: ray-3.2/image-to-video. Its input schema differs (it takes a reference image), so probe it before reusing this request body — see the image-to-video model page.
My generation is slow or seems stuck. What's normal?
Video tasks run for minutes. Keep a non-terminal data.status in your poll loop and wait — budget a 10–15 minute deadline, or use a callback with "when": "final" so hiapi notifies you at the terminal state instead of you guessing an interval.




