Search

Search HiAPI models, tools, and resources.

Back to blog
Tutorial

vidu-q3-ad reference-to-video API: curl, Python, and a working request

hiapividureference-to-videovideo-generationapi-guide

What you'll build

One reference-to-video call on hiapi's async /v1/tasks API that turns 1–7 product stills into a short ad clip with native audio. vidu-q3-ad/reference-to-video is the ad-oriented tier of Vidu Q3: you upload product reference images, write a shot description, and the model returns a 3–15 second MP4 with smart cuts, camera moves, and generated sound — there's no separate audio track to supply.

You submit one POST, poll (or take 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-....

The three things you copy verbatim:

  • Submit endpoint: POST https://api.hiapi.ai/v1/tasks
  • Poll endpoint: GET https://api.hiapi.ai/v1/tasks/{taskId}
  • Model id: vidu-q3-ad/reference-to-video — the slash and the -ad suffix both matter; vidu-q3-mix and vidu-q4-preview are different endpoints.

The input schema (live-probed)

fieldrequiredtypenotes
promptyesstringthe shot description; address products by index, e.g. "the watch from reference image 1"
reference_image_urlsyesarray, 1–7public URLs of your product stills (fetched server-side)
durationyesinteger, 3–15seconds; must be an integer (4.5 is rejected)
sizenoenum (10 values)sets both the aspect ratio and the 720/1080 tier; defaults to 1280*720

The validator is strict — any field not in that list is rejected outright. Two rejections are worth calling out because they trip up everyone coming from other hiapi video models:

  • aspect_ratio and resolution are both rejected. Almost every other hiapi video endpoint uses those; q3-ad does not. Dimensions are controlled only through size.
  • reference_audio_urls is rejected too. Unlike the Vidu Q4 reference-to-video tier (which takes a voice clip), q3-ad has no audio input — its soundtrack is generated natively. (How I confirmed this is in the verification section below.)

Picking size

size is a width*height string from a fixed set. Read it as a grid of five aspect ratios × two quality tiers:

aspect720 tier1080 tier
16:9 landscape1280*7201920*1080
9:16 vertical720*12801080*1920
1:1 square1280*12801808*1808
4:31280*9601920*1440
3:4960*12801440*1920

Pick the row for your placement (vertical 720*1280 / 1080*1920 for Reels and TikTok, a landscape row for YouTube pre-roll) and the column for your quality-vs-budget trade-off. Billing is per second of output and the tier sets the per-second rate — see pricing; the minimum charge is a 3-second clip.

Minimal request (curl)

Two steps: submit, then poll.

# 1) Submit — returns a taskId
curl -s -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer sk-your-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "vidu-q3-ad/reference-to-video",
    "input": {
      "prompt": "A product ad of the titanium smartwatch from reference image 1: it rotates slowly on a dark reflective pedestal while a hard strip of light sweeps across the brushed case from left to right, the screen stays dark, the camera holds a low hero angle. Commercial lighting, high contrast, macro detail.",
      "reference_image_urls": [
        "https://your-cdn.com/watch-front.png",
        "https://your-cdn.com/watch-side.png"
      ],
      "duration": 5,
      "size": "1280*720"
    }
  }'
# -> {"data":{"taskId":"..."}}

# 2) Poll until data.status is "success" or "fail"
curl -s https://api.hiapi.ai/v1/tasks/THE_TASK_ID \
  -H "Authorization: Bearer sk-your-key"
# success -> data.status == "success", and data.output[0].url is the MP4

The reference image URLs must be publicly reachable — the task fetches them on the server side, so localhost or signed URLs that expire in seconds won't work.

Python: submit, poll, download

import time
import requests  # or use urllib/subprocess to stay dependency-free

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

def submit():
    payload = {
        "model": "vidu-q3-ad/reference-to-video",
        "input": {
            "prompt": "Ad shot of the sneaker from reference image 1 spinning on a lit turntable, "
                      "slow push-in, soft rim light, product-hero framing.",
            "reference_image_urls": ["https://your-cdn.com/sneaker.png"],
            "duration": 5,
            "size": "1080*1920",   # vertical 1080p for Reels / TikTok
        },
    }
    body = requests.post(API, headers=HEADERS, json=payload, timeout=60).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, timeout_s=600, interval=5):
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        data = requests.get(f"{API}/{task_id}", headers=HEADERS, timeout=30).json().get("data") or {}
        status = data.get("status")
        if status == "success":
            return data["output"][0]["url"]
        if status == "fail":
            raise RuntimeError(f"task failed: {data.get('error')}")
        time.sleep(interval)
    raise TimeoutError(task_id)

url = wait(submit())
mp4 = requests.get(url, timeout=120).content   # download now — output URLs expire
open("ad.mp4", "wb").write(mp4)

A Q3-ad render usually finishes in a minute or two; a 5-second poll interval is plenty. The output URL is temporary (it carries an expireAt), so download the bytes as soon as the task succeeds instead of hotlinking it into a page.

Production patterns

Callback instead of polling. For a server, attach a top-level callback so you don't hold a poll loop open:

{
  "model": "vidu-q3-ad/reference-to-video",
  "input": { "prompt": "...", "reference_image_urls": ["..."], "duration": 5 },
  "callback": { "url": "https://your-app.com/hooks/hiapi", "when": "final" }
}

when only supports "final" — the hook fires once, when the task reaches a terminal state, and its body is the same payload you'd get from GET /v1/tasks/{taskId}. A hook can be retried, so dedupe on taskId, and keep a slow poll as a fallback in case a delivery is dropped.

Branch on the body, not the HTTP status. A failed render still comes back HTTP 200; the failure lives in data.status == "fail" with details in data.error. Only request-level rejections (a bad field, a missing key, no auth) use a 4xx, and those return {"code":400,"error_code":"INVALID_REQUEST","message":"...","data":null}.

Idempotency. Every submit creates a new billable task, so store the taskId the instant you receive it and re-poll that id on retry. Never resubmit because a single poll request timed out.

What I verified

I probed the live endpoint with invalid values, which bounce at validation for free — no task is created and nothing is billed (an invalid value fails before any render starts, which is safer than omitting a field). The real responses are the schema:

  • Empty-ish body → reference_image_urls: missing required field ...; duration: missing required field .... So prompt, reference_image_urls, and duration are all required.
  • duration: 99999 → maximum: got 99,999, want 15; duration: 1 → minimum: got 1, want 3; duration: 4.5 → got number, want integer. Duration is an integer in 3–15.
  • reference_image_urls with 8 entries → maxItems: got 8, want 7. The cap is 7.
  • size: "9000p" → value must be one of '1280*720', '1280*960', '1280*1280', '960*1280', '720*1280', '1920*1080', '1920*1440', '1808*1808', '1440*1920', '1080*1920'.
  • aspect_ratio and resolution → both additional properties ... not allowed. They are not q3-ad fields; use size.
  • reference_audio_urls as a well-formed array → additional properties 'reference_audio_urls' not allowed. (Pass it as a non-array and a separate media check fires first — "must be an array" — which is misleading: the field genuinely isn't in the schema.) There is no voice-reference input on this tier; audio is produced natively.

This is an integration reference, so there are no visual-quality claims — I did not run a paid render for this guide. For what a successful output[0].url looks like, the model page below hosts the endpoint's own example: a real MP4 with an audio track.

FAQ

How do I make the clip portrait vs landscape? Through size, not aspect_ratio. Use 720*1280 or 1080*1920 for vertical, 1280*720 or 1920*1080 for landscape.

Can I supply a voiceover or sound reference? Not on this tier — reference_audio_urls is rejected. Q3-ad generates native audio on its own. If you need to drive audio from a reference clip, use the Vidu Q4 reference-to-video tier.

How many product images can I send? One to seven, in reference_image_urls, and reference them by index in the prompt ("the bottle from reference image 2").

My task returned 200 but there's no video — why? Read data.status. A "fail" returns HTTP 200 with the reason in data.error; only a malformed request gets a 4xx.

What does it cost? Per second of output, and the size tier (720-class vs 1080-class) sets the per-second rate. The minimum is a 3-second clip. See pricing for current rates.

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