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
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
curland Python 3 withrequests. - 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 manualjson.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
timeoutis 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-Lengthwhen 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 withffprobeis 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 →-
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