Tutorial
Kling 3.0 Turbo Image-to-Video API in Python: Schema, Full Script and Errors
A version-specific reference for Turbo's single-image schema, 3-15 second limits, 720p/1080p tiers, and failure modes.
kling-3.0-turbo/image-to-video has a small, strict schema: one public image URL, a required motion prompt, an integer duration from 3 to 15 seconds, and either 720p or 1080p output. This page is the version-specific reference for validating that payload and separating HTTP 400 errors from asynchronous TASK_FAILED results.
For the shared create, polling, callback, and download workflow, use the Kling Python task runner and Omni/Turbo chooser.
Turbo input schema
| Field | Type | Required | Accepted value |
|---|---|---|---|
prompt |
string | Yes | Describe motion and camera behavior |
image_urls |
array of strings | Yes | Exactly one publicly reachable image URL |
duration |
integer | No | 3 through 15 seconds |
resolution |
string | No | 720p or 1080p |
Turbo does not accept sound, aspect_ratio, negative_prompt, seed, mode, or a second image URL. A strict schema rejects extra properties instead of silently ignoring them.
The input image determines the frame shape. Upload it to a public bucket or CDN before creating the task; localhost, private dashboards, and expired signed URLs cannot be fetched by the renderer.
Minimal valid request
curl -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kling-3.0-turbo/image-to-video",
"input": {
"prompt": "The runner accelerates as the camera tracks from the side",
"image_urls": ["https://cdn.example.com/first-frame.jpg"],
"duration": 6,
"resolution": "1080p"
}
}'
A successful response contains data.taskId. HTTP 200 confirms creation, not completion; the video render continues asynchronously.
Python preflight validation
Validate the constraints before the network call so obvious mistakes never create a task:
from urllib.parse import urlparse
import requests
def turbo_input(prompt: str, image_url: str, duration: int, resolution: str) -> dict:
if not prompt.strip():
raise ValueError("prompt is required")
if type(duration) is not int or not 3 <= duration <= 15:
raise ValueError("duration must be an integer from 3 to 15")
if resolution not in {"720p", "1080p"}:
raise ValueError("resolution must be 720p or 1080p")
parsed = urlparse(image_url)
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
raise ValueError("image_url must be a public HTTP(S) URL")
return {
"prompt": prompt,
"image_urls": [image_url],
"duration": duration,
"resolution": resolution,
}
payload = {
"model": "kling-3.0-turbo/image-to-video",
"input": turbo_input(
prompt="The runner accelerates as the camera tracks from the side",
image_url="https://cdn.example.com/first-frame.jpg",
duration=6,
resolution="1080p",
),
}
response = requests.post(
"https://api.hiapi.ai/v1/tasks",
headers={"Authorization": "Bearer sk-your-key"},
json=payload,
timeout=30,
)
response.raise_for_status()
task_id = response.json()["data"]["taskId"]
print(task_id)
This local check catches value shape, but it cannot prove that the remote image is publicly downloadable. The renderer performs that fetch after creation.
Full Python script: submit, poll, download
The snippet above stops at the task id. This version runs the whole job and saves the MP4. It sends an Idempotency-Key header, so retrying the same submission after a timeout returns the original task instead of creating and billing a second one:
import os
import time
import uuid
import requests
API = "https://api.hiapi.ai/v1/tasks"
HEADERS = {"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}"}
def run_turbo(prompt: str, image_url: str, duration: int = 5,
resolution: str = "720p", out_path: str = "turbo.mp4") -> str:
body = {
"model": "kling-3.0-turbo/image-to-video",
"input": turbo_input(prompt, image_url, duration, resolution),
}
key = str(uuid.uuid4()) # reuse this key if you retry the same submission
r = requests.post(API, json=body, headers={**HEADERS, "Idempotency-Key": key}, timeout=30)
if r.status_code == 400:
raise ValueError(f"schema error, no task created: {r.text}")
r.raise_for_status()
task_id = r.json()["data"]["taskId"]
deadline = time.time() + 900
while time.time() < deadline:
data = requests.get(f"{API}/{task_id}", headers=HEADERS, timeout=30).json()["data"]
if data["status"] == "success":
video = requests.get(data["output"][0]["url"], timeout=120)
video.raise_for_status()
with open(out_path, "wb") as f:
f.write(video.content) # the output URL is temporary, so save it now
return out_path
if data["status"] == "fail":
raise RuntimeError(f"TASK_FAILED {task_id}: {data.get('error')}")
time.sleep(10)
raise TimeoutError(task_id)
print(run_turbo(
"The runner accelerates as the camera tracks from the side",
"https://cdn.example.com/first-frame.jpg",
))
A 5-second 720p clip like this one costs $0.65. In production, replace the polling loop with a top-level "callback": {"url": "https://your-app.example.com/hiapi/callback"} and deduplicate notifications by task id.
HTTP 400: the task was never created
An HTTP 400 response is synchronous schema validation. Typical causes are:
| Invalid input | Why it fails | Fix |
|---|---|---|
"duration": "5" |
A string was sent instead of an integer | Send 5 |
"duration": 20 |
The maximum is 15 | Choose 3-15 |
Two image_urls |
Turbo accepts exactly one | Keep only the starting frame |
"resolution": "4K" |
Turbo supports 720p and 1080p | Use 720p or 1080p, or choose Omni |
"sound": true |
sound is not in the Turbo schema |
Remove it or choose Omni |
Missing prompt |
Turbo requires a prompt | Add a non-empty motion prompt |
Because validation failed before creation, there is no task id to poll. Log the response body and request id, correct the payload, and submit a new request.
TASK_FAILED: creation succeeded, rendering did not
If POST /v1/tasks returned HTTP 200 and a task id, later failure belongs to the asynchronous task lifecycle. Treat it separately from a 400.
Common causes include:
- the image URL requires cookies or an authorization header;
- a signed image URL expired before the renderer fetched it;
- the host blocks server-side fetching or redirects to an HTML page;
- the file is corrupt or its media type does not match its contents;
- the provider could not complete that render.
Record the task id, terminal error code and message, original input URL, and request time. Before retrying, verify that an unauthenticated server can fetch the image with a 2xx response and an image content type. Blindly retrying the same inaccessible URL only creates another failed task.
The generic Python guide contains the shared terminal-status loop and output download code; keeping it there avoids duplicating lifecycle logic on this version reference.
Turbo pricing and model choice
Current production rates are:
| Turbo tier | Price per second | 5-second task |
|---|---|---|
| 720p | $0.130 | $0.65 |
| 1080p | $0.160 | $0.80 |
Check the live pricing page before a large batch. Turbo's name describes the model variant; it is not a promise that every resolution is cheaper or that a task will meet a fixed render-time SLA.
Choose standard Kling 3.0 (kling-3.0/image-to-video) when you need a last frame, audio, or 4K: it takes one or two images (start and end frame), has a sound switch, and costs $0.10/s at 720p or $0.129/s at 1080p without sound. Choose Omni when you need reference images or reference video. For a single input frame at 720p or 1080p, benchmark the variants with your own motion prompts and judge output quality, queue time, and total cost.
FAQ
How many images does Kling 3.0 Turbo image-to-video accept?
Exactly one public image URL. A second image is a schema error (image_urls: maxItems: got 2, want 1); use standard Kling 3.0 image-to-video for first-and-last-frame control.
What duration values are valid?
Send an integer from 3 through 15. A quoted number such as "5" is a string and fails validation.
Does Turbo support 4K or sound?
No 4K: this endpoint accepts 720p or 1080p. There's no sound field, but Turbo clips come with sound already, and you can't turn it off. Use standard Kling 3.0 or Omni when you need 4K or silent output.
What is the difference between HTTP 400 and TASK_FAILED?
HTTP 400 means no task was created because the request violated the schema. TASK_FAILED occurs after a valid create response, during input fetching or generation.
Why does a public-looking image URL cause TASK_FAILED?
The URL may depend on cookies, have an expired signature, block server fetches, or return HTML instead of image bytes. Test it without browser authentication before retrying.
Related articles
View all articles →-
Tutorial
How to Call hailuo-2.3-fast/image-to-video via the HiAPI API: curl and Python
A first working request for the fast, low-cost image-to-video tier — from model ID to downloaded MP4
9 min read -
Tutorial
How to Use hailuo-2.3/text-to-video via the HiAPI API: curl, Python, and a Working Request
11 min read -
Tutorial
Kandinsky 6.0 Pro Text-to-Video on HiAPI: curl and Python Tutorial
A request-first walkthrough: build the prompt-to-clip call, submit it, poll for completion, and download the result
11 min read