HiAPI
Blog

Tutorial

How to Use omnihuman-1.5 via the HiAPI API: curl and Python Tutorial

A working request walkthrough for audio-to-video talking-avatar generation, async polling, and output download

Nov 3, 2026 10 min read
深棕与燕麦色宽带交织的扁平编织纹理封面图,点缀克制黄铜色调,几何秩序感强

What the omnihuman-1.5 API Does and When to Use It

omnihuman-1.5 turns a piece of audio plus a portrait image into a talking-avatar video: the face in the image is animated so that the mouth and head movement follow the supplied speech. On HiAPI the model is exposed as an audio-to-video endpoint — you send audio, a reference image, and a small set of generation parameters, and the API returns a job you poll for the finished video.

Where an API beats a manual tool

A browser tool is fine for one-off clips. The API is the right choice when video generation is a step inside a larger system:

  • Batch pipelines — generating many avatar clips from a queue of scripts and voice tracks, without a human in the loop.
  • Product features — in-app narration, localized onboarding videos, or personalized messages where each render has different audio and a different portrait.
  • Backend automation — content workflows that produce a video, then hand it to a CMS, a storage bucket, or a delivery service.

What the request is shaped like

Every omnihuman-1.5 call on HiAPI follows the same shape: one POST that submits the job, and subsequent requests that check its status. Because rendering is not instantaneous, the integration pattern is submit → poll → download, not submit → receive video in the same response. The rest of this tutorial builds that flow in curl first, then in Python.

Prerequisites and Your HiAPI Credentials

Before the first call, confirm three things.

1. An account and an API key

You need a HiAPI account and an API key created in the dashboard. Treat the key as a secret: keep it out of source control, out of client-side code, and out of logs. Load it from an environment variable in development and from your platform's secret manager in production.

export HIAPI_API_KEY="your-api-key-here"

2. Network access and a tool to make requests

For curl, any recent shell works. For the Python version you only need the requests library — no vendor-specific SDK is required, since the API is plain HTTPS with JSON bodies.

pip install requests

3. Inputs the model expects

Have these ready before you write the request:

  • Audio — the speech track that drives the mouth animation, supplied as a URL the API can read or as an uploaded file reference, depending on the endpoint's accepted input format.
  • A portrait image — the face to animate. A clear, front-facing shot with the subject's mouth visible produces the most predictable results.
  • Generation parameters — any resolution, duration, or quality options the model exposes. Send only the fields you actually need; the defaults are usually sensible for a first test.

The key is passed on every request as an Authorization header in the form Bearer <your-key>. If you get a 401 on your very first call, this header is the first thing to check.

Making the Request with curl

The submission call is a POST with a JSON body. The exact path and field names for omnihuman-1.5 are listed on the model's API reference page; the structure below shows the shape of the call and the fields you will be filling in.

curl -X POST "https://api.hiapi.ai/v1/omnihuman-1.5/generate" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "audio_url": "https://your-bucket.example.com/speech.mp3",
    "image_url": "https://your-bucket.example.com/portrait.jpg"
  }'

Field by field

  • Authorization — carries the API key. Without it the request is rejected before any work starts.
  • Content-Type: application/json — the body is JSON; omitting this header is a common cause of confusing validation errors.
  • audio_url — the speech track. Use a URL that is publicly readable or otherwise accessible to the API, and prefer a stable location rather than a signed link that expires mid-render.
  • image_url — the portrait to animate. Same accessibility rules apply.

What comes back

A successful submission does not return the video. It returns a job object containing an identifier — commonly an id field — and a status such as queued or processing. Store that identifier; it is the handle you use for every subsequent status check.

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

You can confirm the submission worked by checking the HTTP status code and the presence of the job identifier. If the response contains validation messages instead, they will point at the offending field — usually a missing or malformed URL.

The Same Request in Python

The curl call translates directly into Python. The version below is written to be dropped into a service: the key comes from the environment, the payload is a dictionary you can build dynamically, and errors are surfaced rather than swallowed.

import os
import requests

API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = "https://api.hiapi.ai/v1"

HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

def submit_omnihuman_job(audio_url: str, image_url: str, **options) -> str:
    payload = {
        "audio_url": audio_url,
        "image_url": image_url,
    }
    payload.update(options)

    response = requests.post(
        f"{BASE_URL}/omnihuman-1.5/generate",
        headers=HEADERS,
        json=payload,
        timeout=60,
    )
    response.raise_for_status()

    data = response.json()
    return data["id"]

Why this shape

  • json=payload lets requests serialize the body and set the content type, so you cannot forget the JSON header.
  • **options keeps the function flexible: as you add generation parameters you pass them as keyword arguments instead of editing the payload literal.
  • timeout=60 prevents a hung connection from blocking a worker indefinitely. Submission is a fast call; long waits belong in the polling step, not here.
  • raise_for_status() converts non-2xx responses into exceptions you can catch and log, with the response body available for diagnostics.

Keep the returned job identifier and pass it to your polling helper — the next section builds that helper.

Handling the Async Lifecycle: Polling for Completion

Video generation takes time, so the API is asynchronous by design. After submission, you repeatedly ask for the job's status until it reaches a terminal state.

The polling loop

import time

def wait_for_job(job_id: str, interval: int = 5, max_wait: int = 600) -> dict:
    waited = 0
    while waited < max_wait:
        response = requests.get(
            f"{BASE_URL}/omnihuman-1.5/jobs/{job_id}",
            headers=HEADERS,
            timeout=30,
        )
        response.raise_for_status()
        job = response.json()
        status = job["status"]

        if status == "succeeded" or status == "completed":
            return job
        if status == "failed" or status == "error":
            raise RuntimeError(f"Job {job_id} failed: {job}")

        time.sleep(interval)
        waited += interval

    raise TimeoutError(f"Job {job_id} did not finish within {max_wait}s")

Deciding when to stop

Three outcomes end the loop, and your code must handle each:

  • Success — the job reports a completed status and includes a reference to the output video. Exit and move on to download.
  • Failure — the job reports an error state. Do not retry blindly; inspect the error message first, since failures are often caused by unreadable input URLs or unsupported audio.
  • Timeout — your own wall-clock limit is hit. Treat this as a distinct case from failure: the job may still complete server-side, so decide deliberately whether to keep polling, extend the window, or give up.

Polling etiquette

Use a modest interval rather than a tight loop. A few seconds between checks is fine for video rendering and keeps your request volume predictable. Back off progressively if you expect long queues, and always set an upper bound so a stuck worker cannot poll forever. In production, prefer a background worker or job queue over polling inside a web request handler.

Downloading and Storing the Output Video

Once polling returns a completed job, the response carries a reference to the video — typically a URL field inside the result object. Fetch it and persist it to your own storage rather than relying on the link long-term.

def download_video(job: dict, destination: str) -> None:
    video_url = job.get("output_url") or job.get("result", {}).get("video_url")
    if not video_url:
        raise ValueError(f"No video reference in job payload: {job}")

    with requests.get(video_url, stream=True, timeout=120) as response:
        response.raise_for_status()
        with open(destination, "wb") as file:
            for chunk in response.iter_content(chunk_size=8192):
                file.write(chunk)

Notes that save debugging time

  • Stream the download. Videos are large; writing in chunks avoids loading the whole file into memory, which matters in workers with tight memory limits.
  • Use a deterministic filename. Derive it from your job identifier plus a timestamp so runs are traceable and reruns do not collide.
  • Download to temp, then move. Write to a temporary path and rename when complete, so a partial download is never mistaken for a finished file.
  • Treat the returned URL as transient. Copy the bytes into your own bucket or filesystem and record the job identifier alongside the stored object for later lookup.
  • Verify before declaring success. Check that the downloaded file is non-empty and has a plausible size before you mark the job done in your database.

Troubleshooting Common Request and Polling Failures

Most integration problems fall into a handful of categories. Work through this list in order.

Authentication and request errors

  • 401 Unauthorized — the key is missing, malformed, or not sent as a Bearer token. Check the header exactly, including the space after Bearer.
  • 403 — the key is valid but not permitted for this endpoint; confirm the key belongs to the project you expect.
  • 422 / 400 validation errors — read the message. The usual culprits are a missing audio_url, a missing image_url, an unreadable URL, or an unsupported audio format.
  • Unexpected HTML in the response — you are hitting the wrong host or path. Verify the base URL from the API reference rather than reconstructing it from memory.

Jobs that fail or never finish

  • Input URLs not reachable by the API — a link that works in your browser may be blocked by a private network, an expiring signature, or a hotlink guard. Test with a stable public URL first.
  • Audio and image mismatched — an audio track that is silent, extremely long, or in an unsupported format can stall or fail rendering. Start with a short, standard-format clip to validate the pipeline.
  • Polling too aggressively — a tight loop can trip rate limits and produce errors that look like job failures. Slow down and add backoff.
  • Polling too briefly — giving up after a few seconds on a queued job produces false timeouts. Set a realistic maximum wait and log the status you saw before stopping.
  • No terminal status handling — if your loop only checks for success, failures spin until timeout. Branch on the failure state explicitly.

A minimal verification habit

When something breaks, reduce to the smallest test: one short audio clip, one small portrait, a single submission, and a status check a few seconds later. If that works, the failure lives in your payload, your polling logic, or your input hosting — not in the API call itself.

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