Choose a model, enter your prompt, and see the result.
HiAPI Blog
HiAPI
Generate it with HiAPI
flux-3 (Black Forest Labs' FLUX.3 Video) generates up to
20-second clips from a text prompt, a still image, or an existing video, with 720p/1080p output and
synchronized audio baked directly into the .mp4. On hiapi it runs through the same unified async
task API as every other model, so the integration pattern below is the same one you'd use for image
or audio generation — only the input fields change.
sk-...).curl, Python (requests), and Node's built-in fetch.flux-3 is a task model: you POST a job, then either poll for the result or receive it via
callback. There is no synchronous endpoint.
Create a task with POST https://api.hiapi.ai/v1/tasks. The model id is the bare string flux-3 —
no /text-to-video suffix.
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "flux-3",
"input": {
"prompt": "a paper airplane gliding through a sunlit office, slow motion",
"duration": 6,
"resolution": "720p",
"aspect_ratio": "16:9"
}
}'
A successful response returns a taskId:
{"code":200,"data":{"taskId":"task_xxxxxxxx"},"message":"ok"}
Poll GET /v1/tasks/<taskId> until status is success or fail, then read the clip URL from
data.output[0].url:
import time
import requests
API_KEY = "sk-your-api-key"
BASE = "https://api.hiapi.ai/v1/tasks"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def create_flux3_task(prompt, **input_overrides):
payload = {"model": "flux-3", "input": {"prompt": prompt, **input_overrides}}
resp = requests.post(BASE, headers=HEADERS, json=payload, timeout=60)
resp.raise_for_status()
return resp.json()["data"]["taskId"]
def wait_for_task(task_id, timeout_s=600, poll_every=5):
deadline = time.time() + timeout_s
while time.time() < deadline:
resp = requests.get(f"{BASE}/{task_id}", headers=HEADERS, timeout=30)
resp.raise_for_status()
task = resp.json()["data"]
if task["status"] == "success":
return task["output"][0]["url"]
if task["status"] == "fail":
raise RuntimeError(task.get("error"))
time.sleep(poll_every)
raise TimeoutError(f"task {task_id} did not finish in {timeout_s}s")
task_id = create_flux3_task(
"a paper airplane gliding through a sunlit office, slow motion",
duration=6, resolution="720p", aspect_ratio="16:9",
)
video_url = wait_for_task(task_id)
print(video_url)
Node (built-in fetch, no dependencies):
const BASE = "https://api.hiapi.ai/v1/tasks";
const headers = {
Authorization: "Bearer sk-your-api-key",
"Content-Type": "application/json",
};
async function createTask(prompt, overrides = {}) {
const res = await fetch(BASE, {
method: "POST",
headers,
body: JSON.stringify({ model: "flux-3", input: { prompt, ...overrides } }),
});
const { data } = await res.json();
return data.taskId;
}
async function waitForTask(taskId, { pollEveryMs = 5000, timeoutMs = 600000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(`${BASE}/${taskId}`, { headers });
const { data } = await res.json();
if (data.status === "success") return data.output[0].url;
if (data.status === "fail") throw new Error(JSON.stringify(data.error));
await new Promise((r) => setTimeout(r, pollEveryMs));
}
throw new Error(`task ${taskId} timed out`);
}
input object, strict — unknown fields 400)| Field | Type | Required | Notes |
|---|---|---|---|
prompt | string | yes | Scene description. |
duration | integer | no | 5–20 seconds. Omitting it still returns a real, billed clip — see the cost warning below. |
resolution | "720p" | "1080p" | no | Forced to "720p" whenever draft:true. |
aspect_ratio | enum | no | auto, 21:9, 2:1, 16:9, 4:3, 1:1, 3:4, 9:16. |
draft | boolean | no | Flat, cheaper preview tier (720p only). See pricing below. |
generate_audio | boolean | no | Embeds synchronized audio directly in the output .mp4 — not a separate file. |
image_urls | array of public URLs | no | Animates an existing image (image-to-video). |
start_video | string URL | no | A prior flux-3 output URL to continue as a new scene (video-to-video continuation). |
prompt is the only required fieldBecause prompt is the only required field, a request as bare as {"model":"flux-3","input":{"prompt":"..."}}
is a complete, valid request — the API returns 200 with a real taskId and bills a full clip at
the default (non-draft, 720p) rate. There is no dry-run flag separate from draft, and no cancel
endpoint once a task is created. When you're first wiring up the integration or trying a new prompt,
always set draft: true explicitly and pin duration to the minimum:
{"model": "flux-3", "input": {"prompt": "...", "duration": 5, "draft": true}}
duration)| Mode | Rate |
|---|---|
| 720p (default) | $0.25/s |
| 1080p | $0.42/s |
| Draft (any resolution → forced 720p) | $0.09/s flat |
Continuation (start_video set), 720p | $0.59/s |
| Continuation, 1080p | $0.76/s |
| Draft + continuation | $0.18/s flat |
A 5-second draft preview costs $0.45; a 10-second final 720p clip costs $2.50. Current rates always live at /en/pricing — check there before budgeting a production run.
Pass one or more URLs in image_urls to animate a still instead of starting from a blank prompt —
useful for turning a product photo or a generated keyframe into motion. To extend a clip you already
generated, pass its output URL in start_video; the new clip continues the scene rather than
restarting it, which is usually cheaper than re-describing the same setting in a fresh prompt once you
account for how many attempts a from-scratch prompt takes to match an existing shot.
Webhooks instead of polling. Add a top-level callback object to skip the poll loop:
{
"model": "flux-3",
"input": {"prompt": "...", "duration": 8, "resolution": "720p"},
"callback": {"url": "https://yourapp.com/webhooks/hiapi", "when": "final"}
}
when: "final" is the only supported value platform-wide — you get one callback when the task
reaches success or fail, not incremental progress events. For low-volume or interactive use,
polling is simpler; for batch generation, a callback avoids holding a connection or a worker open for
however long a 20-second render takes.
Idempotency. Task creation is not idempotent on request body — retrying an identical payload
creates a second, separately billed task. If your caller might retry (timeout, crash, redeploy),
store the returned taskId against your own request id before you start polling, and check its
status before creating a duplicate.
Error handling. An API key without access to flux-3 gets a 401:
{"error":{"code":"permission_denied","message":"This API key cannot use the selected model...","type":"hiapi_error","request_id":"..."}}
Malformed input gets a 400 naming the offending field, e.g. "duration: maximum: got 1000, want 20"
or "<root>: additional properties 'foo' not allowed" — the schema is strict, so typoed field names
fail loudly instead of being silently ignored. A bad image_urls/start_video value (unreachable URL,
wrong media type) surfaces as a 503 STORAGE_UNAVAILABLE during input normalization rather than a 400,
since that check happens before schema validation.
What's the maximum clip length for flux-3?
20 seconds, set via the duration field (integer, 5–20).
Does flux-3 generate audio?
Yes — set generate_audio: true and the audio is embedded in the same output .mp4, not delivered
as a separate file.
What's the cheapest way to test a prompt before committing to a real render?
Set draft: true. It forces 720p and bills a flat $0.09/s regardless of the resolution you pass,
versus $0.25/s (720p) or $0.42/s (1080p) for a real render.
Can I animate an existing image instead of starting from text?
Yes, pass its URL in image_urls.
Can I extend a video I already generated?
Yes, pass the prior task's output URL in start_video. Continuation is billed separately ($0.59/s at
720p, $0.76/s at 1080p) because it's a distinct pricing tier from a fresh clip.
Do I have to poll for the result?
No — pass a top-level callback: {"url": "...", "when": "final"} and hiapi POSTs the result to your
endpoint once the task finishes instead.
What does an invalid API key error look like?
HTTP 401 with {"error":{"code":"permission_denied", ...}}. A 400 instead means the request body
itself failed schema validation — check the message field, it names the exact bad field.