Tutorial
kandinsky-6.0-pro/image-to-video on HiAPI: curl + Python Tutorial
A working image-to-video request on HiAPI, from first-frame upload to async polling and final download
What the image-to-video flow looks like on HiAPI
Every image-to-video call on HiAPI follows the same three-stage lifecycle, and knowing it up front makes the rest of this tutorial feel mechanical rather than magical.
Stage 1: submit a job
You send a POST request to the kandinsky-6.0-pro/image-to-video endpoint with your first-frame image and a short prompt describing the motion you want. The API does not return a video byte stream in that response. Instead, it accepts the work and returns a job identifier — a handle you use for everything that follows.
Stage 2: poll for status
Video generation takes time, so the endpoint is asynchronous. You call a status route with the job identifier, and it reports where the job stands: queued, processing, succeeded, or failed. You repeat that check on a short interval until the state is terminal.
Stage 3: fetch the output
Once the job reports success, the status payload carries the location of the finished video. You download it and write it to disk. That is the whole contract.
Why this shape matters
The submit-and-poll split is not incidental. It means your HTTP client never has to hold a socket open for the duration of a render, and it means you can persist a job identifier, walk away, and resume later. When something goes wrong, the status field tells you where it went wrong, which is far more useful than a single opaque timeout.
Keep those three stages in mind: submit, poll, download. The curl and Python examples below are just two ways of writing the same sequence.
Prepare your first frame and API credentials
Two things go wrong before most first calls: the key and the image. Sort both out now and the rest of the tutorial goes smoothly.
Set your API key as an environment variable
Do not paste your key into source files. Export it in your shell so both curl and Python pick it up without hardcoding anything:
export HIAPI_API_KEY="your_api_key_here"
On Windows PowerShell the equivalent is $env:HIAPI_API_KEY="your_api_key_here". Verify it is visible to your shell before continuing:
echo "$HIAPI_API_KEY" | head -c 4; echo
You should see the first characters of the key. If you see nothing, the variable did not persist into this shell session — a very common cause of 401 responses later.
Choose a valid first-frame image
The first frame anchors the whole video: its subject, its composition, and its aspect ratio. A few practical constraints prevent most rejections:
- Format: use a standard web image format such as JPEG or PNG. Avoid obscure codecs, progressive edge cases, or CMYK color profiles.
- Accessibility: the API needs to actually read the bytes. If you pass a URL, make sure it is publicly reachable or correctly authenticated; if you pass inline data, make sure the encoding is intact.
- Sizing: a reasonably sized image is fastest to validate. Extremely large files slow the request down and are frequently the source of upload errors.
- Content: pick a frame that makes sense as a starting point for motion — a clear subject, uncluttered background. Motion prompts describe how to move away from this frame, so a confusing frame produces a confusing video.
Decide how you will reference the image
The request body lets you point at the first frame either by URL or by inline encoded data. Pick based on where your image lives:
- If the file is already hosted and publicly readable, a URL is the cleanest option.
- If the image is local or private, inline encoding keeps the whole request self-contained at the cost of a larger payload.
Either way, keep the image reference in the same variable shape in curl and Python so the two examples stay directly comparable.
Make the request with curl
Here is a complete submission request. Replace the image reference and prompt with your own, then run it.
curl -sS -X POST "https://api.hiapi.ai/v1/kandinsky-6.0-pro/image-to-video" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image": "https://your-host.example/first-frame.jpg",
"prompt": "slow dolly-in on the subject, gentle ambient motion, stable camera",
"duration": 5,
"aspect_ratio": "16:9"
}'
What each part does
- The URL targets the
kandinsky-6.0-pro/image-to-videomodel specifically. Model selection lives in the path, not the body, so a typo here is a 404 rather than a friendly validation message. Authorizationcarries the bearer token from the environment variable you exported. Nothing else authenticates the call.Content-Type: application/jsonmatters for any POST with a JSON body; omit it and some proxies will mangle the payload before it reaches the API.imageis your first-frame reference — the URL form here, or an inline encoded string if your image is private.promptdescribes the motion. Keep it about movement and camera behavior rather than restating what the image already contains.durationandaspect_ratioshape the output. Match the aspect ratio to your first frame; a mismatch crops or letterboxes it. Billing is metered on the output you generate, so these settings also determine what you are charged for.
Read the response
A successful submission returns a job payload, not a video. Capture the job identifier from it — you will feed it to the status endpoint in a moment:
curl -sS -X POST "https://api.hiapi.ai/v1/kandinsky-6.0-pro/image-to-video" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d @body.json | jq -r '.id'
Using a body.json file instead of inline -d keeps long prompts and encoded images readable. jq is optional but turns the response into a single line you can paste straight into the next call.
Call the same endpoint from Python
The Python version is the same request with the same body, wrapped in a function so you can reuse it. Keep the body identical to the curl version — that is the point of showing both.
import os
import json
import requests
API_BASE = "https://api.hiapi.ai/v1"
ENDPOINT = f"{API_BASE}/kandinsky-6.0-pro/image-to-video"
def submit_video_job(image_ref: str, prompt: str, duration: int = 5, aspect_ratio: str = "16:9") -> str:
"""Submit a first-frame image-to-video job and return the job id."""
api_key = os.environ.get("HIAPI_API_KEY")
if not api_key:
raise RuntimeError("HIAPI_API_KEY is not set in this environment")
body = {
"image": image_ref,
"prompt": prompt,
"duration": duration,
"aspect_ratio": aspect_ratio,
}
response = requests.post(
ENDPOINT,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json=body,
timeout=60,
)
if response.status_code >= 400:
raise RuntimeError(f"submit failed: {response.status_code} {response.text}")
payload = response.json()
return payload["id"]
if __name__ == "__main__":
job_id = submit_video_job(
image_ref="https://your-host.example/first-frame.jpg",
prompt="slow dolly-in on the subject, gentle ambient motion, stable camera",
)
print(json.dumps({"job_id": job_id}, indent=2))
Notes on the Python version
- Fail fast on the key. The explicit environment check turns the most common mistake into a clear local error instead of a remote 401.
json=bodyvsdata=json.dumps(body). Passingjson=lets requests set the content type for you, which keeps the headers minimal. If you usedata=instead, setContent-Typeexplicitly.- A generous timeout. Video submission occasionally involves moving a large image payload. A short timeout will produce spurious failures.
- Read the body on error. Raising with both the status code and the response body means the failure message actually tells you what the API objected to.
If your first frame is a local file, read it, base64-encode it, and substitute that string for image_ref inside submit_video_job. The function signature does not change, which is exactly why the two call styles share one body definition.
Poll the job and download the video
A submitted job is not a finished video. Poll until the job reaches a terminal state, then download.
The polling loop
import time
import requests
def wait_for_video(job_id: str, interval: float = 10.0, max_wait: float = 600.0) -> str:
"""Poll the job until it finishes; return the output URL."""
import os
api_key = os.environ["HIAPI_API_KEY"]
url = f"https://api.hiapi.ai/v1/kandinsky-6.0-pro/image-to-video/{job_id}"
headers = {"Authorization": f"Bearer {api_key}"}
deadline = time.time() + max_wait
while time.time() < deadline:
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
payload = response.json()
status = payload.get("status")
if status == "succeeded":
return payload["output"]
if status == "failed":
raise RuntimeError(f"job failed: {payload.get('error', 'unknown error')}")
time.sleep(interval)
raise TimeoutError(f"job {job_id} did not finish within {max_wait}s")
Three details make this loop behave:
- Back off by interval, not by luck. A fixed sleep of around ten seconds is a reasonable starting point; do not hammer the endpoint in a tight loop.
- Handle every terminal state.
succeededgives you an output,failedgives you an error message. Treat both as exits. A loop that only checks for success will spin until its deadline on a dead job. - Cap the wait.
max_waitguarantees your script ends instead of running forever behind a stuck job.
Download the finished video
Once you have the output URL, stream it to disk rather than loading the whole file into memory:
def download_video(output_url: str, path: str = "result.mp4") -> str:
with requests.get(output_url, stream=True, timeout=120) as response:
response.raise_for_status()
with open(path, "wb") as handle:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
handle.write(chunk)
return path
Putting the pieces together:
job_id = submit_video_job(
image_ref="https://your-host.example/first-frame.jpg",
prompt="slow dolly-in on the subject, gentle ambient motion, stable camera",
)
output_url = wait_for_video(job_id)
saved = download_video(output_url, "kandinsky_result.mp4")
print(f"saved to {saved}")
In production, persist job_id the moment submission succeeds. If your process restarts, you can re-enter the polling loop with the stored identifier instead of paying for a fresh submission.
Troubleshoot common request and job errors
Most failures fall into three buckets: the key, the image, or the job state. Isolate the bucket before changing anything else.
The key is rejected
Symptoms: an authentication or authorization error before any job is created.
- Confirm the variable is actually exported in this shell:
echo "$HIAPI_API_KEY". - Check for whitespace or newlines pasted along with the key; trim them.
- Verify the header is spelled
Authorizationand the value begins withBearerfollowed by a single space. - If curl works but Python does not, print
os.environ.get("HIAPI_API_KEY")from the same interpreter — the variable may exist in your shell but not in the environment your script inherits.
The image is rejected
Symptoms: a validation error on submission, or a job that fails immediately.
- Wrong or unusual format. Re-export as a standard JPEG or PNG and retry. CMYK profiles and exotic codecs are frequent culprits.
- Unreachable URL. Fetch your image URL in a private browser window; if you cannot get the bytes, neither can the API.
- Broken inline encoding. If you base64-encode locally, decode the string back once and confirm it is a valid image before sending.
- Aspect mismatch. If the video comes back cropped or letterboxed, align
aspect_ratiowith your first frame rather than fighting the output later.
The job does not finish, or fails
Symptoms: the status never leaves queued/processing, or reports failed.
- Read the error field. A failed job carries a message. It usually names the field or condition that broke. Do not guess — log the whole status payload.
- Respect the asynchronous design. Waiting a few seconds and re-checking is normal. A tight retry loop that gives up after two seconds is not a status mechanism.
- Cap your waits. If a job exceeds a generous ceiling, treat it as failed, surface the job id, and submit a fresh job rather than polling forever.
- Compare against a known-good call. If a custom image fails, run the exact curl from earlier in this tutorial with a small public image and a short prompt. If that succeeds, the problem is your input; if it also fails, the problem is credentials or the request shape.
Download errors after success
If the job reports succeeded but the download fails, the output URL is likely expired or access-controlled. Re-poll the job to get a fresh output location rather than retrying the stale one.
Work through the buckets in order — key, then image, then job state — and each error narrows down to one cause quickly. That is the whole loop: submit a first frame, poll to completion, download the result.
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