Search

Search HiAPI models, tools, and resources.

Back to blog
Tutorial

How to Use omnihuman-1.5 via the hiapi API: curl, Python, and a Working Request

hiapiomnihuman-1.5Video APITalking AvatarTask API

omnihuman-1.5 turns a single portrait plus an audio clip into a lip-synced talking video. On hiapi it runs on the same async task protocol as every other media model: one POST /v1/tasks to create the job, then poll (or get a webhook) until the video URL is ready. This guide uses the real request schema — verified field-by-field against the live API — so the curl and Python below are copy-paste runnable, not illustrative.

What you'll build

A script that sends a face image and a voice track to omnihuman-1.5 and gets back an MP4 of that face speaking the audio, lip-synced. The clip length follows your audio — there is no duration field to set.

Prerequisites

  1. A hiapi API key. Create one in the dashboard; it is sent as a Bearer token on every request. See the authentication docs for the header format.
  2. A publicly reachable image URL and audio URL. omnihuman-1.5 downloads both server-side, so http(s) URLs that your machine can open anonymously work; localhost paths and signed URLs that expire mid-job do not.

That's it — no SDK, no account beyond the key.

The input schema

omnihuman-1.5 takes a strict input object. Unknown fields are rejected, so this is the whole surface:

FieldTypeRequiredNotes
image_urlstringyesOne portrait URL. A single clear, front-facing face works best.
audio_urlstringyesThe voice track to lip-sync to. Drives the output length.
promptstringnoOptional guidance for motion/expression.
resolutionstringno"720p" or "1080p".

There is no text-only mode: audio_url is mandatory. If you only have a script, generate speech first (a TTS model, or a text-driven avatar like heygen-avatar-v), then pass the resulting audio URL here.

Minimal working example (curl)

Create the task:

curl -s -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer sk-YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "omnihuman-1.5",
    "input": {
      "image_url": "https://example.com/portrait.jpg",
      "audio_url": "https://example.com/voiceover.mp3",
      "resolution": "720p"
    }
  }'

The response carries the task id:

{ "code": 200, "data": { "taskId": "b3f1c2e8-...", "status": "pending" } }

Poll the task until it reaches a terminal state:

curl -s https://api.hiapi.ai/v1/tasks/b3f1c2e8-... \
  -H "Authorization: Bearer sk-YOUR_KEY"

When it finishes, data.status is "success" and the video sits in data.output[0].url:

{
  "code": 200,
  "data": {
    "taskId": "b3f1c2e8-...",
    "status": "success",
    "output": [
      { "url": "https://temp.hiapi.ai/.../omnihuman.mp4", "expireAt": 1760000000 }
    ]
  }
}

That output URL is temporary (it carries an expireAt). Download the MP4 as soon as the task succeeds — don't hotlink it in production.

The same call in Python

import time
import requests  # pip install requests

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


def create_task() -> str:
    payload = {
        "model": "omnihuman-1.5",
        "input": {
            "image_url": "https://example.com/portrait.jpg",
            "audio_url": "https://example.com/voiceover.mp3",
            "resolution": "720p",
        },
    }
    r = requests.post(API, headers=HEADERS, json=payload, timeout=60)
    r.raise_for_status()
    return r.json()["data"]["taskId"]


def wait_for_video(task_id: str, timeout: int = 600, interval: int = 5) -> str:
    deadline = time.time() + timeout
    while time.time() < deadline:
        r = requests.get(f"{API}/{task_id}", headers=HEADERS, timeout=30)
        data = r.json()["data"]
        status = data["status"]
        if status == "success":
            return data["output"][0]["url"]
        if status == "fail":
            raise RuntimeError(f"omnihuman-1.5 task failed: {data.get('error')}")
        time.sleep(interval)
    raise TimeoutError(f"task {task_id} did not finish in {timeout}s")


if __name__ == "__main__":
    tid = create_task()
    print("task:", tid)
    video_url = wait_for_video(tid)
    # URLs expire — persist the bytes immediately.
    mp4 = requests.get(video_url, timeout=120).content
    with open("talking_head.mp4", "wb") as f:
        f.write(mp4)
    print("saved talking_head.mp4")

Going to production

Prefer a callback over tight polling. Add a top-level callback object and hiapi will POST the finished task to your endpoint instead of making you poll:

{
  "model": "omnihuman-1.5",
  "input": {
    "image_url": "https://example.com/portrait.jpg",
    "audio_url": "https://example.com/voiceover.mp3",
    "resolution": "1080p"
  },
  "callback": { "url": "https://your.app/webhooks/hiapi", "when": "final" }
}

when only accepts "final", and it fires on both success and failure — so your webhook must branch on data.status. The delivered body is the same object you'd get from GET /v1/tasks/{id}. Delivery is at-least-once, so dedupe on taskId.

Never resubmit on a transient error. If a poll request times out or drops the connection, re-poll the same taskId — do not call POST /v1/tasks again. A resubmit starts a second billed job. Save the taskId the moment you get it.

Handle the three failure shapes separately:

  • Auth failure → HTTP 401, body {"error": {"code": "permission_denied", "type": "hiapi_error", ...}}. Fix the key; don't retry.
  • Bad request (missing/invalid field) → HTTP 400, body {"code": 400, "error_code": "INVALID_REQUEST", "message": "..."}. The message names the offending field — fix and resend.
  • A task that started but failed → HTTP 200 with data.status == "fail" and a data.error. The most common cause is an image_url/audio_url the server couldn't download. Check that both are public and still valid. Always branch on data.status, not just the HTTP code.

Prices are per second of output and are listed on the pricing page; because the clip length tracks your audio, cost scales with audio duration.

FAQ

Can I drive omnihuman-1.5 with text instead of an audio file? No. audio_url is required and the model lip-syncs to that audio. For a script-to-video flow, synthesize speech first (any TTS) or use a text-driven avatar like heygen-avatar-v, then feed the audio URL here.

How long is the output video? It follows your audio clip — there is no duration parameter. A 12-second voice track yields roughly a 12-second video.

What image works best? A single, clearly lit, front-facing portrait at a public URL. Group shots or heavily occluded faces give weaker lip-sync.

720p or 1080p? Both are valid for resolution. Start at 720p while iterating; switch to 1080p for final renders. Higher resolution costs more per second — see pricing.

My task returns status: "fail" immediately. Why? Almost always an input URL the server couldn't fetch. Confirm image_url and audio_url are publicly reachable (open them in an incognito window) and haven't expired. The data.error message narrows it down.

Do I need to poll, or can I get a webhook? Either. Poll GET /v1/tasks/{id} for simple scripts; register a callback.url with when: "final" for production so you aren't holding open a poll loop.

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