Search

Search HiAPI models, tools, and resources.

Back to blog
Tutorial

How to Use the Kling 2.5 Turbo Image-to-Video API (curl + Python)

A runnable first-frame-to-video walkthrough: create the task, poll it, download the MP4 — with the strict input schema spelled out.

hiapiKlingImage-to-VideoAPI tutorialVideo generation

TL;DR

  • kling-2.5-turbo/image-to-video turns 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/tasks to create, then poll GET /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 singular image_url is 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:

  1. 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.
  2. 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-looking image_url fails before the task is created:
    { "code": 400, "error_code": "INVALID_REQUEST",
      "message": "invalid input: image_urls: missing required field \"image_urls\"; <root>: additional properties 'image_url' not allowed" }
    
    Any unrecognized field (mode, typos, etc.) is rejected the same way — there are no silently-ignored extras.
  • duration is an enum, not a range. Only 5 and 10 are accepted; 7 returns duration: value must be one of 5, 10.
  • image_urls holds 1 or 2 items. One URL is the first frame; a second pins the last frame for a controlled transition. Three URLs returns maxItems: got 3, want 2; an empty array returns minItems: got 0, want 1.
  • Optional fields that do exist: aspect_ratio accepts exactly 16:9, 9:16, 1:1, 4:3, 3:4, 21:9; cfg_scale is capped at 1 (passing 99 returns cfg_scale: maximum: got 99, want 1); negative_prompt must be a string. Omit aspect_ratio to 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 200 with a taskId, then the task flips to failure asynchronously:
    { "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." } } }
    
    That task produced no output and 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 off data.status.

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.

Generate it with HiAPI

Choose a model, enter your prompt, and see the result.

HiAPI Blog

Related articles

HiAPI

Generate it with HiAPI

Start generating