HiAPI
Blog

Tutorial

How to Use kling-3.0-omni/image-to-video via the HiAPI API: curl and Python Guide

Nov 3, 2026 13 min read
扁平液状色块交叠的抽象封面图,墨色底上点缀青瓷绿与石板灰,用于该教程文章。

What the image-to-video endpoint does

Video generation is not a single synchronous call. On HiAPI, kling-3.0-omni/image-to-video follows a submit-then-poll pattern: you send one HTTP request that describes the job, the API accepts it and returns a task identifier, and you then query that task until the video is finished.

That two-step shape exists for good reasons. Rendering a video from a still image takes time — far longer than a typical HTTP request should stay open. If the endpoint blocked until the file was ready, you would be fighting gateway timeouts and dropped connections on every call. By returning immediately with a task handle, the API lets your code decide how long to wait, how often to check, and what to do if something goes wrong.

A typical job has three logical parts:

  • Input media — the image (or images) that anchor the shot.
  • Generation settings — prompt text, motion-related options, audio and resolution choices.
  • Lifecycle — a submit response containing a task ID, followed by status checks that eventually yield a video URL or a failure reason.

It helps to think of the endpoint as a queue with a receipt. The submit call is you handing over the work order; the receipt is the task ID; the polling calls are you asking the counter whether the order is ready. Nothing about the video itself is returned at submit time, so any client you write should be organized around those two phases from the start.

Two consequences follow. First, your error handling has to be split: a rejected payload is a submit-time problem you can fix in code, while a failed render is a job-level problem that no amount of retrying the same payload will solve. Second, you need somewhere to keep the task ID — a log line, a database row, a variable in a script — because it is the only handle you have on the work in progress.

With that mental model in place, the rest of this guide is mostly plumbing: shaping the request, then waiting on it correctly.

Build a working curl request

Start with the smallest request that could possibly work, confirm it is accepted, and only then add options. The exact JSON field names and the endpoint path are defined in the model's reference page on HiAPI — check that page for the current schema rather than trusting a snippet from a blog post, including this one.

A submit call has a predictable anatomy. You authenticate with your HiAPI API key in the Authorization header, declare Content-Type: application/json, and send a JSON body. Conceptually the body looks like this:

{
  "model": "kling-3.0-omni/image-to-video",
  "input": {
    "image": "https://your-cdn.example.com/frame-first.jpg",
    "prompt": "A slow dolly-in as morning light crosses the room"
  }
}

The shape matters more than the literal values: a model identifier, an input object carrying your source image reference and prompt, and room for generation options. Verify each key against the current documentation before you hardcode it.

The equivalent curl invocation follows the usual pattern:

curl -sS -X POST "$HIAPI_BASE_URL/v1/video/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json

Keeping the payload in request.json rather than inlining it makes iteration far less painful — you edit a file, not a shell history entry.

Reading the response

A successful submit is deliberately boring. Expect an identifier for the task and a status that indicates it has been accepted but not completed. Parse it, print it, and keep it:

curl -sS -X POST "$HIAPI_BASE_URL/v1/video/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json | tee submit.json

Check the HTTP status code first. A 4xx means the request itself was rejected — bad key, malformed JSON, unsupported option, invalid image URL. A 2xx means the job entered the system, and from that point the useful signal comes from the task status, not the HTTP code.

Before writing any polling code, run the submit by hand once. Confirm you can see the task ID in the output. That single manual round trip catches most configuration mistakes early.

Handle first and last frame inputs

The image you send is not just a reference — it constrains where the shot begins. That distinction drives most of the design decisions in this section.

First frame only

When you supply a single image, treat it as the opening frame. The prompt then describes what should happen from that frame: camera movement, subject motion, lighting change, pacing. Because the model has to invent everything after the first frame, prompts that describe a trajectory work better than prompts that describe a static scene. "Hold still and look beautiful" gives the model nothing to animate; "she turns toward the window as the light shifts warm" gives it a direction.

Adding a last frame

A last-frame input turns the job into an interpolation problem. You provide both endpoints and the model generates a path between them. This is powerful for controlled transitions — a product rotating from front to back view, a character shifting between two poses — because the destination is pinned rather than imagined.

Three practical notes:

  • Keep the two frames compatible. Similar framing, perspective, and subject scale make interpolation believable. Wildly different compositions force the model to invent a bridge that may not exist.
  • Rewrite the prompt. With two frames, your text should describe the motion between them rather than the destination. Naming a movement style and a duration feels more natural than narrating the end state you already supplied.
  • Expect the request shape to change. The last frame is a separate input field, so your payload grows a key. Build your request object so optional fields can be added without restructuring everything.

In code, treat frames as a small set of named inputs rather than a loose bag of URLs:

inputs = {"image": first_frame_url}
if last_frame_url:
    inputs["last_frame"] = last_frame_url

That one conditional keeps single-frame and two-frame calls on the same code path, which matters once you start generating variations.

Finally, host your frames somewhere reliably reachable. A URL that expires mid-job will fail after submission, and the error will look like a generation failure rather than an asset problem.

Configure audio and output resolution

Audio and resolution are request-level choices, not global settings. You decide them per job, which means they belong in your payload builder next to the prompt rather than in some configuration constant you set once and forget.

Audio

Audio options typically let you request a soundtrack alongside the video, or omit it. Treat this as a modelling decision tied to your use case: a social clip may benefit from generated audio, while a clip you will score in an editor probably should not carry one. Requesting audio you never use costs you generation time for no benefit.

Because audio behaviour is one of the areas most likely to evolve, do not hardcode assumptions about what the audio track contains or how it relates to the prompt. Read the current model documentation, then verify the output once with a real job before designing anything downstream around it.

Resolution

Higher resolution means more pixels per frame and materially more compute. A practical workflow is to iterate at modest resolution while you tune the prompt and frame selection, then regenerate the winning configuration at the final resolution. Regenerating one good take is cheaper than discovering at the end that your best take was produced with the wrong settings.

A few habits worth adopting:

  • Validate before you submit. If the model does not support a resolution you pass, you want a clear submit-time rejection, not a confusing job-level failure minutes later.
  • Store the settings with the task. When you are comparing takes, you need to know which resolution and audio choice produced each one.
  • Do not assume parity across frames. A two-frame job with large source images may behave differently from a single-frame job; test both paths.

Where these belong in code

Group generation options into their own object so the intent is obvious and so you can swap defaults in one place:

options = {
    "resolution": "1080p",   # confirm supported values in the model docs
    "audio": True,
}

Comment the line that depends on documented values. Future you — or a teammate — should be able to tell instantly which fields are safe and which need checking against the reference page before a release. When in doubt, the model's documentation on HiAPI is the source of truth, not an example payload.

Poll for completion in Python

Polling code is where most integrations quietly become fragile. The goal is a small, reusable client that submits a job and waits on it with sane limits, rather than a script you copy-paste and edit each time.

A minimal client shape

import os, time, requests

BASE = os.environ["HIAPI_BASE_URL"]
KEY = os.environ["HIAPI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}

def submit(payload: dict) -> str:
    r = requests.post(f"{BASE}/v1/video/generations",
                      headers=HEADERS, json=payload, timeout=30)
    r.raise_for_status()
    return r.json()["id"]

def wait(task_id: str, interval: float = 5.0, max_wait: float = 600.0) -> dict:
    deadline = time.monotonic() + max_wait
    while time.monotonic() < deadline:
        r = requests.get(f"{BASE}/v1/video/generations/{task_id}",
                         headers=HEADERS, timeout=30)
        r.raise_for_status()
        job = r.json()
        if job["status"] in ("succeeded", "failed"):
            return job
        time.sleep(interval)
    raise TimeoutError(f"task {task_id} not finished in {max_wait}s")

Four details in that code carry most of the weight:

  • Explicit timeouts on every HTTP call. A hanging request is worse than a failed one.
  • A maximum wait. Unbounded loops turn a stuck job into a stuck service.
  • Terminal-state checking. Only stop when the status is genuinely final; treat everything else as still running, including states you do not recognise.
  • Returning the whole job object. The caller usually needs the output URL and any error detail, not just a boolean.

Polling cadence

Five seconds is a reasonable default for video jobs. Polling every 200ms burns quota and adds nothing, since the render will take minutes regardless. If you are submitting many jobs, put the polling loop in a worker rather than in a request handler, and back off slightly on long-running tasks.

Wiring it together

payload = {
    "model": "kling-3.0-omni/image-to-video",
    "input": {"image": first_frame_url, "prompt": prompt},
    "options": options,
}

job = wait(submit(payload))
if job["status"] == "failed":
    raise RuntimeError(job.get("error") or "generation failed")
print(job["output"]["url"])

Because submit returns a plain string and wait returns a dict, you can test each half independently: mock the submit response to exercise the polling logic, and call submit alone to check your payload against the live API.

Debug common failures

Failures cluster into three layers, and identifying the layer is most of the diagnosis.

Layer 1: submit-time rejections

These surface as 4xx responses before any task exists. Common causes:

  • Authentication — missing header, malformed bearer token, key not exported into the environment you think it is.
  • Payload shape — a misspelled field, a string where an array belongs, options nested at the wrong level.
  • Unsupported values — a resolution or audio combination the model does not accept.
  • Asset problems — an image URL that is unreachable, expired, or not an image.

Read the error body. APIs generally name the offending field, and the fastest fix is usually the one the message suggests. Reproduce the failing call in curl to confirm it is the payload and not your client code.

Layer 2: transport problems

Networking sits between your code and the task lifecycle. Timeouts and connection resets on the submit call are ambiguous: the job may or may not have been created. If you retry blindly you can spawn duplicates.

The safe pattern is to retry only idempotent-safe operations and to log every submit attempt along with its response. If you genuinely cannot tell whether a submit landed, check your task history before resubmitting. On the polling side, a single failed status check is not a failed job — catch the exception, wait, and ask again.

Layer 3: job-level failures

These arrive as a terminal status on an otherwise accepted task. They usually mean the content could not be generated as specified: two frames too dissimilar to interpolate, a prompt the model cannot reconcile with the source image, or an internal error during rendering.

Distinguish them clearly in your code, because the remedy is different. A 4xx means fix the request. A failed task means change the input or try again — and if it fails twice with the same payload, stop retrying and change something: simplify the prompt, pick closer frames, lower the resolution.

A quick triage checklist

  1. Is the HTTP status 4xx? Fix the payload or credentials; do not poll.
  2. Did the submit succeed but the task never finish? Your wait limit is too short or the job is stuck — log the task ID and investigate rather than looping forever.
  3. Did the task fail? Read the error detail, then adjust input rather than resubmitting unchanged.
  4. Did it succeed but the video is wrong? That is a prompt and frame-selection problem, not an API problem.

Ship it: production checklist

Before this goes in front of users, harden the pieces that fail quietly.

Configuration. Keep the base URL and API key in environment variables or a secret manager, never in source. Fail fast at startup if either is missing rather than at the first user request.

Timeouts and retries. Every outbound call needs an explicit timeout. Retry submit only where duplication is tolerable, and treat polling retries as safe — an idempotent status read. Cap total wait time and surface a clear timeout to the caller.

Task IDs are records. Persist the task ID alongside the submitted payload: frames, prompt, resolution, audio choice, and timestamp. When someone asks why a video looks wrong two weeks later, that row is the entire answer.

Outputs are ephemeral. Treat returned video URLs as temporary. Download and store them in your own durable storage as soon as a job succeeds, and record the stored location against the task.

Cost awareness. Video generation is billed by generated output, so resolution, duration, and take count are the levers that matter. Iterate at low resolution, generate finals deliberately, and log settings so you can attribute spend to a use case instead of discovering it on an invoice.

Observability. Emit a log line at submit and at terminal state, with task ID, latency, and status. A simple success-rate metric per model will tell you when a prompt change or an option tweak starts failing jobs.

Validate against the docs, not against examples. Before each release that touches audio, resolution, or frame inputs, confirm the field names and accepted values on the model's reference page, since these are the settings most likely to drift.

With the client, the polling loop, and these safeguards in place, kling-3.0-omni/image-to-video becomes a predictable component rather than a script you hope still works.

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