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
- 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.
- 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;localhostpaths 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:
| Field | Type | Required | Notes |
|---|---|---|---|
image_url | string | yes | One portrait URL. A single clear, front-facing face works best. |
audio_url | string | yes | The voice track to lip-sync to. Drives the output length. |
prompt | string | no | Optional guidance for motion/expression. |
resolution | string | no | "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
200withdata.status == "fail"and adata.error. The most common cause is animage_url/audio_urlthe server couldn't download. Check that both are public and still valid. Always branch ondata.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.
Related guides
- Unified Async API introduction — the
/v1/taskscreate-and-poll contract every model shares. - Quick start — your first hiapi request end to end.
- HeyGen Avatar IV API tutorial — another photo-to-talking-video model, if you want to compare lip-sync approaches.
- HeyGen Avatar V — a script-driven (text-to-video) avatar for when you don't have an audio file.
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.



