Animate a still image into a video clip with kling-3.0-omni/image-to-video on hiapi's async /v1/tasks API — copy-paste curl and Python, the exact input schema, callbacks, and error handling.

You have a still image and you want it to move. This guide shows exactly how to turn a first frame into a video clip with kling-3.0-omni/image-to-video through the hiapi API: what to send, how to wait for the result, and how to run it in production. Every request and response below comes from the live task API, so you can copy, paste, and run.
The goal: hand the model a starting image, get a downloadable video URL that animates it. Because video generation isn't instant, it runs on hiapi's asynchronous task API — you create a task, then either poll it or receive a callback when it finishes.
You need two things:
An API key. Grab it from your hiapi dashboard and export it:
export HIAPI_API_KEY="sk-your-key-here"
A publicly reachable image URL for the first frame. The model fetches it, so it has to be a URL the API can reach — not a local file path. Upload your image to your own storage (S3/R2/a CDN) first and use that link.
Every image-to-video job follows the same three-step shape:
POST /v1/tasksGET /v1/tasks/<taskId> (or use a callback)data.output[0].urlThe one required field is image_urls, an array whose first entry is your starting frame. prompt is optional but recommended — use it to describe the motion you want:
curl -s -X POST "https://api.hiapi.ai/v1/tasks" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kling-3.0-omni/image-to-video",
"input": {
"image_urls": ["https://your-cdn.example.com/first-frame.jpg"],
"prompt": "The camera slowly pushes in as the subject turns to look at the viewer",
"duration": 5
}
}'
The response hands you a task ID — that's all you get immediately:
{
"code": 200,
"data": { "taskId": "tk-hiapi-01KWEZSS16W8DQBQAYMRPAMV2N" },
"message": "success"
}
Use the bare model id — kling-3.0-omni/image-to-video — and authenticate with Authorization: Bearer sk-<your-key>.
curl -s "https://api.hiapi.ai/v1/tasks/tk-hiapi-01KWEZSS16W8DQBQAYMRPAMV2N" \
-H "Authorization: Bearer $HIAPI_API_KEY"
While the job runs, data.status moves through in-progress states until it lands on a terminal value. On success you get:
{
"code": 200,
"data": {
"taskId": "tk-hiapi-01KWEZSS16W8DQBQAYMRPAMV2N",
"model": "kling-3.0-omni/image-to-video",
"status": "success",
"output": [
{
"type": "video",
"url": "https://temp.hiapi.ai/.../result-0.mp4",
"expireAt": 1783519537
}
]
},
"message": "success"
}
Your video is data.output[0].url. Note the expireAt timestamp: the URL is temporary, so download the file as soon as the task succeeds rather than storing the link.
Here's a self-contained script that creates the task, polls to completion, and downloads the MP4 — standard library only, nothing to install:
import os
import time
import json
import urllib.request
API_BASE = "https://api.hiapi.ai/v1/tasks"
API_KEY = os.environ["HIAPI_API_KEY"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def _request(url, method="GET", body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, headers=HEADERS, method=method)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read())
def create_task(first_frame_url, prompt="", duration=5):
payload = {
"model": "kling-3.0-omni/image-to-video",
"input": {
"image_urls": [first_frame_url], # required; first entry is the start frame
"prompt": prompt, # optional: describe the motion
"duration": duration, # integer seconds, not a string
},
}
data = _request(API_BASE, method="POST", body=payload)
task_id = data["data"]["taskId"]
print("created task:", task_id)
return task_id
def wait_for_task(task_id, timeout_s=600, poll_interval=5):
deadline = time.time() + timeout_s
while time.time() < deadline:
data = _request(f"{API_BASE}/{task_id}")["data"]
status = data.get("status")
if status == "success":
return data["output"][0]["url"]
if status == "fail":
err = data.get("error", {})
raise RuntimeError(f"task failed: {err.get('code')} {err.get('message')}")
print(" status:", status)
time.sleep(poll_interval)
raise TimeoutError(f"task {task_id} did not finish in {timeout_s}s")
def download(url, path):
urllib.request.urlretrieve(url, path) # grab it now — the URL expires
print("saved:", path)
if __name__ == "__main__":
tid = create_task(
"https://your-cdn.example.com/first-frame.jpg",
prompt="The camera slowly pushes in as the subject turns to look at the viewer",
)
video_url = wait_for_task(tid)
download(video_url, "kling-output.mp4")
Run it:
python3 generate_video.py
Image-to-video takes a different input from text-to-video — most importantly, it starts from an image, so there's no aspect_ratio to set (the frame defines it). Send only these fields inside input:
| Field | Type | Required | Notes |
|---|---|---|---|
image_urls | array of strings | Yes | The first frame(s). Entry 0 is the starting frame. Must be a public URL the API can fetch. |
prompt | string | No | Describe the motion and camera move. Keep it about how things move, not what the scene is — the image already sets that. |
duration | integer | No | Clip length in seconds (e.g. 5). Must be an integer — "5" as a string returns a 400. |
resolution | string | No | Output resolution (e.g. 720p, 1080p, 4K). |
Two things the schema will bite you on:
aspect_ratio and negative_prompt — are rejected here with 400: additional properties 'aspect_ratio' not allowed. The aspect ratio comes from your input image, so there's nothing to set.duration: "5" (a string) returns got string, want integer. Send a real integer.For the full list of Kling variants (text-to-video, image-to-video, reference-to-video) and their capabilities, see the hiapi models catalog.
Polling is fine for scripts and prototypes. For a backend firing many jobs, a callback is better — hiapi POSTs to your URL when the task reaches a terminal state, so you skip the poll loop entirely.
Add a top-level callback object (a sibling of model and input, not inside input):
curl -s -X POST "https://api.hiapi.ai/v1/tasks" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kling-3.0-omni/image-to-video",
"input": {
"image_urls": ["https://your-cdn.example.com/first-frame.jpg"],
"prompt": "Gentle wind moves the leaves; the camera drifts left",
"duration": 5
},
"callback": {
"url": "https://your-app.example.com/webhooks/hiapi",
"when": "final"
}
}'
Your endpoint receives the same task object you'd see from a poll, including data.output[0].url. when: "final" means you're notified only when the job succeeds or fails — no intermediate noise. (The URL has to be publicly resolvable; hiapi validates it at create time and rejects one it can't resolve.)
Polling vs. callback — when to use which:
taskId and treat a repeat delivery of the same id as a no-op.Error handling. Three failure modes to code against:
400) — a schema problem: missing image_urls, an unknown field (aspect_ratio, negative_prompt), or a wrong type (duration as a string). The message names exactly what's wrong. These are caller bugs — fix the payload, don't retry.401) — the response body's error.code is permission_denied, meaning the key is missing, invalid, or not entitled to the model. Check the Authorization header and the key's model permissions in your dashboard.200, but a later poll shows data.status == "fail" with a populated data.error.code / data.error.message. Handle both the HTTP status and the task status.For per-request costs across durations and resolutions, see hiapi pricing. The complete task-API reference — status values, callback payloads, and every field — lives in the hiapi API docs.
What's the difference between image-to-video and text-to-video?
Text-to-video starts from a prompt alone. Image-to-video starts from your image (image_urls) and animates it — the prompt just steers the motion. If you need a specific first frame (a product shot, a character, your brand art), use image-to-video so the output preserves it.
Do I have to send a prompt?
No. Only image_urls is required. But a short motion prompt ("the camera pushes in", "the water ripples") gives you far more control than leaving it blank.
Can I pass a local file instead of a URL?
No — image_urls must be publicly reachable URLs. Upload your first frame to your own storage or CDN and pass that link.
Why did my request fail with "additional properties … not allowed"?
The schema is strict and per-model. Fields that work on text-to-video, like aspect_ratio and negative_prompt, are not accepted for image-to-video. Send only image_urls, prompt, duration, and resolution inside input, and put callback at the top level.
How long is the output URL valid?
The output[0].url includes an expireAt timestamp and is temporary. Download the MP4 to your own storage as soon as the task reports success — don't serve the temp URL to end users.
What does a permission_denied error mean?
It's an HTTP 401: the API key is invalid or isn't authorized for kling-3.0-omni/image-to-video. Verify the Authorization: Bearer sk-… header and the key's permissions in your dashboard.
Can I generate video without an API key? No. Every request must be authenticated with a Bearer key from your hiapi account.
Key Takeaways