MiniMax Lip Sync Video (ia2v)
One portrait image, one audio track and a prompt in — a beat-accurate singing or talking performance out. Test it live with your own API key, or read how it works.
API key
Enter your API key to submit or inspect tasks.
Create a task
Fields map directly onto POST /aigc/task with task_type: "minimax_ia2v".
Or inspect an existing task
Result
Request log
Every call this page makes to the API, in order, most recent first.
Nothing sent yet.
Demo Videos
More lip-sync demos
Pop Female Vocal (5s)
4/3Female Vocal (17s)
9/16Male Pop (8s)
9/16Overview
MiniMax Lip Sync Video (minimax_ia2v) turns one portrait image and one audio track into a performance video: the character's mouth movements and micro-expressions are driven jointly by the image and the audio track, so the lip sync follows what's actually being sung or said. It's a single call — you get a task_id back immediately and the clip is typically ready in a little over a minute.
Basic configuration
| Setting | Value |
|---|---|
| Base URL | https://api.foxaihub.com/api/v2 |
| Auth header | api-key: YOUR_API_KEY |
| task_type | minimax_ia2v |
Warning: The header is api-key, not api_key. Any account with a balance greater than $0 can submit — there is no separate minimum-balance gate like the Music Video pipeline has.
Endpoints at a glance
| Endpoint | Purpose |
|---|---|
| POST /aigc/task | Submit an image + audio to video (lip sync) task |
| POST /aigc/task/wait | Submit and block until the task finishes (or times out) |
| GET /aigc/task?ids= | Read the status and result of one or more tasks |
This is the exact same underlying model that drives the singing shots in our Music Video pipeline, exposed directly as a single call — no multi-stage storyboard, no shot graph, just one image, one audio track and one prompt.
Parameters & constraints
All parameters go in the params object of the create call. Every one of them is validated before you are charged — an invalid value returns 400 with no deduction.
| Field | Type | Required | Constraint |
|---|---|---|---|
| image | string (URL) | yes | Publicly downloadable http(s) URL — base64 is not accepted |
| audio | string (URL or base64) | yes | Public http(s) URL, or a base64 Data URL up to 30MB. Drives the lip movement — see below |
| prompt | string | yes | Describes the performance — expression, energy, camera framing |
| resolution | string | yes | "768p" or "1080p" |
| aspect_ratio | string | yes | One of "1:1", "4:3", "3:4", "16:9", "9:16" |
| duration | integer (seconds) | yes | 3–15 |
Note: The actual pixel size is looked up server-side from resolution + aspect_ratio. Portrait ratios like 9:16 are the most common choice for a talking/singing character — 768p + 9:16 is 768x1344 (1080p is 1088x1920). The other four ratios at 768p are 1:1 = 768x768, 4:3 = 1024x768, 3:4 = 768x1024, and 16:9 = 1344x768; at 1080p they scale to 1:1 = 1088x1088, 4:3 = 1440x1088, 3:4 = 1088x1440, and 16:9 = 1920x1088.
Sending the audio: URL or inline upload
If your track is already hosted somewhere public, pass the URL — nothing gets copied and repeat requests against the same URL are free. If it is only on your machine, send it inline instead: put the whole file in params.audio as a base64 Data URL (up to 30MB decoded) and we store it on our CDN, rewrite params.audio to the resulting URL, and queue the task with that. The playground's Upload tab does exactly this. Note that image has no inline form — it must still be a public URL.
{
"task_type": "minimax_ia2v",
"params": {
"image": "https://example.com/portrait.jpg",
"audio": "data:audio/mpeg;base64,SUQzBAAAAAA...",
"duration": 5
}
}
Note: An inline upload is a convenience for one-off tests, not a hosting product: the stored file is temporary, and every request carries the whole track again. For anything running in production — and especially for several shots off one song — host the audio once and pass its URL.
How the audio drives the lip sync
- We read the first duration seconds out of your track. If you need a different segment for several shots, cut the audio yourself before uploading it.
- Cropping happens on the decoded waveform, so it is sample-accurate — the window is exactly duration seconds long, never a frame more or less.
- The actual clip length is min(real audio length, duration). If your track is shorter than duration, the clip simply ends early.
- Billing is always based on the requested duration, not the actual clip length — a short audio clip does not get a partial refund.
- If the audio URL fails to download, the whole task fails (and is refunded automatically, see Billing).
Warning: If you must cut the audio yourself, never use ffmpeg's stream-copy mode (-c copy). It can only cut on MP3 frame boundaries, so a "5 second" clip typically comes out around 4.977s and shifted by ~30ms — long enough to break lip sync, and the container metadata still reports 5s so it looks fine. Re-encode instead, and keep the length a whole number of seconds.
If you cut it yourself: the safe ffmpeg recipe
Two rules keep a hand-cut clip in sync: re-encode rather than stream-copy, so the cut lands on an exact sample instead of the nearest compressed frame; and make the clip a whole number of seconds, so it fills the duration x 24 + 1 frame window exactly. WAV is the safest output because it has no encoder delay or padding at all. MP3 is fine too as long as you re-encode.
# Recommended — WAV, sample-accurate, no encoder padding
ffmpeg -ss 8 -i song.mp3 -t 5 -ar 48000 -ac 2 -c:a pcm_s16le shot.wav
# Also fine — MP3, as long as you re-encode instead of stream-copying
ffmpeg -ss 8 -i song.mp3 -t 5 -ar 48000 -ac 2 -c:a libmp3lame -q:a 2 shot.mp3
# NEVER do this — -c copy cuts on frame boundaries and drifts out of sync
# ffmpeg -ss 8 -i song.mp3 -t 5 -c copy shot.mp3
Verify by counting decoded samples rather than reading the container duration, which is exactly the field that lies about a stream-copied cut. The count has to equal your sample rate times the clip length — 48000 x 5 = 240000 for the commands above.
ffprobe -v error -select_streams a:0 -count_samples \
-show_entries stream=sample_rate,nb_read_samples -of default=nw=1 shot.wav
# sample_rate=48000
# nb_read_samples=240000
Why duration must be a whole number of seconds
Generation runs at 24 frames per second, and the frame count is computed as duration x 24 + 1, for both the video and the audio window that drives it. A fractional duration would misalign the two, so only integers from 3 to 15 are accepted.
API reference
POST /aigc/task
Submit an image + audio to video (lip sync) task. Charged immediately at submit time.
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
task_type |
string | Yes | Must be "minimax_ia2v" |
params.image |
string (URL) | Yes | Publicly downloadable http(s) URL |
params.audio |
string (URL or base64) | Yes | Public http(s) URL, or a base64 Data URL up to 30MB |
params.prompt |
string | Yes | Performance / expression / framing |
params.resolution |
string | Yes | "768p" or "1080p" |
params.aspect_ratio |
string | Yes | "1:1" / "4:3" / "3:4" / "16:9" / "9:16" |
params.duration |
integer | Yes | 3-15 seconds; also how much of the audio is used |
Request Body
{
"task_type": "minimax_ia2v",
"params": {
"image": "https://example.com/portrait.jpg",
"audio": "https://common.foxai.me/sample_music/jp2.mp3",
"prompt": "she sings to the camera, warm expression, static framing",
"resolution": "768p",
"aspect_ratio": "9:16",
"duration": 5
}
}
Response
{ "success": true, "task_id": "a42e2c77-ded8-48aa-8aa4-dfd9daa43801" }
Errors
| Status | When | Body |
|---|---|---|
400 |
A param fails validation (bad resolution/aspect_ratio, duration out of range, non-URL image, audio that is neither a URL nor decodable base64) | {"success":false,"error":"Invalid params for minimax_ia2v: audio: invalid base64"} |
400 |
task_type is unknown or disabled | {"success":false,"error":"Unsupported task type: minimax_ia2v"} |
401 |
Missing/unknown api-key, or balance is 0 | {"message":"Unauthorized"} |
402 |
The deduction itself failed (insufficient balance) | {"success":false,"error":"Insufficient balance"} |
413 |
The request body is too large — an inline audio upload over 30MB | {"success":false,"error":"Request body exceeds 42008576 bytes"} |
500 |
Pricing configuration for this task_type is invalid | {"success":false,"error":"Invalid price configuration for task type: minimax_ia2v"} |
POST /aigc/task/wait
Same as POST /aigc/task, but blocks the HTTP response until the task reaches a terminal state or wait_timeout elapses. Convenient for scripts that don't want to poll.
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
task_type |
string | Yes | "minimax_ia2v" |
params |
object | Yes | Same shape as above |
wait_timeout |
integer (seconds) | No | 1-600, default 60 |
Request Body
{
"task_type": "minimax_ia2v",
"params": {
"image": "https://example.com/portrait.jpg",
"audio": "https://common.foxai.me/sample_music/jp2.mp3",
"prompt": "she sings to the camera, warm expression, static framing",
"resolution": "768p",
"aspect_ratio": "9:16",
"duration": 5
},
"wait_timeout": 120
}
Response
The full task object (same shape as GET /aigc/task) once it reaches completed or failed.
Errors
| Status | When | Body |
|---|---|---|
408 |
The task is still running when wait_timeout elapses | {"success":false,"task_id":"...","status":"processing","message":"Task wait timeout after 60s"} |
400 / 401 / 402 / 500 |
Same as POST /aigc/task | See above |
GET /aigc/task?ids=
Read one or more tasks by id. Free. Response is a bare array, not an object.
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
ids |
string | Yes | Comma-separated task ids, up to 25 at once |
Response
[ { "task_id": "...", "task_type": "minimax_ia2v", "status": "completed", "params": {...}, "data": ["https://cdn.../result.mp4"], "created_at": "...", "queue_time_sec": 4, "generation_time_sec": 58 } ]
Errors
| Status | When | Body |
|---|---|---|
400 |
ids is missing, or more than 25 ids were requested | {"error":"Task IDs required"} |
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
Webhook
There are two ways to track video task progress: poll GET /aigc/task?ids= (or use POST /aigc/task/wait for short scripts), or configure a webhook. We recommend webhooks in production — your server is pushed whenever a task completes or fails instead of holding connections open or polling constantly.
Note: A webhook is configured in your Dashboard (one URL per account), not per task — configure it once and all AIGC tasks (including minimax_i2v and minimax_ia2v) will be pushed to your endpoint upon completion or failure.
Delivery format
Every delivery is a POST with a standard envelope:
{
"event": "task.completed",
"type": "minimax_ia2v",
"delivery_id": "9f2c1a3e-abcd-4d11-8f00-0123456789ab",
"created_at": "2026-08-26T09:31:02.000Z",
"data": {
"task_id": "a42e2c77-ded8-48aa-8aa4-dfd9daa43801",
"task_type": "minimax_ia2v",
"status": "completed",
"params": {
"image": "https://example.com/character.jpg",
"audio": "https://example.com/vocal.mp3",
"prompt": "singing passionately, expressive micro-expressions",
"resolution": "768p",
"aspect_ratio": "9:16",
"duration": 5
},
"data": ["https://cdn.../result.mp4"],
"created_at": "2026-08-26T09:30:10.000Z",
"queue_time_sec": 4,
"generation_time_sec": 58
}
}
| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | FoxAI-Webhook/1 |
| X-Webhook-Event | Same as event in the body (task.completed or task.failed) |
| X-Webhook-Type | Same as type in the body |
| X-Webhook-Delivery | Same as delivery_id in the body |
| X-Webhook-Timestamp | Unix seconds at delivery time |
Result type
Since one URL receives every product on your account, event tells you what happened and type tells you which kind of result it is. type always matches the task_type you passed when creating the task, so you can dispatch on it directly:
| type | Result |
|---|---|
| minimax_ia2v | Lip-sync (image + audio to video) task reached a terminal state |
| minimax_i2v | Image-to-video task reached a terminal state |
| asr-serverless | Music transcription task reached a terminal state |
| mv | Music video pipeline event |
| ping | Test delivery triggered from the Dashboard |
Events
| Webhook event | Trigger condition | data contents |
|---|---|---|
| task.completed | Lip-sync task successfully finished | Full task object with generated video URL in data array |
| task.failed | Task failed (charge refunded automatically) | Task object with error details |
Retry policy
- 10 second timeout per attempt.
- Retries at 5s, 10s, 20s — up to 4 attempts total, wrapping up within ~35 seconds.
- Only retried on network errors, timeouts, HTTP 408, 429 and 5xx. Non-retryable 4xx codes are not retried.
- The same delivery keeps the same X-Webhook-Delivery (delivery_id) across retries — use it to deduplicate.
Security
Warning: Add a long random path segment or token query parameter to your webhook URL. Always treat an incoming webhook as a notification signal and confirm with GET /aigc/task?ids= when processing critical business logic.
Limits & lifecycle
Task lifecycle
A task only ever has four states, and it only moves forward:
pending -> processing -> completed
-> failed
| Status | Meaning |
|---|---|
| pending | Accepted, waiting for a GPU worker to pick it up |
| processing | A worker is running the generation |
| completed | data holds the result — an array with the video URL as its first element |
| failed | Something went wrong (including a bad audio URL); the charge was refunded automatically |
There is no fixed SLA. Lip-sync generation is somewhat heavier than plain i2v, so expect it to typically take a bit longer than a minute — poll GET /aigc/task?ids= or use POST /aigc/task/wait rather than assuming a fixed time.
Queue time and generation time
The task object carries two timing fields, both in seconds. queue_time_sec is the time from creation (created_at) until the task was first picked up by a worker (entered processing) — how long it waited in the queue. generation_time_sec is the time from that first pickup until it reached completed or failed — how long the actual generation took. Both are null before the corresponding milestone happens: for example, both are null while pending, and generation_time_sec stays null while processing, only getting a value once the task is terminal.
Note: minimax_ia2v actually runs as a two-stage pipeline internally (a draft stage produces a latent, then a refine stage turns it into the final clip, passing through an internal streaming state that is never exposed to you). generation_time_sec spans the whole thing — first pickup to final completed/failed — including that internal handoff, so it isn't split into two numbers or missing a stage just because the pipeline runs in two parts.
Retries and timeouts
A worker that claims a task but stops responding is reclaimed and retried, up to 3 attempts total. If every attempt is exhausted the task is marked failed and the charge is refunded — you don't need to build your own timeout-and-refund logic.
Task visibility
Warning: GET /aigc/task does not check ownership — anyone who has a task_id can read its status and result with any valid api-key. Treat a task_id as semi-sensitive if the prompt, audio, or result is private, and don't expose it in a public URL.
Webhooks instead of polling
See the Webhook section above for the full configuration endpoints, delivery format and retry policy.
Client database schema
If you are storing task states in your own backend (e.g. MySQL / PostgreSQL), here is the recommended table schema:
Tasks table
CREATE TABLE aigc_tasks (
task_id CHAR(36) NOT NULL PRIMARY KEY,
task_type VARCHAR(32) NOT NULL,
status VARCHAR(20) NOT NULL,
params JSON NOT NULL,
data JSON NULL,
created_at DATETIME(3) NULL,
synced_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
CONSTRAINT chk_aigc_tasks_status CHECK (status IN ('pending', 'processing', 'completed', 'failed'))
) ENGINE=InnoDB;
CREATE INDEX idx_aigc_tasks_status ON aigc_tasks (status);
CREATE INDEX idx_aigc_tasks_task_type ON aigc_tasks (task_type);
Webhook deduplication table
Pair this table with INSERT IGNORE or ON DUPLICATE KEY UPDATE so retried webhook deliveries are never processed twice:
CREATE TABLE webhook_deliveries (
delivery_id CHAR(36) NOT NULL PRIMARY KEY,
task_id CHAR(36) NULL,
event VARCHAR(64) NOT NULL,
payload JSON NOT NULL,
received_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB;
CREATE INDEX idx_webhook_deliveries_task_id ON webhook_deliveries (task_id);
Sync strategy
- Insert a row into aigc_tasks as soon as POST /aigc/task returns task_id.
- When a webhook arrives or polling returns updated data, upsert aigc_tasks using task_id as the primary key.
- Extract the resulting video URL from data[0] once status becomes completed.
Billing
minimax_ia2v is billed the same way as minimax_i2v — by resolution tier and per second, at its own rates below — deducted immediately when you submit. There is no separate reservation/settlement step like the Music Video pipeline. If the task fails or times out, the full amount is refunded automatically.
Formula
price = unit_price_by_resolution[resolution] x duration
| resolution | Per-second price | Meaning |
|---|---|---|
| 768p | $0.025 | Per-second price at 768p — also the practical minimum charge (duration's minimum is 3s, i.e. $0.075) |
| 1080p | $0.05 | Per-second price at 1080p, independent of aspect_ratio |
Worked examples
| resolution | aspect_ratio | Duration | Calculation | Price |
|---|---|---|---|---|
| 768p | 9:16 (portrait, typical) | 5s | $0.025 x 5 | $0.125 |
| 768p | 16:9 (landscape) | 3s (shortest) | $0.025 x 3 | $0.075 |
| 1080p | 9:16 (portrait) | 15s (longest) | $0.05 x 15 | $0.75 |
Warning: Duration is what you're billed for, not the actual clip length. A 10-second audio track with duration: 5 costs the same as a 5-second audio track with duration: 5 — you are not refunded for the unused part of a longer audio file, and a shorter audio file is not charged less.
What happens on failure
- The charge happens before the worker starts, at submit time.
- If the task ends in failed — including because the audio URL couldn't be downloaded — the full amount is refunded automatically.
- A worker that stops responding is retried up to 3 times; if every attempt is exhausted the task is marked failed and refunded the same way.