HiAPI
Blog

Tutorial

How to Use the hailuo 2.3 Image to Video API: curl, Python, and a Working Request

Jul 11, 2026
How to Use the hailuo 2.3 Image to Video API: curl, Python, and a Working Request

TL;DR

  • What this is: a working recipe for the hailuo-2.3/image-to-video API on hiapi — turn one still image into a short video clip: create a task, poll it, download the .mp4.
  • Endpoint: POST https://api.hiapi.ai/v1/tasks with "model": "hailuo-2.3/image-to-video", then GET /v1/tasks/<taskId> until it reaches a terminal status.
  • Required input fields (both): prompt and image_url — a single public URL to your first frame (not an array).
  • duration is a string, not a number: "6" or "10". Passing 6 as an integer is rejected with a 400.
  • The schema is strict. There are no motion_strength, camera_movement, or resolution parameters — they're rejected with a 400. Motion is controlled entirely through the prompt.

What you're building, and what you need

You have a still image — a product shot, a character render, a photo — and you want hailuo-2.3 to animate it into a video clip that starts exactly from that frame. hailuo-2.3 is known for physically plausible motion (cloth, hair, body mechanics), which makes it a strong pick for bringing static images to life.

You need two things:

  1. A hiapi API key. Create one from your dashboard. Keys look like sk-... and go in the Authorization: Bearer header.
  2. A publicly reachable URL for your first-frame image. The API fetches the image from image_url, so it must be accessible without authentication — an S3/R2/CDN link works; a localhost path or a signed URL that has expired does not.

Like every generation model on hiapi, this one runs on the async task interface: you POST a task, get a taskId back immediately, and collect the result later by polling or via callback. There is no synchronous endpoint for video — clips take a while to render, longer than typical HTTP timeouts.

The request schema

The full input schema for hailuo-2.3/image-to-video — verified against the live API:

Field Type Required Notes
prompt string ✅ Describes the motion, not the scene — the scene comes from your image.
image_url string ✅ One public URL, the first frame. Singular — not image_urls.
duration string optional "6" or "10" (seconds). Must be a string.
prompt_optimizer boolean optional Lets the platform expand your prompt before generation.

The schema is strict: anything not in this table is rejected. Send motion_strength, camera_movement, or resolution and you get exactly this back:

{
  "code": 400,
  "error_code": "INVALID_REQUEST",
  "message": "invalid input: <root>: additional properties 'resolution', 'camera_movement', 'motion_strength' not allowed"
}

Two consequences worth internalizing:

  • There are no motion knobs. hailuo-2.3's motion physics are steered by the prompt alone — see the prompting section below.
  • There is no resolution or aspect-ratio control on this endpoint. Don't build UI for parameters the API will 400 on.

Minimal working example: curl

Create the task:

curl -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer sk-<your-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "hailuo-2.3/image-to-video",
    "input": {
      "prompt": "The woman turns toward the camera and smiles; wind gently moves her hair; slow cinematic push-in",
      "image_url": "https://your-cdn.example.com/first-frame.jpg",
      "duration": "6",
      "prompt_optimizer": true
    }
  }'

The response (abridged) contains the task id under data.taskId:

{ "data": { "taskId": "..." } }

Poll it:

curl -s https://api.hiapi.ai/v1/tasks/<taskId> \
  -H "Authorization: Bearer sk-<your-key>"

While the clip renders, data.status reports a non-terminal state. When it flips to "success", the video URL is at data.output[0].url; if it's "fail", data.error.code and data.error.message say why.

Download the file as soon as the task succeeds. Output URLs are signed and carry an expiry (expireAt) — store the bytes on your own storage, never hotlink the task URL.

Minimal working example: Python

The same flow as a complete script — create, poll, download:

import time
import requests

API_BASE = "https://api.hiapi.ai/v1/tasks"
API_KEY = "sk-<your-key>"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


def create_task() -> str:
    payload = {
        "model": "hailuo-2.3/image-to-video",
        "input": {
            "prompt": (
                "The cat stretches, hops off the windowsill, and walks toward "
                "the camera; soft afternoon light, subtle handheld camera sway"
            ),
            "image_url": "https://your-cdn.example.com/first-frame.jpg",
            "duration": "6",          # string, not int: "6" or "10"
            "prompt_optimizer": True,
        },
    }
    r = requests.post(API_BASE, json=payload, headers=HEADERS, timeout=30)
    task_id = (r.json().get("data") or {}).get("taskId")
    if not task_id:
        raise RuntimeError(f"create failed ({r.status_code}): {r.text[:300]}")
    return task_id


def wait_task(task_id: str, timeout_s: int = 900, poll_s: int = 10) -> dict:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        r = requests.get(f"{API_BASE}/{task_id}", headers=HEADERS, timeout=30)
        task = r.json().get("data") or {}
        status = task.get("status")
        if status == "success":
            return task
        if status == "fail":
            err = task.get("error") or {}
            raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
        time.sleep(poll_s)
    raise TimeoutError(f"task {task_id} not finished after {timeout_s}s")


def main() -> None:
    task_id = create_task()
    print("task created:", task_id)
    task = wait_task(task_id)
    url = task["output"][0]["url"]
    # The URL is signed and expires -- download immediately, store the bytes yourself.
    video = requests.get(url, timeout=120).content
    with open("hailuo-output.mp4", "wb") as f:
        f.write(video)
    print("saved hailuo-output.mp4")


if __name__ == "__main__":
    main()

Video tasks run noticeably longer than image tasks, so the script polls every 10 seconds with a generous 15-minute ceiling. Don't poll more aggressively than every ~5 seconds — it doesn't make the render faster.

Writing prompts that control motion

Since there are no motion parameters, the prompt is your only steering wheel — and for hailuo-2.3 that's enough. Three rules of thumb:

  • Describe motion, not appearance. The model already sees your image. "a beautiful woman in a red dress" wastes the prompt; "she turns her head slowly to the left as the camera pushes in" uses it.
  • Name the camera work in plain language. "slow push-in", "orbit right", "handheld sway", "static camera" — these belong in the prompt text, not in a (nonexistent) camera_movement field.
  • Let prompt_optimizer help when in doubt. With "prompt_optimizer": true the platform expands terse prompts before generation — good default for short inputs. Turn it off when you want strict adherence to carefully engineered wording.

Production notes

Prefer callbacks over polling. Attach a callback when creating the task and hiapi will POST the terminal result to your endpoint, so you hold no open loops:

{
  "model": "hailuo-2.3/image-to-video",
  "input": { "...": "..." },
  "callback": { "url": "https://your-app.example.com/hooks/hiapi", "when": "final" }
}

"when": "final" fires once, on the terminal state. Polling is fine for scripts and notebooks; callbacks are the right shape for servers — video renders are long enough that a worker blocked on polling is real money.

Handle these errors explicitly:

  • 401 with "code": "permission_denied" — bad key, or the key isn't allowed to use this model. The message includes a request_id you can send to support.
  • 400 INVALID_REQUEST — schema violations. The message is precise, e.g. duration: got number, want string or image_url: missing required field "image_url". Log it verbatim.
  • 402 on task creation — insufficient balance. Note that video bills more per task than images, so a key that still runs image tasks fine can start failing video creation first. Top up, or check your usage in the dashboard.
  • Task reaches "fail" — generation-side failure; inspect data.error. A common cause for image-to-video specifically: image_url wasn't fetchable (private bucket, expired signed URL).

Be idempotent around retries. If your process dies between create and download, a naive restart creates (and pays for) a second render. Persist the taskId as soon as create_task returns, and on restart resume polling instead of re-creating.

Related reading

FAQ

What durations does hailuo-2.3 image-to-video support? Two: "6" and "10" seconds, passed as strings. Any other value — or the same value as a number — returns 400 INVALID_REQUEST.

Why does "duration": 6 fail when "duration": "6" works? The schema types duration as a string enum. The API tells you exactly that: duration: got number, want string. It's the single most common 400 on this endpoint.

Can I set the output resolution or aspect ratio? No. The endpoint exposes no resolution or aspect_ratio field, and sending one is a 400 — the schema rejects unknown properties rather than ignoring them.

How do I control camera movement or motion strength? Through the prompt. There are no camera_movement or motion_strength parameters — write the camera and motion directions as plain language ("slow push-in", "she walks toward the camera").

Can I pass multiple reference images? No — image_url is a single string, the first frame. This differs from some other models on hiapi that take an image_urls array; schemas are per-model, so always check the model page.

What image formats work for the first frame? Standard web formats (JPEG/PNG) served from a publicly reachable URL. If the platform can't fetch the URL, the task fails after creation — so validate the link is publicly accessible before submitting.

How is this different from hailuo-2.3/text-to-video? Text-to-video invents the scene from your prompt alone; image-to-video pins the first frame to your image and animates from it. The task lifecycle is identical — only the input schema differs (image_url is required here, and text-to-video has no image field at all).

What does it cost? Video is billed per second of output, and rates change — check the hailuo-2.3/image-to-video model page and the pricing page for current numbers.

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