Search

Search HiAPI models, tools, and resources.

Back to blog
Tutorial

How to Use the vidu-q4-preview Reference-to-Video API (curl + Python)

hiapividureference-to-videovideo-generationapi-guide

What you'll build

One reference-to-video call on hiapi's async /v1/tasks API that blends several reference stills — and, optionally, a voice clip — into a single 3–16 second take at up to 4K. You submit one POST, poll (or get a callback) until the task finishes, then download the MP4.

Grab a key from the hiapi dashboard first; every request below uses Authorization: Bearer sk-<your-key>.

Reference-to-video vs image-to-video. This endpoint blends 1–15 stills — hand it an actor still, a location still, a prop still, and it composes them into one shot. If you only have a single first frame to animate, use vidu-q4-preview/image-to-video instead; it takes a single image_url and rejects reference_image_urls.

Minimal runnable request (curl)

curl -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer sk-<your-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "vidu-q4-preview/reference-to-video",
    "input": {
      "prompt": "The person from reference image 1 walks through the cafe from reference image 2, warm morning light, slow dolly-in",
      "reference_image_urls": [
        "https://your-cdn.com/person.jpg",
        "https://your-cdn.com/cafe.jpg"
      ],
      "resolution": "1080p",
      "duration": 5,
      "aspect_ratio": "16:9"
    }
  }'

The POST returns a task id, not the video:

{ "code": 200, "data": { "taskId": "task_abc123" } }

Poll GET /v1/tasks/<taskId> until data.status is success, then read the MP4 from data.output[0].url:

curl https://api.hiapi.ai/v1/tasks/task_abc123 \
  -H "Authorization: Bearer sk-<your-key>"
{
  "code": 200,
  "data": {
    "status": "success",
    "output": [
      { "url": "https://temp.hiapi.ai/....mp4", "expireAt": 1760000000 }
    ]
  }
}

The output URL is temporary — it carries an expireAt (a few days out) — so download the bytes right away instead of hotlinking.

Python: submit → poll → download

import time
import requests

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

def submit() -> str:
    payload = {
        "model": "vidu-q4-preview/reference-to-video",
        "input": {
            "prompt": (
                "The person from reference image 1 walks through the cafe "
                "from reference image 2, warm morning light, slow dolly-in"
            ),
            "reference_image_urls": [
                "https://your-cdn.com/person.jpg",
                "https://your-cdn.com/cafe.jpg",
            ],
            "resolution": "1080p",
            "duration": 5,
            "aspect_ratio": "16:9",
        },
    }
    r = requests.post(BASE, json=payload, headers=HEADERS, timeout=60)
    r.raise_for_status()
    body = r.json()
    task_id = (body.get("data") or {}).get("taskId")
    if not task_id:
        raise RuntimeError(f"submit rejected: {body}")  # 400s land here
    return task_id

def wait(task_id: str, timeout_s: int = 600, interval: int = 5) -> str:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        r = requests.get(f"{BASE}/{task_id}", headers=HEADERS, timeout=40)
        task = r.json().get("data") or {}
        status = task.get("status")
        if status == "success":
            return task["output"][0]["url"]
        if status == "fail":
            raise RuntimeError(f"task failed: {task.get('error')}")
        time.sleep(interval)
    raise TimeoutError(task_id)

def download(url: str, path: str) -> None:
    with requests.get(url, stream=True, timeout=120) as r:
        r.raise_for_status()
        with open(path, "wb") as f:
            for chunk in r.iter_content(1 << 16):
                f.write(chunk)

task_id = submit()
url = wait(task_id)
download(url, "out.mp4")

One thing to internalize: a render that fails still comes back as HTTP 200, with the reason under data.status: "fail" and data.error.code / data.error.message. A failed generation is not an HTTP error, so branch on the body, not the status code.

Adding a voice reference

The voice field is reference_audio_urls — an array of up to 3 clips. The audio sets the timbre; your prompt still carries the words and drives lip-sync.

{
  "model": "vidu-q4-preview/reference-to-video",
  "input": {
    "prompt": "The presenter from reference image 1 speaks in the voice of reference audio 1: 'Welcome back to the channel.'",
    "reference_image_urls": ["https://your-cdn.com/presenter.jpg"],
    "reference_audio_urls": ["https://your-cdn.com/voice.mp3"],
    "resolution": "1080p",
    "duration": 8
  }
}

Field-name gotcha: the intuitive voice_reference_urls is rejected (the validator only allows reference_audio_urls). See the probe table below.

Production: callbacks and idempotency

For a server workflow you usually don't want to hold a polling loop open. Attach a top-level callback (sibling of input, not inside it) and hiapi will POST the finished task to your URL:

{
  "model": "vidu-q4-preview/reference-to-video",
  "input": { "prompt": "...", "reference_image_urls": ["..."] },
  "callback": { "url": "https://your-app.com/hooks/vidu", "when": "final" }
}
  • when only supports "final" — you're notified once, on the terminal state (success or fail), never on intermediate progress.
  • The callback body is the same payload you'd get from GET /v1/tasks/<id>.
  • Delivery retries a few times and can duplicate, so dedupe on taskId and make the handler idempotent.
  • Keep polling as a fallback for the window where your endpoint was briefly unreachable.

Save the taskId the moment you get it. If your poller hits a transient network error, re-poll the saved id — never resubmit, or you pay for a second render.

What I verified (schema contract)

I probed the live endpoint with deliberately invalid values (free — rejected requests don't bill, and an invalid value never creates a task). The validator is strict and returns a flat { "code": 400, "error_code": "INVALID_REQUEST", "message": ... }:

What you sendWhat comes back
resolution: "9999p"resolution: value must be one of '540p', '720p', '1080p', '2k', '4k'
duration: 999duration: maximum: got 999, want 16
aspect_ratio: "banana"aspect_ratio: value must be one of '16:9', '9:16', '1:1', '4:3', '3:4'
voice_reference_urls: [...]additional properties 'voice_reference_urls' not allowed
four reference_audio_urlsreference_audio_urls: maxItems: got 4, want 3
omit reference_image_urlsmissing required field "reference_image_urls"

So the confirmed input shape is:

FieldRequiredValues
promptyestext
reference_image_urlsyes1–15 image URLs
reference_audio_urlsnoup to 3 audio URLs (the voice field)
resolutionno540p / 720p / 1080p / 2k / 4k
durationnointeger 3–16 (seconds)
aspect_rationo16:9 / 9:16 / 1:1 / 4:3 / 3:4

duration is an integer (5), not a string — unlike some other video models on hiapi that expect "6". Passing extra keys is a hard error (additional properties ... not allowed), so don't copy fields over from the image-to-video schema.

Output-side notes before you wire up downloads

  • Every clip carries an AAC audio track even when you send no reference_audio_urls. Sound words in the prompt drive an ambient track, so don't assume "no voice reference = silent file."
  • 4K output from Vidu Q4 Preview is delivered as HEVC/H.265. It plays in most native players, but many browsers won't inline an H.265 MP4 inside a <video> tag — re-encode to H.264 if you're embedding on the web. The lower tiers come back as standard H.264.

Preview model — treat the enums as a snapshot

vidu-q4-preview/reference-to-video is a preview model: it's callable today but isn't listed in /v1/models, and a preview schema can shift. Before a large batch, re-run one invalid-value probe (like the table above) to confirm the enums still hold. The authoritative spec is what the validator enforces plus the model page.

FAQ

Is reference-to-video the same as image-to-video? No. Reference-to-video blends 1–15 stills (optionally plus up to 3 voice clips) into one composed shot; image-to-video animates a single first frame. They're separate endpoints with different inputs — reference-to-video even rejects the image-to-video image_url field and vice versa.

How do I add a voice? Attach reference_audio_urls (up to 3 clips) and name it in the prompt ("…in the voice of reference audio 1"). The field is exactly reference_audio_urls; voice_reference_urls and other names are rejected.

What's the longest clip I can request? 16 seconds. duration is an integer from 3 to 16.

Do I have to poll? No — attach callback: { "url": ..., "when": "final" } to be notified once on the terminal state. Dedupe on taskId and keep polling as a fallback.

How much does a 4K clip cost? It's billed per second and the rate rises with resolution, with 4K the top tier — see hiapi pricing. A practical workflow is to iterate at 720p and re-render only the keeper at 4k.

Generate it with HiAPI

Choose a model, enter your prompt, and see the result.

HiAPI Blog

Related articles

HiAPI

Generate it with HiAPI

Start generating