TL;DR
kling-2.5-turbo/image-to-videoturns a first frame into a 5- or 10-second clip; pass a second image URL to pin the last frame. It runs on HiAPI's async task API —POST /v1/tasksto create, then pollGET /v1/tasks/{taskId}for the finished MP4.- Output costs $0.06 per second: $0.30 for a 5 s clip, $0.60 for 10 s. Billing is per finished second, and a task that fails on bad input produces no output and isn't charged.
- The input schema is strict and closed. Required:
image_urls(array, 1–2 items — note the plural),prompt,duration(5 or 10 only). Optional:aspect_ratio,negative_prompt,cfg_scale. The singularimage_urlis rejected outright. - It's live on HiAPI: open the model page to try it in the browser first, then wire up the API below.
What you'll build and what you need
By the end you'll have a working first-frame → video call: submit an image plus a motion prompt, poll the task, and download the MP4. Two things to get first:
- An API key from the HiAPI dashboard — every request sends it as
Authorization: Bearer sk-.... See the authentication docs if you haven't created one. - A publicly reachable first-frame image URL. The create call only checks the shape of your input; the image is actually fetched when the task runs, so a localhost or private URL will create a task that then fails (more on that below).
This is image-to-video, so the model id is kling-2.5-turbo/image-to-video. There's a separate text-to-video endpoint if you want to generate from a prompt alone.
Minimal working example (curl)
Create the task. The model goes at the top level; everything the model reads goes inside input:
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kling-2.5-turbo/image-to-video",
"input": {
"prompt": "The perfume bottle rotates slowly on the marble stand, light glints across the glass, petals drift down, slow orbit",
"image_urls": ["https://static.hiapi.ai/gallery/2026/10/4852c542c4447593.jpg"],
"duration": 5,
"aspect_ratio": "9:16"
}
}'
A successful create returns a task id — not the video:
{ "code": 200, "data": { "taskId": "tk-hiapi-01M5CW..." }, "message": "success" }
Then poll the detail endpoint until status is terminal:
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-01M5CW... \
-H "Authorization: Bearer sk-YOUR_KEY"
While it runs you'll see a non-terminal status; when it finishes you get the MP4 URL under data.output[0].url:
{
"code": 200,
"data": {
"status": "success",
"model": "kling-2.5-turbo/image-to-video",
"storage": "temp",
"output": [{ "url": "https://temp.hiapi.ai/kling-i2v/tk-hiapi-01M5CW.mp4?expireAt=1761350400" }]
},
"message": "success"
}
That output[0].url is a temporary link — download it right away rather than hot-linking it from your app (see production notes).
Minimal working example (Python)
No SDK — the HiAPI task API is plain HTTP, so the standard library is enough:
import time
import requests
API = "https://api.hiapi.ai/v1/tasks"
HEADERS = {"Authorization": "Bearer sk-YOUR_KEY", "Content-Type": "application/json"}
def create(image_url: str, prompt: str, duration: int = 5) -> str:
payload = {
"model": "kling-2.5-turbo/image-to-video",
"input": {
"prompt": prompt,
"image_urls": [image_url], # 1–2 items; 2nd = last frame
"duration": duration, # 5 or 10 only
},
}
r = requests.post(API, json=payload, headers=HEADERS, timeout=60)
body = r.json()
task_id = (body.get("data") or {}).get("taskId")
if not task_id:
raise RuntimeError(f"create failed: {body}")
return task_id
def wait(task_id: str, timeout_s: int = 600, interval: int = 5) -> str:
deadline = time.time() + timeout_s
while time.time() < deadline:
r = requests.get(f"{API}/{task_id}", headers=HEADERS, timeout=30)
task = r.json().get("data") or {}
status = task.get("status")
if status == "success":
return task["output"][0]["url"]
if status == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task failed: {err.get('code')} {err.get('message')}")
time.sleep(interval)
raise TimeoutError(f"task {task_id} did not finish in {timeout_s}s")
task_id = create(
"https://static.hiapi.ai/gallery/2026/10/4852c542c4447593.jpg",
"slow orbit around the product, light glints across the glass, petals drift down",
duration=5,
)
mp4_url = wait(task_id)
# Download immediately — the URL is short-lived.
with requests.get(mp4_url, stream=True, timeout=120) as resp:
resp.raise_for_status()
with open("clip.mp4", "wb") as f:
for chunk in resp.iter_content(1 << 16):
f.write(chunk)
print("saved clip.mp4")
Going to production
Poll or use a callback. Polling every 5 s (as above) is the simplest path and is what the examples use. If you'd rather be pushed a result, the create call accepts a top-level callback object — {"url": "https://you/webhook", "when": "final"} — and HiAPI POSTs your endpoint once the task reaches a terminal state. The exact webhook contract is in the create-task docs; for most single-clip jobs, polling the task detail endpoint is enough.
Always branch on data.status, never on the HTTP code of create. A 200 on create means accepted, not rendered. The clip only exists once detail returns status: "success".
Treat the output URL as ephemeral. storage is temp and the URL expires, so download the MP4 (or copy it to your own bucket) as soon as the task succeeds. Don't serve the HiAPI URL to end users.
Cost control. You pay per finished second — $0.06/s — so a 5 s clip is $0.30 and a 10 s clip is $0.60. Render at 5 s while you iterate on the prompt and first frame, then re-run at 10 s only when you're happy. Current numbers are on the pricing page.
Tested notes and gotchas
Everything here came from real requests against the live endpoint (schema re-verified 2026-10-21), not just the parameter table:
- The field is
image_urls(plural), and the schema is closed. Sending the natural-lookingimage_urlfails before the task is created:
Any unrecognized field ({ "code": 400, "error_code": "INVALID_REQUEST", "message": "invalid input: image_urls: missing required field \"image_urls\"; <root>: additional properties 'image_url' not allowed" }mode, typos, etc.) is rejected the same way — there are no silently-ignored extras. durationis an enum, not a range. Only5and10are accepted;7returnsduration: value must be one of 5, 10.image_urlsholds 1 or 2 items. One URL is the first frame; a second pins the last frame for a controlled transition. Three URLs returnsmaxItems: got 3, want 2; an empty array returnsminItems: got 0, want 1.- Optional fields that do exist:
aspect_ratioaccepts exactly16:9,9:16,1:1,4:3,3:4,21:9;cfg_scaleis capped at1(passing99returnscfg_scale: maximum: got 99, want 1);negative_promptmust be a string. Omitaspect_ratioto let the model follow your first frame. - Create validates shape, not reachability — this is the one that bites. A syntactically valid but unreachable image URL returns
200with ataskId, then the task flips to failure asynchronously:
That task produced no{ "data": { "status": "fail", "error": { "code": "INVALID_REQUEST", "message": "The image URL is not readable. Please provide a valid public HTTP(S) URL or an accessible file path and try again." } } }outputand was not billed — failures cost nothing — but if you only checked the create response you'd think it succeeded. This is exactly why the polling loop keys offdata.status.
Related
- Kling 2.5 Turbo image-to-video model page — try it in the browser, no code.
- Kling 2.5 Turbo text-to-video — generate from a prompt with no starting frame.
- 6 Kling 2.5 Turbo prompt templates — copy-ready prompts for products, vertical, travel, food, pets and transitions.
- Create Task and Task Detail docs — full async-task reference, including callbacks.
- Quickstart — the five-minute version if this is your first HiAPI call.
FAQ
How long can the clip be? 5 or 10 seconds — those are the only two duration values the schema accepts.
Can I control the ending? Yes. Put two URLs in image_urls: the first is the opening frame, the second is the last frame. The model interpolates motion between them.
Why does my task say fail even though create returned 200? Create only checks that your JSON matches the schema. The image is fetched when the task runs, so an unreachable or private URL creates the task and then fails it. Always read data.status from the detail endpoint, and make sure the first-frame URL is publicly reachable.
Do failed tasks cost money? No. You're billed per finished second of output, so a task that fails on invalid input produces no output and isn't charged.
Is there an aspect-ratio control? aspect_ratio is optional and accepts 16:9, 9:16, 1:1, 4:3, 3:4, or 21:9. Leave it off to follow your first frame.





