HiAPI
Blog

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

Nov 3, 2026 11 min read
深色底上青绿与石板灰的扁平液态色块相互交叠,构成 kandinsky-6.0-pro 视频教程的封面图。

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-video model specifically. Model selection lives in the path, not the body, so a typo here is a 404 rather than a friendly validation message.
  • Authorization carries the bearer token from the environment variable you exported. Nothing else authenticates the call.
  • Content-Type: application/json matters for any POST with a JSON body; omit it and some proxies will mangle the payload before it reaches the API.
  • image is your first-frame reference — the URL form here, or an inline encoded string if your image is private.
  • prompt describes the motion. Keep it about movement and camera behavior rather than restating what the image already contains.
  • duration and aspect_ratio shape 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=body vs data=json.dumps(body). Passing json= lets requests set the content type for you, which keeps the headers minimal. If you use data= instead, set Content-Type explicitly.
  • 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. succeeded gives you an output, failed gives 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_wait guarantees 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 Authorization and the value begins with Bearer followed 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_ratio with 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 →

Start building with persistent AI artifacts

Get your API key and call image, video and audio models through one task-based API.

Read the API docs