HiAPI
Blog

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

Nov 3, 2026 11 min read
米色纸层与石墨色顶边、一块陶土色纸片层叠成阶梯状的封面图,呈现平静的地层节奏。

What the kandinsky-6.0-pro/text-to-video request does

kandinsky-6.0-pro/text-to-video turns a text prompt into a short video clip. You send the prompt and a few generation parameters, and the model produces a video file you can save and play. That is the whole product: prompt in, clip out.

The important architectural detail is that this is not a synchronous call. You do not get a video back in the response body of your first request. Instead you create a job, the API returns an identifier for that job, and the model works in the background. Your code then checks the job's status until it reports completion, at which point a result is available to download.

That shape has consequences you should design around from the start:

  • Your client must poll. A single POST cannot give you a finished video, so any integration needs a status loop or a webhook-style re-check.
  • Requests should be idempotent from your side. If your script crashes after submitting, you do not want to silently create a second job. Capture the job identifier and persist it.
  • Latency is variable. Video generation is slower than image or text calls, so waiting is normal, not a bug.

The rest of this tutorial follows that lifecycle in order: prepare, submit with curl, poll, download, then rebuild the same flow in Python. Treat the code as a working skeleton — endpoint paths and field names should always be confirmed against the API reference for this model, since those are the details that shift as the platform evolves.

Before you send the first request

You need four things in place before the first call.

1. A HiAPI account and an API key

Authenticate with an API key issued from your HiAPI account. Keep the key out of source control and out of your shell history where possible. The usual pattern is to export it as an environment variable and have your code read from there:

export HIAPI_API_KEY="your-key-here"

Every request below assumes the key is sent as a bearer token in the Authorization header. If you have not created a key yet, do that first in your account dashboard.

2. An HTTP client

curl is enough to prove a request works. For anything repeatable, use Python 3 with the requests library — one dependency, readable code, and easy to drop into a scheduler or service later:

python3 -m pip install requests

3. Somewhere to put the output

Video files are large compared with JSON responses. Decide up front whether you are writing to a local directory or streaming to object storage. For this tutorial, a local ./output directory is fine:

mkdir -p output

4. Familiarity with your prompt

Write the prompt before you write code. A concrete, descriptive prompt — subject, action, camera behaviour, visual style — gives you a baseline you can compare against when something looks wrong. Changing one variable at a time is much easier when the prompt is a named constant in your script rather than a string typed at a shell prompt.

That is the full prerequisite list. There is no SDK to install and no build step; the platform is reached over plain HTTPS.

The working curl request

The first goal is a single command that returns a job, not a video. Get that working before adding any code.

The request has three parts: the endpoint for the model, an Authorization header, and a JSON body carrying your prompt and generation parameters.

curl -X POST "https://api.hiapi.ai/v1/models/kandinsky-6.0-pro/text-to-video/jobs" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A slow drone shot over a misty pine forest at sunrise, soft golden light, cinematic",
    "duration_seconds": 5,
    "aspect_ratio": "16:9"
  }'

Read the response carefully. You are looking for:

  • A job identifier — the handle you will poll with. It may be named id, job_id, or similar.
  • An initial status field, typically something like queued or processing.
  • Any echoed parameters, which are useful for confirming that your body was parsed the way you intended.

A response that returns a job identifier and a non-error status means the request was accepted. A video file in that response would mean something different happened than described here.

Confirm the contract before you build on it

The field names above — the endpoint path, duration_seconds, aspect_ratio, the identifier key — are the parts most likely to vary by model and over time. The reliable source of truth is the model's API reference page. Check it once, copy the exact names into your script as constants, and structure your code so a name change is a one-line edit rather than a search across the codebase.

If you get a 4xx, the body of the error is more informative than the status code. 401 means the key is wrong or missing; 422-style validation errors usually name the offending field.

Submitting and polling the job to completion

Once the submit call works, the lifecycle is: submit, capture the identifier, poll, stop on a terminal state.

Submit

The POST from the previous section creates the job. Whatever your code does next, it should store the returned identifier somewhere durable — a variable in a script, a row in a jobs table, a message in a queue. If the process dies after the submit response is received but before the identifier is persisted, you lose the ability to poll that job.

Poll

Polling is a GET against the job resource, using the identifier you captured:

curl "https://api.hiapi.ai/v1/models/kandinsky-6.0-pro/text-to-video/jobs/$JOB_ID" \
  -H "Authorization: Bearer $HIAPI_API_KEY"

The response reports the job's current status. In practice you will see a small set of states: queued or pending, processing, completed, and some form of failed or cancelled. Design your loop around those categories rather than around exact strings — map whatever the API returns onto running, done, failed in your own code, so a rename does not break your retry logic.

Decide how long to wait

Two numbers matter: the interval between polls and the overall timeout.

  • Interval. Poll every few seconds, not in a tight loop. A tight loop gives you nothing extra and can trip rate limits. Something on the order of seconds between requests is reasonable for video jobs.
  • Timeout. Set a ceiling. Video generation takes time, but "forever" is not an acceptable answer for a production script. Pick a maximum wall-clock budget, and treat exceeding it as a failure you can log and retry deliberately.

A pattern that survives real use

In production, the polling step is usually decoupled from the submit step rather than held in one sleeping process. A worker submits, records the job, and exits; a scheduled re-check picks up unfinished jobs and polls them. That is more robust to restarts and easier to observe. For the tutorial, a simple loop in one process is fine — just structure it so the loop is a function, not inline code, and moving it to a scheduled worker later is straightforward.

Downloading the generated clip

A completed job is not yet a file on your disk. The status response for a finished job will point you at the output — often a URL, sometimes a nested object containing a URL. Read that field and treat it as the input to a download step.

curl -L "$OUTPUT_URL" -o output/clip.mp4

A few things to check so you know the download actually succeeded:

  • Follow redirects. Output URLs are frequently served through a redirect; -L handles that with curl, and allow_redirects=True handles it in Python.
  • Verify content type. A completed job should yield a video payload. If you get JSON or an HTML error page saved as clip.mp4, the download went wrong, not the generation.
  • Check size. A zero-byte or suspiciously small file is a failed download. Compare the byte count against what the response headers advertised, if present.
  • Consider expiry. Output URLs are often time-limited. Download promptly after completion, and if you need durability, copy the file into your own storage rather than storing the URL as a permanent reference.

For anything beyond a tutorial, stream the download to its destination rather than loading the whole file into memory, and write to a temporary filename before renaming — that way a half-finished download never looks like a complete one to downstream code.

The same request in Python

Here is the curl flow rebuilt as one callable function: submit, poll, download. The names to adjust are collected at the top as constants.

import os
import time
import requests

API_BASE = "https://api.hiapi.ai/v1"
MODEL_PATH = "/models/kandinsky-6.0-pro/text-to-video"
API_KEY = os.environ["HIAPI_API_KEY"]

HEADERS = {"Authorization": f"Bearer {API_KEY}"}

RUNNING_STATES = {"queued", "pending", "processing", "running"}
DONE_STATES = {"completed", "succeeded", "done"}
FAILED_STATES = {"failed", "cancelled", "error"}


def generate_clip(prompt, out_path, timeout_s=600, interval_s=5):
    # 1. submit
    resp = requests.post(
        API_BASE + MODEL_PATH + "/jobs",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"prompt": prompt, "duration_seconds": 5, "aspect_ratio": "16:9"},
        timeout=30,
    )
    resp.raise_for_status()
    job = resp.json()
    job_id = job["id"]
    print(f"submitted job {job_id}")

    # 2. poll
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        status_resp = requests.get(
            f"{API_BASE}{MODEL_PATH}/jobs/{job_id}",
            headers=HEADERS,
            timeout=30,
        )
        status_resp.raise_for_status()
        data = status_resp.json()
        state = str(data.get("status", "")).lower()

        if state in DONE_STATES:
            break
        if state in FAILED_STATES:
            raise RuntimeError(f"job {job_id} ended in state {state}: {data}")
        time.sleep(interval_s)
    else:
        raise TimeoutError(f"job {job_id} did not finish within {timeout_s}s")

    # 3. download
    url = data["output"]["url"] if isinstance(data.get("output"), dict) else data["output"]
    with requests.get(url, stream=True, allow_redirects=True, timeout=60) as r:
        r.raise_for_status()
        tmp = out_path + ".part"
        with open(tmp, "wb") as f:
            for chunk in r.iter_content(chunk_size=1 << 16):
                f.write(chunk)
        os.replace(tmp, out_path)
    print(f"saved {out_path}")
    return job_id


if __name__ == "__main__":
    generate_clip(
        "A slow drone shot over a misty pine forest at sunrise, soft golden light, cinematic",
        "output/clip.mp4",
    )

Why it is written this way

  • State sets instead of exact matches. Mapping returned statuses onto running/done/failed means a new intermediate state does not break the loop.
  • Monotonic deadline. time.monotonic() is immune to clock adjustments, so the timeout is honest.
  • Atomic write. Writing to .part and renaming means a crashed download never leaves a truncated file that looks valid.
  • The job id is returned. Callers can log it, store it, and poll again later if needed — which is the first step toward moving polling out of this process entirely.

Adjust the endpoint path, the request field names, and the response keys to match the model's API reference. Everything else — the loop, the timeout, the download — is generic.

Error states and debugging checklist

When a run fails, isolate the stage before changing anything. Each stage has a distinct failure signature.

Authentication failures

A 401 or 403 on submit means the key is missing, wrong, or not being sent. Check that the environment variable is actually populated in the shell or process running the code — a key exported in one terminal is invisible to a cron job. Print the first few characters of the key to confirm it resolved, never the whole thing.

Malformed requests

A 400- or 422-class error on submit means the body did not validate. The response body usually names the field. Common causes: a misspelled parameter, a duration outside the supported range, or a prompt that is empty. Compare your JSON against the API reference field by field; a single renamed key is the most frequent culprit.

Job stuck in a running state

If polling never reaches a terminal state, confirm three things: that the identifier you are polling is the one you submitted, that your polling interval is not so aggressive it is being rate-limited, and that your timeout is long enough for the model. Log the raw status response on each poll so you can see whether the state is advancing or frozen.

Job completed but download failed

A completed job with a broken download is a file-handling problem, not a generation problem. Check redirects are followed, that the URL has not expired, and that the content type is video rather than JSON. Try the URL in isolation with curl -L to separate it from your script.

Repeated failures on the same prompt

If one prompt consistently fails while others succeed, suspect content policy or prompt length before infrastructure. Change one variable — shorten the prompt, simplify the scene description — and retry.

A general rule

Log four things on every job: the job identifier, the final state, the elapsed time, and the HTTP status of the download. Those four values turn most "it didn't work" reports into a specific, fixable stage within a minute.

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