HiAPI
Blog

Tutorial

How to Use kling-4.0/text-to-video via the HiAPI API: curl, Python, and a Working Request

A developer walkthrough for cinematic prompting, synchronized audio, async polling, and output download

Nov 3, 2026 11 min read
深炭底色上一条宽大的琥珀色光带斜扫而过,下方配一条细窄的暗棕平行条带,整体用于文章封面。

What kling-4.0/text-to-video Does and When to Use It

kling-4.0/text-to-video is a generative video model available through the HiAPI unified API. You give it a text prompt, and it returns a video clip — motion, camera movement, and scene composition implied entirely by your description. It is a text-to-video endpoint: there is no source image to animate, so the model has full latitude over framing and subject appearance.

Use it when:

  • You need short cinematic clips generated from a script or prompt template — b-roll, teasers, concept boards, social video.
  • You want synchronized audio produced alongside the video rather than a silent file you have to score downstream.
  • You're building a pipeline where video generation is one step among many (scripting, assets, publishing) and you need an HTTP API rather than a web UI.

It is a weaker fit when you need frame-exact control over a specific real subject (image-to-video or editing models handle that better) or when you need a synchronous response inline with a user request — video generation takes time, so this endpoint is asynchronous by design.

Because everything goes through HiAPI, the same credential, request/response shape, and job lifecycle apply as with any other model on the platform. That consistency is the practical reason to build against it: adding or swapping a model later doesn't require rewriting your auth or polling layer.

Prerequisites and Authentication on HiAPI

Before the first request, make sure you have:

  • A HiAPI account and a project API key. Treat it as a secret — never commit it, never ship it in client-side code.
  • An HTTP client of your choice. The examples below use curl and Python 3 with requests.
  • The model identifier exactly as it appears in the catalog: kling-4.0/text-to-video. Typos in the model name are the single most common cause of failed first calls.

Credentials are sent as an HTTP header on every request, including polling requests. The conventional pattern on HiAPI is a bearer token:

export HIAPI_API_KEY="your-api-key-here"

Then in requests:

Authorization: Bearer $HIAPI_API_KEY
Content-Type: application/json

Keep the base URL and key in environment variables or a secrets manager rather than hard-coding them. For local development, a .env file plus os.environ lookup in Python is enough. For deployed services, inject the key at runtime.

One setup habit worth adopting early: build a tiny "who am I" style call first — any lightweight authenticated request — to confirm the key and base URL are correct before you debug a video job. Isolating auth from model logic saves time later.

A Working curl Request

The minimal call creates a job and returns a job identifier. Here is the shape:

curl -X POST "$HIAPI_BASE_URL/video/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kling-4.0/text-to-video",
    "prompt": "A slow aerial push over a rain-soaked Tokyo alley at night, neon signs reflecting in puddles, steam rising from vents, shallow depth of field, cinematic color grading",
    "generate_audio": true
  }'

Reading the body field by field:

  • model — the model identifier. Required.
  • prompt — your scene description. This is where most of your quality budget lives; see the prompting section.
  • generate_audio — requests synchronized audio alongside the video. Omit or set false if you only want the visual track.

The response is a job object, not a finished video. Expect a status field and an id you'll use for polling — for example, a status of queued or processing plus a job id. Store that id immediately; if your process dies, the id is your only handle on the work in flight.

Two practical notes. First, keep the JSON in a file and use -d @payload.json once prompts grow long — shell quoting around multiline prompts is a frequent source of errors. Second, capture the response body and the HTTP status code separately (curl -s -w "\n%{http_code}"), so you can distinguish a rejected request from a network failure.

Translating the Request to Python

The same request in Python, structured so it's easy to lift into a real script:

import os
import requests

BASE_URL = os.environ["HIAPI_BASE_URL"].rstrip("/")
API_KEY = os.environ["HIAPI_API_KEY"]

HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

def create_video_job(prompt: str, generate_audio: bool = True) -> dict:
    payload = {
        "model": "kling-4.0/text-to-video",
        "prompt": prompt,
        "generate_audio": generate_audio,
    }
    resp = requests.post(
        f"{BASE_URL}/video/generations",
        headers=HEADERS,
        json=payload,
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()

if __name__ == "__main__":
    job = create_video_job(
        "A slow aerial push over a rain-soaked Tokyo alley at night, "
        "neon signs reflecting in puddles, steam rising from vents, "
        "shallow depth of field, cinematic color grading"
    )
    print(job)

Why it's written this way:

  • requests.post(..., json=payload) handles serialization and the content type together, avoiding manual json.dumps.
  • raise_for_status() turns a 4xx/5xx into an exception you can catch, instead of silently parsing an error body as a job.
  • A timeout is mandatory. Without one, a stuck connection blocks your worker indefinitely.
  • Config lives in module-level constants read from the environment, so the function is testable and reusable.

This function only creates the job. It returns immediately with a job handle — which is exactly what you want in a web handler or a queue worker. The next sections cover waiting for the result.

Writing Cinematic Prompts That Hold Up

Text-to-video models reward structure. A prompt that names the subject, the camera, the motion, and the mood consistently outperforms a one-line idea, because each element removes ambiguity the model would otherwise resolve at random.

A workable template:

[subject and setting], [camera position and movement],
[motion within the frame], [lighting], [mood or grade], [style reference]

Applied:

A lone cyclist in a yellow raincoat crossing a stone bridge at dawn, low camera tracking alongside at wheel height, mist drifting over the water, soft blue-hour light with warm lantern accents, calm and slightly melancholic, anamorphic widescreen look.

What each part is doing:

  • Subject and setting — concrete nouns beat abstractions. "A lone cyclist in a yellow raincoat" gives the model something to render; "freedom" does not.
  • Camera — position (low camera, aerial, eye level) and movement (tracking, slow push in, static) are the strongest levers on perceived production value.
  • Motion within the frame — mist drifting, fabric moving, water rippling. Video needs motion; name it.
  • Lighting and mood — time of day, color temperature, emotional register. Keep these coherent with each other.
  • Style — lens or format language (anamorphic, shallow depth of field, cinematic color grading) tends to shape texture more reliably than naming a director.

Keep prompts to a few dense sentences rather than a paragraph of competing ideas. If a shot needs two distinct actions, generate two clips. And iterate: change one variable at a time — camera, then lighting — so you learn what the model responds to rather than guessing.

Handling Asynchronous Jobs and Polling

Video generation isn't instantaneous, so the pattern is create, then poll. You already have the create half; here's the loop:

import time

def wait_for_job(job_id: str, timeout: int = 600) -> dict:
    deadline = time.monotonic() + timeout
    delay = 2.0
    while time.monotonic() < deadline:
        resp = requests.get(
            f"{BASE_URL}/video/generations/{job_id}",
            headers=HEADERS,
            timeout=30,
        )
        resp.raise_for_status()
        job = resp.json()
        status = job.get("status")

        if status == "succeeded":
            return job
        if status in ("failed", "cancelled"):
            raise RuntimeError(f"Job {job_id} ended with status {status}: {job}")

        time.sleep(delay)
        delay = min(delay * 1.5, 15.0)  # back off, cap at 15s

    raise TimeoutError(f"Job {job_id} did not finish in {timeout}s")

The design decisions that matter in production:

Poll with backoff, not in a tight loop

Start with a short interval and grow it — the exponential backoff above caps at a fixed ceiling. Constant rapid polling burns your rate limit and gains nothing, since generation time doesn't shrink because you asked sooner.

Always have a deadline

Every poll loop needs a maximum wall-clock time and a clear error when it's exceeded. Otherwise a stuck job silently occupies a worker forever.

Treat terminal states as terminal

Distinguish in-progress states (queued, processing) from terminal ones (succeeded, failed, cancelled). Only success returns an output; the others should raise or route to a retry path, not loop.

Decouple polling from your request handler

In a web server, don't block the HTTP response waiting for video. Create the job, return the id to the client, and poll from a background worker or a queue consumer. Persist job ids and their last-known status so a restart doesn't lose track of in-flight work.

Downloading and Verifying the Output

When the job reaches succeeded, the response carries a reference to the generated media — typically a URL to the finished file. Download it promptly and treat the URL as ephemeral: fetch, store, and record your own durable location rather than passing the provider URL straight to end users.

import pathlib

def download_output(job: dict, dest: pathlib.Path) -> pathlib.Path:
    url = job["output"]["video_url"]
    with requests.get(url, stream=True, timeout=120) as r:
        r.raise_for_status()
        dest.parent.mkdir(parents=True, exist_ok=True)
        with open(dest, "wb") as f:
            for chunk in r.iter_content(chunk_size=1 << 20):
                f.write(chunk)
    return dest

Verification steps worth doing before handing the file to anyone:

  • Check the transfer completed. Stream in chunks, compare the written size against Content-Length when present, and fail loudly on a short read.
  • Confirm the audio track. If you requested generate_audio, the file should have an audio stream. A quick probe with ffprobe is the cheapest check:
ffprobe -v error -show_entries stream=codec_type -of csv=p=0 output.mp4

You should see both video and audio lines. If the audio is missing, re-check that the flag was set on the create call — it cannot be added after the fact.

  • Sanity-check duration and resolution against what you expected, so a truncated or mis-specified render is caught before publishing.
  • Store with a deterministic key (job id plus a hash), write to a temp path and rename on success, and keep the job id alongside the asset for traceability.

If the download fails mid-transfer, retry with backoff; if the URL has expired, you'll need to re-fetch the job record for a fresh reference.

Wrapping It Into a Reusable Client

The pieces above form a natural class. Consolidating them gives you something you can import across projects instead of copying:

class KlingTextToVideo:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url.rstrip("/")
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        }

    def generate(self, prompt: str, generate_audio: bool = True,
                 timeout: int = 600) -> dict:
        job = create_video_job(prompt, generate_audio)  # create half
        return wait_for_job(job["id"], timeout=timeout)  # poll half

A few things to add as the client matures:

  • Structured logging keyed on job id — status transitions, poll counts, elapsed time, and failures. When a render misbehaves, the job id is your join key across logs.
  • Bounded retries on transient errors only (network, 5xx). Never blind-retry a rejected request; a malformed payload will fail the same way every time and just costs you quota.
  • Idempotency at your layer. If your service might submit the same prompt twice, dedupe before calling create rather than paying for two jobs.
  • Prompt templates as data, not code. Keep your cinematic template in a config file so prompt iteration doesn't require a deploy.
  • Cost accounting by output seconds. Billing is usage-based on generated output, so tracking generated duration per job lets you attribute spend to features or users.

With the client in place, the pipeline is short: build a prompt from your template, call generate, download and probe the output, and hand the stored file to your app. The same auth header, job lifecycle, and download pattern carry over if you later add another model on HiAPI — which is the main reason to build the abstraction once.

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