TL;DR
- FLUX 3 Max on hiapi is three separate model ids on the same
POST /v1/tasksendpoint:flux-3-max/text-to-video,flux-3-max/image-to-videoandflux-3-max/keyframes-to-video. You switch modes by changingmodeland one input field. - All three make 5–20 second clips at 720p or 1080p, billed per second of output by resolution. Check live rates on /en/pricing.
- Frame-number rules for keyframes are not checked when you submit. A bad timeline returns HTTP 200 with a
taskId, then the task fails about 2 seconds later. Validate on your side first.
This guide is about the Max tier. The base flux-3 model, with draft mode and video continuation, is covered in the FLUX.3 Video API Guide, and editing existing footage is in flux-3 video editing.
Prerequisites
Create a key in the hiapi dashboard and export it:
export HIAPI_KEY=sk-your-key
The request flow is the same for every mode: POST /v1/tasks returns a taskId, then you poll GET /v1/tasks/<taskId> (or use a callback) until status is success, and download data.output[0].url.
Which model id for which input
| You have | Model id | Input that changes |
|---|---|---|
| Only a text prompt | flux-3-max/text-to-video | prompt |
| One still (optionally a last frame) | flux-3-max/image-to-video | image_url, optional end_image_url |
| 1–10 images that must land at specific moments | flux-3-max/keyframes-to-video | keyframes: [{image_url, frame_index}] |
Shared optional fields on all three: duration (whole seconds, 5–20, default 5), resolution (720p or 1080p), aspect_ratio (auto, 21:9, 2:1, 16:9, 4:3, 1:1, 3:4, 9:16), generate_audio (bool, on by default) and reasoning_level (low or high, high by default). Model pages: text-to-video, image-to-video, keyframes-to-video.
Minimal example: text-to-video with curl
curl -X POST "https://api.hiapi.ai/v1/tasks" \
-H "Authorization: Bearer $HIAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-3-max/text-to-video",
"input": {
"prompt": "a slow aerial shot of ocean waves at sunrise, photorealistic",
"duration": 5,
"resolution": "720p",
"generate_audio": false,
"reasoning_level": "low"
}
}'
A valid request returns {"code":200,"data":{"taskId":"tk-hiapi-..."},"message":"success"}. Then:
curl https://api.hiapi.ai/v1/tasks/tk-hiapi-... -H "Authorization: Bearer $HIAPI_KEY"
generate_audio and the 5-second low reasoning setup are a cheap way to compare prompts; the model page states that audio does not change the price.
One Python helper for all three modes
# pip install requests
import os, time, requests
KEY = os.environ["HIAPI_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
BASE = "https://api.hiapi.ai/v1/tasks"
def submit(mode: str, **inp) -> str:
"""mode: text-to-video | image-to-video | keyframes-to-video"""
body = {"model": f"flux-3-max/{mode}", "input": inp}
res = requests.post(BASE, json=body, headers=H, timeout=30).json()
if res.get("code") != 200:
# sync 400s look like {"code":400,"error_code":"INVALID_REQUEST","message":"invalid input: ..."}
raise RuntimeError(res)
return res["data"]["taskId"]
def wait(task_id: str, timeout_s: int = 900, every: int = 10) -> str:
deadline = time.time() + timeout_s
while time.time() < deadline:
d = requests.get(f"{BASE}/{task_id}", headers=H, timeout=30).json()["data"]
if d["status"] == "success":
return d["output"][0]["url"]
if d["status"] == "fail":
raise RuntimeError(d.get("error"))
time.sleep(every)
raise TimeoutError(task_id)
# 1) text-to-video
t2v = submit("text-to-video",
prompt="A single white sneaker rotates slowly on a wet black plinth, studio softbox lighting, macro product shot",
duration=5, resolution="720p", aspect_ratio="16:9", generate_audio=False)
# 2) image-to-video: first frame, optional last frame
i2v = submit("image-to-video",
prompt="Morning mist drifts over the harbor; the boats rock gently and the camera slowly pushes forward.",
image_url="https://example.com/harbor.jpg",
# end_image_url="https://example.com/harbor-end.jpg", # optional fixed ending
duration=5, resolution="720p", generate_audio=False)
# 3) keyframes-to-video
FPS = 24
def keyframes_ok(duration: int, frames: list[dict]) -> None:
idx = [f["frame_index"] for f in frames]
assert 1 <= len(frames) <= 10, "1-10 keyframes"
assert len(set(idx)) == len(idx), "frame_index must be unique"
assert all(0 <= i <= duration * FPS for i in idx), f"frame_index must be 0..{duration * FPS}"
frames = [
{"image_url": "https://example.com/harbor.jpg", "frame_index": 0},
{"image_url": "https://example.com/latte.jpg", "frame_index": 120},
]
keyframes_ok(5, frames)
kf = submit("keyframes-to-video",
prompt="A continuous cinematic morph from the misty harbor into a close-up of an iced matcha latte.",
keyframes=frames, duration=5, resolution="720p", generate_audio=False)
url = wait(kf)
open("clip.mp4", "wb").write(requests.get(url).content)
Output URLs expire, so download or copy the file as soon as the task succeeds instead of storing the link.
Keyframes: how the timeline works
frame_index is a position on a 24 fps clock starting at 0, not a percentage. The highest allowed value is duration × 24: 120 for 5 seconds, 240 for 10, 480 for 20. Indexes must be unique, and you can pass 1 to 10 keyframes. For a clean A-to-B transition, put the first anchor at 0 and the last at duration × 24; move the last anchor earlier to hold the ending, or later to compress the change. Keep every keyframe in the same aspect ratio, and for a seamless loop make the first and last images look like the same moment.
Production notes
- Polling vs callback. The task API accepts
callback.urlwithwhen: "final"so you do not have to poll; polling every 10 seconds as above is fine for scripts and batch jobs. - Always put a timeout on the poll loop and persist the
taskIdbefore polling, so a crash does not lose a paid job. - Download immediately. Treat
output[0].urlas short-lived. - Cost control. Billing is per output second by resolution, so a 20-second 1080p clip is several times the cost of a 5-second 720p test. Prototype at 5 seconds and 720p, then raise both once the prompt works. See pricing for current rates.
What I tested, and what I didn't
These checks were made against the live API on October 23, 2026, using a read-only test key. I did not render full clips for this article, so I make no claims here about output quality; the sample prompts come from the model pages' own examples.
Synchronous validation (HTTP 400, nothing queued). Out-of-range values are rejected at submit with error_code: INVALID_REQUEST and a readable message:
| Request | Response message |
|---|---|
duration: 3 | invalid input: duration: minimum: got 3, want 5 |
resolution: "4k" | value must be one of '720p', '1080p' |
aspect_ratio: "5:4" | value must be one of 'auto', '21:9', '2:1', '16:9', '4:3', '1:1', '3:4', '9:16' |
reasoning_level: "medium" | value must be one of 'low', 'high' |
image-to-video with image_urls instead of image_url | image_url: missing required field "image_url"; ... additional properties 'image_urls' not allowed |
The last row is a common mistake: other hiapi image-input models use plural fields, but FLUX 3 Max image-to-video takes a single image_url. Unknown fields are rejected, not ignored.
Keyframe timeline errors are asynchronous. I submitted two 5-second keyframes requests with a bad timeline: one with frame_index: 121 (over the 120 limit) and one with two keyframes both at frame_index: 0. Both were accepted with HTTP 200 and a taskId. Polling returned a terminal state about 1–2 seconds after creation:
{"code":200,"data":{"status":"fail","model":"flux-3-max/keyframes-to-video",
"error":{"code":"INVALID_REQUEST","message":"Invalid request parameters for this model. Please check the request and try again."},
"taskId":"tk-hiapi-..."},"message":"success"}
Two things follow. First, a 200 on submit does not mean your timeline is valid, so check status and error.code when polling. Second, the failure message is generic and does not say which frame was wrong, which is why the keyframes_ok check above is worth running before you submit. The model pages state that failed tasks are not charged; I did not inspect the billing record for these two.
FAQ
Which model id should I use for a first frame plus a last frame? flux-3-max/image-to-video with image_url and end_image_url. Use keyframes only when you need anchors between the ends.
Does adding more keyframes cost more? Per the model page, no: billing is per output second by resolution, and anchor count does not change it.
What is the difference between this and flux-3? flux-3-max/* are separate model ids with a per-mode split (text, image, keyframes), while base flux-3 is one id that switches mode by input and also supports draft mode and continuation. See the FLUX.3 Video API Guide. I did not compare output quality between the two, so pick by features and by the rates on the pricing page.
Can I use another image-to-video model instead? Yes. For a different image-to-video route, see How to use the Ray 3.2 image-to-video API.





