Tutorial
How to Call veo-3.1-fast/image-to-video via the HiAPI API (curl + Python)
A working request walkthrough: first-frame input, native audio, async polling, and downloading the output
What this tutorial builds and what you need first
This tutorial walks through a complete, working integration with veo-3.1-fast/image-to-video on HiAPI: you send a first-frame image plus a prompt, receive an asynchronous job, poll it until it finishes, then download the resulting video with its native audio.
By the end you'll have two equivalent implementations — a curl command you can run from a terminal and a Python client you can drop into a script or service — both hitting the same HiAPI endpoint.
Prerequisites
Before you start, gather the following:
- A HiAPI API key. Keep it out of source control. In the examples below it's read from an environment variable rather than pasted inline.
- The HiAPI base URL. Use the one shown in your HiAPI dashboard or API reference; store it as a variable so you only configure it once.
- A first-frame image. A publicly reachable URL is the simplest input. Use a real image you have rights to, in a common format such as JPEG or PNG.
- An HTTP client.
curlis enough for the walkthrough. For the Python version you only needrequests— no vendor SDK required. - A local directory to store the downloaded video.
Set up your environment
Export your key and base URL so both the shell and Python examples can reuse them:
export HIAPI_API_KEY="your-api-key"
export HIAPI_BASE_URL="https://api.hiapi.ai"
export FIRST_FRAME_URL="https://example.com/your-first-frame.jpg"
The exact endpoint path and parameter names for veo-3.1-fast/image-to-video are documented in the HiAPI API reference next to the model entry — check there for the canonical field list rather than relying on copied snippets from elsewhere. This tutorial focuses on the request shape and the workflow, which stay the same even as field details are confirmed against the docs.
One thing worth internalizing now: this is a job-based model, not a synchronous one. You don't get a video back in the submission response. You get a job identifier, and the video arrives later.
The shape of an image-to-video request
Before writing any code, it helps to understand what you're actually sending and what comes back. Image-to-video generation is heavier than text generation, so the API models it as a background job.
Inputs: a prompt and a first frame
A veo-3.1-fast/image-to-video request has two conceptual halves:
- A text prompt describing motion, camera behavior, and scene intent — what should happen after the first frame.
- A reference image that anchors the first frame of the output. This is what distinguishes image-to-video from text-to-video: the model continues from your image rather than inventing an opening frame.
The model name itself tells you the trade-off you're making: the fast variant is tuned for quicker turnaround on image-to-video jobs, which makes it a good fit for iteration and for pipelines where latency matters more than maximum fidelity.
The generated output includes native audio, so the finished file is a complete video with sound rather than a silent clip you'd need to score separately.
The job lifecycle
Because generation takes time, the interaction has three phases:
- Submit. You
POSTthe prompt and image to the model endpoint. The response is a lightweight acknowledgement containing a job identifier — not the video. - Poll. You repeatedly
GETthe job's status using that identifier until it reports completion (or failure). - Fetch. Once complete, the job response points you at the generated video, which you download and store.
Keeping these phases distinct in your code is the single most important structural decision. Mix them together and retries, timeouts, and error handling all become murky.
What a well-formed submission looks like
Regardless of the exact serialization, plan for a request that includes:
- The model identifier,
veo-3.1-fast/image-to-video. - Your prompt text.
- The first-frame image reference.
- Authentication via your API key in a request header.
Everything else — optional parameters, defaults, output resolution options — belongs in the HiAPI API reference for the model. Read it once, put the confirmed field names in a constant, and don't scatter them through your codebase.
Treat the submission response as a receipt. Log it, store the job ID, and move on to polling.
Making the call with curl
Let's submit a job. The command below sends a JSON body with the prompt and first-frame image to the model endpoint on HiAPI, authenticating with your key from the environment.
curl -sS -X POST "$HIAPI_BASE_URL/v1/videos/generations" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "veo-3.1-fast/image-to-video",
"prompt": "Slow dolly forward through a sunlit forest path, leaves drifting, soft birdsong ambience",
"image": "'"$FIRST_FRAME_URL"'"
}'
A few notes on this command:
- The endpoint path and the
imagefield name should be checked against the HiAPI API reference forveo-3.1-fast/image-to-video; substitute the canonical values if they differ. The structure — model, prompt, image — is what matters here. - The prompt is doing real work. Describe motion and camera movement explicitly ("slow dolly forward", "leaves drifting"), and mention the audio atmosphere you want, since the model produces native audio alongside the picture.
-sSkeeps output quiet but still surfaces errors, which is what you want in a terminal walkthrough.- The image is passed as a URL string. If you prefer uploading bytes, check the reference for the supported input form rather than guessing.
Reading the submission response
A successful submission returns a compact JSON object with a job identifier and a status — something in the shape of:
{
"id": "job_abc123",
"status": "queued"
}
The field names are illustrative; confirm them against the live response. What matters is that you now have an ID to poll with, and a status that is not a finished video.
Capture that ID in your shell so the next steps are easy:
JOB_ID="job_abc123"
If you get an authentication error, your key is missing or malformed. A validation error usually means the request body doesn't match the documented schema — check field names and types before anything else. A rate-limit response means you're sending too aggressively; back off rather than retrying in a tight loop.
The same request in Python
Here's the equivalent submission in Python. It uses only requests, reads configuration from the environment, and keeps the submit step separate from polling so you can reuse each piece.
import os
import time
import requests
API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = os.environ["HIAPI_BASE_URL"].rstrip("/")
FIRST_FRAME_URL = os.environ["FIRST_FRAME_URL"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
MODEL = "veo-3.1-fast/image-to-video"
def submit_job(prompt: str, image_url: str) -> str:
payload = {
"model": MODEL,
"prompt": prompt,
"image": image_url,
}
resp = requests.post(
f"{BASE_URL}/v1/videos/generations",
headers=HEADERS,
json=payload,
timeout=60,
)
resp.raise_for_status()
data = resp.json()
job_id = data["id"]
print(f"submitted job: {job_id} (status={data.get('status')})")
return job_id
if __name__ == "__main__":
job_id = submit_job(
prompt="Slow dolly forward through a sunlit forest path, "
"leaves drifting, soft birdsong ambience",
image_url=FIRST_FRAME_URL,
)
Why this structure
- Configuration lives in constants. Base URL, headers, endpoint path, and model name are defined once. When the API reference tells you a field name differs, you change one line.
timeoutis explicit. A submission that hangs forever is worse than one that fails fast. Sixty seconds is generous for a small JSON body.raise_for_status()surfaces failures early. You get an exception on 4xx and 5xx instead of silently parsing an error body as if it were a job.- The function returns only the job ID. Callers don't need to know what the submission payload looked like; they just need something to poll with.
If your application is a web service, call submit_job in a request handler and hand the returned ID to a background worker. Don't block an HTTP request while a video renders.
Polling an async job until the video is ready
Now the job exists and is queued. Polling is how you find out when it's done.
A basic polling loop
def poll_job(job_id: str, timeout_s: int = 900, initial_delay: float = 5.0) -> dict:
url = f"{BASE_URL}/v1/videos/generations/{job_id}"
delay = initial_delay
deadline = time.time() + timeout_s
while time.time() < deadline:
resp = requests.get(url, headers=HEADERS, timeout=30)
resp.raise_for_status()
data = resp.json()
status = data.get("status")
print(f"status={status}")
if status == "succeeded":
return data
if status in ("failed", "cancelled"):
raise RuntimeError(f"job {job_id} ended with status={status}: {data}")
time.sleep(delay)
delay = min(delay * 1.5, 30.0) # back off, cap at 30s
raise TimeoutError(f"job {job_id} did not finish within {timeout_s}s")
Why each part exists
- Polling, not webhooks. The simplest reliable integration is to ask for status on a schedule. Webhooks are nicer at scale but require a public endpoint; polling works everywhere.
- Exponential backoff with a cap. Early polls are cheap and frequent; later polls space out. The cap prevents a runaway interval on a long job. Start around five seconds and grow from there.
- Explicit terminal states. Success is one state; failure and cancellation are others. Treat anything else as "keep waiting," but never wait indefinitely.
- A wall-clock deadline. Video generation is slow compared to text. Pick a generous overall timeout that matches your product's patience, and fail cleanly when it's exceeded rather than looping forever.
- Status logging. Print or log every status transition. When something goes wrong, this is the first thing you'll want.
Polling from the shell
For a quick manual check while debugging, one curl call is enough:
curl -sS "$HIAPI_BASE_URL/v1/videos/generations/$JOB_ID" \
-H "Authorization: Bearer $HIAPI_API_KEY"
Re-run it every few seconds, or wrap it in a small while loop, until the status flips to a terminal state. Confirm the exact field and path names against the HiAPI API reference — the lifecycle is stable, but the wire details are the source of truth.
Once poll_job returns, the response should contain a reference to the finished video. That's what you download next.
Downloading and verifying the output
The job is complete and the response points at the generated file. Downloading it is a plain HTTP GET, but do two things correctly: stream it to disk and verify what you got.
Fetch the video
import os
from urllib.parse import urlparse
def download_video(job: dict, out_dir: str = "outputs") -> str:
video_url = job["output"]["url"] # confirm the field path in the API reference
os.makedirs(out_dir, exist_ok=True)
parsed = urlparse(video_url)
ext = os.path.splitext(parsed.path)[1] or ".mp4"
dest = os.path.join(out_dir, f"{job['id']}{ext}")
with requests.get(video_url, stream=True, timeout=120) as r:
r.raise_for_status()
with open(dest, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 16):
f.write(chunk)
size = os.path.getsize(dest)
if size == 0:
raise IOError(f"downloaded file is empty: {dest}")
print(f"saved {dest} ({size} bytes)")
return dest
Notes:
- Stream in chunks. Video files are large; loading one into memory with
r.contentis wasteful and can fail on constrained hosts. - Preserve the extension from the URL so players and downstream tools recognize the container.
- Name by job ID. It's unique, traceable, and avoids collisions when you run batches.
- Reject empty files. A zero-byte download is a failed download, even if the status code said 200.
If the video URL is signed or time-limited, download promptly after the job completes rather than storing the URL for later.
Verify it's a real video with audio
A file that exists is not the same as a file that works. Check it:
- Play it locally with a media player and confirm both picture and sound.
veo-3.1-fast/image-to-videoproduces native audio, so a silent result is a red flag worth investigating before it reaches users. - Inspect the container with
ffprobeif you have ffmpeg installed:
ffprobe -hide_banner outputs/job_abc123.mp4
Look for a video stream and an audio stream in the output, plus a plausible duration. This is also the fastest way to catch a truncated file.
- Sanity-check the first frame. It should resemble your input image. If it doesn't, suspect the image URL or the field you used to pass it before suspecting the model.
Once a download verifies, persist the file to durable storage and record the job ID alongside it. That mapping is what makes later debugging, re-runs, and billing reconciliation possible.
Pitfalls and checks before you scale
Most integration problems with async video APIs are not model problems. They're plumbing. Here's what to check first.
Image input
- Is the image actually reachable? A URL that works in your browser may require auth, block hotlinking, or expire. Prefer stable, publicly readable URLs.
- Is it a supported format? Stick to common formats like JPEG and PNG unless the API reference says otherwise.
- Is it the right content? The first frame anchors the whole clip. A low-quality or off-topic image produces a disappointing video no amount of prompt tuning will fix.
Request shape
- Field names and types. Validate against the HiAPI API reference for
veo-3.1-fast/image-to-video. Copied snippets from other platforms will fail — the endpoint is HiAPI's, and field names are case-sensitive. - Content type. Send
Content-Type: application/jsonand an actual JSON body, not form data. - Model identifier. Use the full
veo-3.1-fast/image-to-videostring; a truncated name returns an error that looks unrelated.
Timeouts and retries
- Separate timeouts per phase. Submission, polling requests, and file downloads each deserve their own. A single global timeout is either too tight for downloads or too loose for status checks.
- Retry safely. Submissions can be retried on transient network errors and 5xx responses, but only if you can tell whether the job was actually created — otherwise you may double-bill and double-generate. Polling and downloads are idempotent and safe to retry freely.
- Back off on rate limits. Honor the response and slow down rather than hammering the endpoint.
Polling behavior
- Never poll in a tight loop. It wastes quota and looks like abuse.
- Always have a terminal condition. A deadline that raises is better than a job stuck in memory.
- Log every status transition with timestamps. You'll need it to reason about latency.
Operational hygiene
- Keep API keys server-side. Never ship them to a browser or embed them in a mobile app.
- Store the job ID with every artifact. It's your join key across logs, files, and usage records.
- Validate downloads before publishing. Check size and, ideally, streams. An empty or silent file that reaches users is far more expensive to fix than a failed download you retried.
- Don't assume field paths. The
outputlocation in the completed job response is exactly the kind of detail that varies — read it from the API reference, not from a tutorial, including this one.
Work through this list in order before opening a ticket against the model. Most of the time, the fix is a URL, a field name, or a missing backoff.
That's the full loop: submit with a first-frame image and a prompt, poll the job to completion, then download and verify the video with its native audio. Start from the veo-3.1-fast/image-to-video model page on HiAPI to confirm the current request fields, then lift the two functions above into your own service.
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