HiAPI
Blog

Tutorial

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

Nov 3, 2026 11 min read
极简封面图:浅藕紫色与墨黑色两块纯色平面以一道清晰的硬边相接,边缘有一条细紫缝,构图庄重平静。

What the hailuo-2.3/text-to-video endpoint does

If you have a text prompt and need a short video clip generated from it, hailuo-2.3/text-to-video is the endpoint you are looking for. It accepts a natural-language prompt describing the scene, the subject, and the motion you want to see, and returns a generated video clip. The model is tuned for short-form output, so it works best when you ask for a compact, self-contained moment rather than a long narrative.

On HiAPI, the call follows the same pattern as other asynchronous generation endpoints: you send a request, the service accepts the job, and you poll until the result is ready. That means the endpoint is a natural fit for backend pipelines—content tools, prototype generators, batch jobs, or anything that needs a few seconds of video on demand without a human sitting in a video editor.

The request carries a prompt and a small set of generation controls; the response carries a job identifier, and later a URL to the finished video. You do not need to manage GPUs or model weights—HiAPI handles the compute and returns a link to the output. If you have used other text-to-video APIs, the shape will feel familiar, but the prompting style for hailuo-2.3 rewards thinking about physical motion rather than just visual description (more on that in a later section).

Prerequisites and API credentials

Before your first call, make sure you have three things ready:

  • An API key from your HiAPI account. Treat it like a password: keep it out of source control, load it from an environment variable, and rotate it if it leaks.
  • A HTTP client. For the shell examples below you only need curl. For the Python examples you need Python 3.8+ with the requests library (pip install requests).
  • The HiAPI API base URL. All requests are sent to the HiAPI API host, and the endpoint path is hailuo-2.3/text-to-video. You authenticate by sending your key in the Authorization header as a bearer token.

Set your key in the environment so it never appears in your code:

export HIAPI_API_KEY="your_api_key_here"

In Python you can read it with os.environ["HIAPI_API_KEY"]. In curl you will reference the same variable with $HIAPI_API_KEY.

One more thing to decide up front: where generated videos will live. The API returns a URL to the output, and that URL is meant to be fetched promptly and stored in your own bucket or filesystem—do not build a pipeline that assumes the URL is a permanent asset.

A working request with curl

Here is a complete curl call that submits a text-to-video job. Replace the prompt with your own scene description. The request body is JSON, and the key fields are the prompt and the generation parameters that control duration and output shape.

curl -X POST "https://api.hiapi.ai/v1/hailuo-2.3/text-to-video" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A glass marble rolls down a wooden ramp, picks up speed, and drops onto a stone floor, bouncing twice before settling",
    "duration": 6
  }'

A successful submission returns a JSON object containing a job identifier. It will look roughly like this:

{
  "id": "job_abc123",
  "status": "queued"
}

The important part is the id. Generation does not complete synchronously—the endpoint queues the job and returns immediately. Save the id; you will use it to poll for status and, eventually, to retrieve the video URL.

If you get an authentication error, check that the key is exported in the same shell session. If you get a validation error, check that the JSON body is well-formed and that the prompt is present. A 429 response means you are sending requests too quickly and should back off—more on that in the production section.

The same call in Python

The Python version does the same thing but wraps it in a reusable function so you can drop it into a pipeline. It submits a job and returns the job identifier.

import os
import requests

HIAPI_BASE = "https://api.hiapi.ai/v1"


def submit_text_to_video(prompt: str, duration: int = 6) -> str:
    """Submit a hailuo-2.3/text-to-video job and return its job id."""
    api_key = os.environ["HIAPI_API_KEY"]
    response = requests.post(
        f"{HIAPI_BASE}/hailuo-2.3/text-to-video",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        json={"prompt": prompt, "duration": duration},
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()
    return data["id"]


if __name__ == "__main__":
    job_id = submit_text_to_video(
        "A glass marble rolls down a wooden ramp, picks up speed, and drops onto a stone floor, bouncing twice before settling"
    )
    print("submitted job:", job_id)

A few details worth keeping:

  • raise_for_status() turns non-2xx responses into exceptions, which is what you want in a pipeline. It is better to fail loudly at submit time than to poll a job that was never created.
  • The timeout on the submit request is short on purpose. Submission should be fast; the long wait happens during polling, not here.
  • The duration argument is a separate parameter so callers can request shorter or longer clips without duplicating the request-building logic.

Once this function returns, you have everything you need to move on to status polling.

Writing motion-physics prompts for 6-10 second clips

Short clips are unforgiving. In six to ten seconds you cannot establish a character, build a world, and pay off a plot—so the prompts that work best describe a single continuous action with clear physical consequences. Think of the prompt as a shot description for a moment, not a story.

Lead with the subject and the action

Start with what is moving and what it is doing. "A glass marble rolls down a wooden ramp" instantly gives the model a subject, a material, and a direction of travel. Vague openers like "a beautiful scene" give it nothing to animate.

Describe the forces, not just the look

Motion physics prompting means naming the forces at play: gravity, friction, momentum, weight, elasticity. Words like rolls, picks up speed, bounces, settles, swings, topples, splashes, and rips imply a physical model. For a bouncing object, say how many bounces and where it ends up. For a falling object, say what it lands on and whether it breaks, dents, or rebounds.

Keep the camera explicit and simple

A locked-off shot or one slow camera move is usually enough for a short clip. If the camera and the subject both do complex things, the clip gets busy and the motion reads as noise. Name the shot type—close-up, wide, tracking—only when it matters to the action.

Build the event arc

A useful template for 6–10 second clips is: setup → action → consequence. The marble is at the top of the ramp; it rolls down and accelerates; it falls, bounces twice, and comes to rest. That arc gives the model a beginning, a middle, and an end that fit inside the clip length. Avoid asking for multiple unrelated actions; a single well-specified physical event reads far better than three half-finished ones.

Finally, keep prompts concrete. Materials, lighting, and surface textures help the model ground the motion—a marble on wood behaves differently from a marble on ice, and saying so guides the result.

Handling async jobs with polling

Generation takes time, so the endpoint is asynchronous. After you submit a job you receive an id; you then poll a status endpoint until the job reaches a terminal state. A robust polling loop handles three things: interval, timeout, and error surfacing.

import time
import requests

HIAPI_BASE = "https://api.hiapi.ai/v1"


def wait_for_video(job_id: str, api_key: str, interval: float = 3.0, max_wait: float = 600.0) -> str:
    """Poll until the job finishes and return the output video URL."""
    deadline = time.time() + max_wait
    url = f"{HIAPI_BASE}/hailuo-2.3/text-to-video/{job_id}"

    while time.time() < deadline:
        response = requests.get(
            url,
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=30,
        )
        response.raise_for_status()
        data = response.json()
        status = data.get("status")

        if status == "completed":
            return data["output"]["video_url"]
        if status in ("failed", "cancelled"):
            raise RuntimeError(f"job {job_id} ended with status {status}: {data.get('error')}")

        time.sleep(interval)

    raise TimeoutError(f"job {job_id} did not finish within {max_wait} seconds")

Notes on the loop:

  • Poll on an interval, not in a tight loop. A few seconds between checks is plenty and avoids hammering the API.
  • Always cap total wait time. A stuck job should not hang a worker forever. The max_wait value here is conservative; tune it to your clip lengths and queue conditions.
  • Treat failed as a real branch. Surface the error payload rather than retrying blindly—prompt or parameter problems will not fix themselves.
  • Return the video URL, not the raw response. Callers of this function should not need to know the response schema.

For heavier workloads, run the submit and poll steps in separate workers: one worker enqueues jobs and stores the job ID, another polls and downloads. That decouples your request latency from generation time.

Downloading and storing the output

Once polling returns a video URL, fetch the bytes and write them somewhere durable. Do this promptly: the returned URL is a delivery link, not a permanent asset store. Stream the download instead of loading the whole file into memory, and write to a temporary path before renaming so a partial download never looks like a finished file.

import os
import tempfile
import requests


def download_video(video_url: str, dest_path: str) -> None:
    """Stream the finished video to dest_path atomically."""
    os.makedirs(os.path.dirname(dest_path) or ".", exist_ok=True)
    with requests.get(video_url, stream=True, timeout=120) as response:
        response.raise_for_status()
        fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(dest_path) or ".")
        try:
            with os.fdopen(fd, "wb") as f:
                for chunk in response.iter_content(chunk_size=1 << 16):
                    f.write(chunk)
            os.replace(tmp_path, dest_path)
        except Exception:
            if os.path.exists(tmp_path):
                os.unlink(tmp_path)
            raise


api_key = os.environ["HIAPI_API_KEY"]
video_url = wait_for_video("job_abc123", api_key)
download_video(video_url, f"out/{'job_abc123'}.mp4")

A few practical choices:

  • Name files by job ID. It gives you a stable, collision-resistant key and makes it trivial to correlate a file back to the request that produced it.
  • Store metadata alongside the video. Save the original prompt, duration, and timestamp in a sidecar JSON or your database. Six months from now, you will want to know what produced a given clip.
  • Upload to object storage in production. Writing to local disk is fine for a prototype; a real pipeline should push the file to your bucket and record the object key, so the video survives worker restarts.

Production tips and common failure modes

Most first integrations break in the same handful of places. Here is what to watch for.

Retry the right things

Retry on transient failures—network errors, timeouts, 429, and 5xx responses—with exponential backoff and jitter. Do not retry on 4xx validation errors; a malformed prompt or bad parameter will fail the same way every time. If a generation job itself fails, surface the error and decide deliberately whether to resubmit.

Separate submit from poll

In production, submission and polling should be different steps, often in different processes. Store the job ID in a queue or database as soon as submission succeeds. If your worker crashes, you can resume polling instead of losing the job.

Set timeouts everywhere

The submit request, the status poll, and the file download should each have their own timeout. A missing timeout on the download step is a classic cause of hung workers.

Keep prompts inside the model's strengths

Request single, physically plausible actions in the six-to-ten second range. When a prompt asks for too much, you get a busy clip with muddy motion. If output quality is inconsistent, shorten the prompt and simplify the action before changing parameters.

Handle credentials properly

Load the API key from the environment or a secret manager, never from a committed file. Use separate keys per environment so a leaked development key cannot touch production.

Watch for the URL expiry assumption

The single most common production bug is treating the returned video URL as permanent and rendering it directly in a user interface months later. Download and rehost the file as part of the job, and store your own URL.

With those habits in place, the integration is small: submit, poll, download, store.

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