Music Video Generator
One audio track, one image and a prompt in — a finished, beat-synced music video out. Test the full pipeline live with your own API key, or read how it works.
API key
Enter your API key to create or inspect tasks.
Create a task
Fields map directly onto POST /mv/task.
Task creation is free (account balance must be above $5.00). The video fee is charged by actual audio duration when the storyboard delivers the asset list; each first frame is charged when it is generated.
Or inspect an existing task
Enter your API key to create or inspect tasks.
How to use
For convenience in development and testing, the online tools on this page are designed for a specific music video task: create a new one using the form on the left, or paste an mv_id into “Load an existing task”, and it will appear here.
- Pipeline status — After submitting a task, audio and image information are preprocessed first, followed by the director system generating storyboard details, then first-frame images, then each video clip, and finally editing and stitching them into a complete MV video.
- Advance progress — Automatically pauses at images_done / videos_done states to allow adjusting any unsatisfactory first frames or video clips, and can be advanced to the next stage via the API. After completion you can still swap assets and re-cut.
- Regenerate — Re-run the first frame or video for a specific shot with a new prompt, billed immediately on its own. In addition to regenerating, you can also generate assets yourself and fill them in; everything can be updated freely.
- First frames or image-to-video clips can be generated and filled in yourself by selecting parameters at task submission, while audio-driven lip-sync videos are generated by this API system and can then be replaced with your own clip via Fill in.
Request log
Every call this page makes to the MV API, in order, most recent first.
Nothing sent yet.
Overview
This series of APIs can turn a song into a complete music video. You submit an audio track, a main image, and a text prompt; the platform analyzes the audio track, writes a storyboard script, generates a first frame for each shot, animates each frame into a video clip, and finally edits all clips onto the beat into a finished video.
This is a long-running pipeline — a full song may take ten-plus minutes or even longer — so there are no synchronous endpoints here. POST /mv/task returns an id immediately; in production environments, configuring a Webhook endpoint is recommended to actively advance progress and avoid polling. The event stream (Events) is more suitable for development and debugging, or you can directly poll GET /mv/task/{mv_id}.
Basic configuration
| Setting | Value |
|---|---|
| Base URL | https://api.foxaihub.com/api/v2 |
| Auth header | api-key: YOUR_API_KEY |
Warning: The header is api-key, not api_key. Creating an MV also requires an account balance strictly greater than $5.00 — see Billing.
Endpoints at a glance
| Endpoint | Purpose |
|---|---|
| POST /mv/task | Create an MV task |
| GET /mv/task/{mv_id} | Read one task with all its assets |
| GET /mv/task | List your tasks |
| POST /mv/task/{mv_id}/assets | Fill in assets you generated yourself |
| POST /mv/task/{mv_id}/assets/{asset_id}/regenerate | Regenerate one asset |
| POST /mv/task/{mv_id}/advance | Advance to the next stage |
| POST /mv/events/{mv_id} | Append an event |
| GET /mv/events/{mv_id} | Read the event stream |
Production pipeline
status is always a string in API responses. There are twelve values, always moving in one direction except for the manual gates and the possibility of failure from any state. storyboard_done is numbered 11 but sits between creating and images_generation in the pipeline.
| # | status | What is happening | What you can do |
|---|---|---|---|
| 0 | pending | Task accepted, waiting for the preprocess worker | Wait |
| 1 | processing | Audio is being fetched and analysed (beats, structure, lyrics) | Wait |
| 2 | storyboard | Waiting for the storyboard agent to pick the task up | Wait |
| 3 | creating | The agent is writing the shot plan and shot prompts | Wait |
| 11 | storyboard_done | The shot plan is delivered; first-frame generation is paused here. Only reached when the task was created with pause_after_storyboard: true | Review the asset list, then POST /mv/task/{mv_id}/advance to start first frames |
| 4 | images_generation | First frames are being generated, one per shot; the task waits here if any frame failed | Regenerate / fill in the failed frames |
| 5 | images_done | Every first frame was generated successfully; video generation is paused here | Review, then POST /mv/task/{mv_id}/advance, or fill in / regenerate assets |
| 6 | videos_generation | Each first frame is being animated into a clip | Fill in / regenerate assets |
| 7 | videos_done | Every clip reached a terminal state | POST /mv/task/{mv_id}/advance |
| 8 | editing | The editor is assembling the final cut | Wait |
| 9 | completed | Final cut is ready | Download final_cut_url, or swap assets and POST /mv/task/{mv_id}/advance to re-cut |
| 10 | failed | The task died; charges already made (director fee, first frames) are not refunded | Read the last event for the reason |
An unrecognised value is reported as unknown. That only happens if a task row carries a status this API version does not know about.
Flow
pending -> processing -> storyboard -> creating
|
(pause_after_storyboard: true) storyboard_done
| POST /mv/task/{mv_id}/advance
v
images_generation
|
v
images_done
|
POST /mv/task/{mv_id}/advance
|
v
videos_generation
|
v
videos_done
|
POST /mv/task/{mv_id}/advance
|
v
editing -> completed
|
POST /mv/task/{mv_id}/advance (re-cut after swapping assets)
|
editing
any state -> failed
- images_generation -> images_done and videos_generation -> videos_done are not triggered by a call. images_done requires every first frame to succeed: while any frame is failed the task stays in images_generation, and it moves on by itself once those frames are regenerated or filled in. The video stage flips to videos_done as soon as no sub-task in it is still pending or processing.
- With pause_after_storyboard: true, the task stops at storyboard_done once the shot plan is delivered: GET /mv/task/{mv_id} already returns the full asset list (first-frame prompt, video prompt and duration per shot), and first frames only start after you call POST /mv/task/{mv_id}/advance. Only the director fee has been charged at this point (it is refunded once the task reaches videos_done), and a task can stay parked there for as long as you like. Without the flag the task goes straight on to images_generation.
- images_done -> videos_generation, videos_done -> editing and completed -> editing (re-cut) are all driven by you, through the same endpoint: POST /mv/task/{mv_id}/advance. Video generation never starts on its own, even once every first frame is ready — you decide when to spend on it.
- Create the task with auto: true and the platform releases those two gates itself (images_done -> videos_generation and videos_done -> editing) and never stops at storyboard_done, so you do not call advance at all. The checks are exactly the same as a manual advance: if an asset failed or the balance is short, the task stays at the gate and continues on its own once that is fixed. Re-cutting after completion (completed -> editing) is not affected by auto and still needs a manual advance. Without auto nothing changes.
- Everything else moves on its own.
- Regenerating or filling in an asset never changes the task status. Once the task has reached images_generation, you can swap any first frame or clip at any stage — the task stays where it is and only that one asset is re-run (each regenerate is billed on its own). Moving forward is always your call via advance. If you change clips after the task is completed, call advance to re-cut from completed -> editing; the re-cut is free. The one exception is a failed task, which is pulled back to images_generation / videos_generation so the asset can run again.
Watermarks
The watermark object on the create call is applied at two different points:
- image_enable burns the text into the platform-generated first frames.
- video_enable produces a second, watermarked copy of the final cut, exposed as final_cut_watermarked_url. final_cut_url is always the clean version.
Both are ignored when text is empty, and neither applies to assets you upload yourself or to assets you paid to regenerate — those are never watermarked.
Assets model
A storyboard produces a list of shots. Every shot is one asset, identified by an asset_id such as A001, and it has two halves:
| Half | What it is | tool |
|---|---|---|
| Image | The first frame of the shot | i2v or lip-sync |
| Video | The clip animated from that frame | i2v or lip-sync |
tool: "i2v" is an ordinary image-to-video shot. tool: "lip-sync" is a singing shot driven by a slice of the song; those are generated by the platform as well (you can still replace the clip with your own afterwards) and their video prompt and audio window are never returned.
What the platform generates
Every video clip (both i2v and lip-sync) is generated by the platform with MiniMax H3. You cannot pick another video model when creating a task, and you cannot opt out of platform video to upload clips yourself. Whether the platform also generates the first frames is decided by image_model:
| Setting | Meaning |
|---|---|
| image_model: "foxaihub-image" | The platform generates every first frame and bills per image |
| image_model: "none" | The platform only writes prompts; you upload first frames via POST /mv/task/{mv_id}/assets with no image fees |
Self-supplied first frames cost nothing. The video fee is always charged once per MV, priced off the billable audio length, when the task reaches videos_done — see Billing.
Warning: An awaiting_user asset (a first frame you must upload when image_model is none) does hold its stage back. A stage only advances once nothing in it is pending, processing, or awaiting_user, so a task that is waiting on your first frames stays put until you supply them — in practice, the task will not reach images_done and advance will keep returning 409 until every first frame has been uploaded. All four states from images_generation to videos_done accept uploads, so you can upload frames and clips as they become ready rather than in one batch.
Failure handling
- A single failed shot does not kill the MV. The asset ends up failed and you can either upload a replacement or pay for a regenerate. A failed first frame keeps the task out of images_done until it is replaced.
- It does block the manual gates: advance refuses to leave a half of the pipeline with a failed (or still awaiting_user) asset in it and answers 409 with the gap, leaving the task where it is. Fill in or regenerate those assets, then call advance again.
- If every asset of a stage fails, the whole task goes to failed.
- A task waiting on you at storyboard_done, images_done or videos_done has no time limit and is never failed for sitting there.
Nothing is settled when a task ends: the director fee was charged when the asset list was delivered, each first frame when it was generated, and the video fee is charged when the task reaches videos_done (the director fee is refunded then), so there is no reservation to refund. See Billing.
API reference
Every endpoint below requires the api-key header and operates on tasks owned by that key. A task you do not own returns 404, never 403 — that keeps the status code from confirming that an unowned mv_id exists.
POST /mv/task
Create an MV task. Charges nothing, but requires a balance strictly greater than $5.00.
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
audio |
string | Yes | Publicly accessible audio URL (HTTP(S)). File size <= 30 MB |
audio_duration |
integer | No | 0-360 seconds, default 0 (whole track) |
main_image |
string | Yes | Publicly accessible image URL (HTTP(S)). File size <= 30 MB |
supporting_image |
string | No | Publicly accessible image URL (HTTP(S)). File size <= 30 MB. Optional reference photo of a supporting character, at most one per task; omit it and the MV has the lead only |
prompt |
string | Yes | 1-4096 characters |
aspect_ratio |
enum | No | 1:1, 4:3, 3:4, 16:9, 9:16 — default 9:16 |
resolution |
enum | No | 768p, 1080p — default 768p |
image_model |
enum | No | none, foxaihub-image — default none. gemini-3.1-flash-image-preview is still accepted and treated as foxaihub-image |
i2v_model |
enum | No | Only minimax-h3 is supported, and it is the default, so you can omit it; none returns 400 |
watermark |
object | No | video_enable, image_enable, position, text, font_size |
pause_after_storyboard |
boolean | No | Defaults to false. When true, the task stops at storyboard_done after the shot plan is delivered and first frames wait for advance |
auto |
boolean | No | Defaults to false; omitting it is the same as false. When true the task runs end to end: it never stops at storyboard_done, and the images_done and videos_done gates are released by the platform, so you never need to call advance. It still holds at a gate while an asset is failed / awaiting_user or the balance is short, and continues by itself once that is fixed. Re-cutting after completion (completed -> editing) still needs a manual advance |
Note:
supporting_imageis a second person, anchored separately frommain_image(the lead) — use a photo of someone different from the lead. The platform treats them as a supporting character and, like the lead, uses the photo to generate first frames and keep their look consistent. Leave it out if you only want a lead. This is the only way to add a supporting character, and a task takes at most one.
Request Body
{
"audio": "https://common.foxai.me/sample_music/3.mp3",
"audio_duration": 60,
"main_image": "https://common.foxai.me/sample_music/character.webp",
"supporting_image": "https://example.com/supporting-character.webp",
"prompt": "A neon-lit night city, the singer walking through the rain",
"aspect_ratio": "9:16",
"resolution": "768p",
"image_model": "foxaihub-image",
"i2v_model": "minimax-h3",
"watermark": {
"video_enable": true,
"image_enable": false,
"position": "right-bottom",
"text": "FoxAIHub",
"font_size": "medium"
}
}
Response
{ "mv_id": "6f1f4d2e-2b23-4c4e-9d3d-2a1c4c9d1f77" }
No success wrapper.
Errors
| Status | When | Body |
|---|---|---|
400 |
A field fails validation | Zod issue list |
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
402 |
Balance not greater than $5.00 (or the estimated video fee when audio_duration is set) | {"success":false,"error":"..."} |
413 |
Request body larger than 83951616 bytes | {"success":false,"error":"Request body exceeds 83951616 bytes"} |
500 |
Upload failed, pricing row missing, or task write failed | {"success":false,"error":"..."} |
GET /mv/task
List your tasks, newest first. Free.
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
limit |
integer | No | 1-100, default 20 |
offset |
integer | No | >= 0, default 0 |
Response
{ "success": true, "tasks": [ { "mv_id": "...", "status": "videos_generation", "assets_summary": { "images": {...}, "videos": {...} }, ... } ], "limit": 20, "offset": 0 }
assets replaced by assets_summary; no total count — page until fewer than limit entries come back.
Errors
| Status | When | Body |
|---|---|---|
400 |
limit / offset out of range | Zod issue list |
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
GET /mv/task/{mv_id}
Read one task with every asset. Free. The one MV endpoint that never has a success wrapper, on success or failure.
Response
{ "mv_id": "...", "status": "videos_generation", "params": {...}, "final_cut_url": null, "identity_prompt": "...", "assets": [ { "asset_id": "A001", "tool": "i2v", "image": {...}, "video": {...} } ] }
Errors
| Status | When | Body |
|---|---|---|
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
404 |
Unknown mv_id, malformed mv_id, or a task owned by someone else | {"error":"task not found"} |
POST /mv/task/{mv_id}/assets
Upload assets you produced yourself. Free. Accepted from images_generation onward and never changes the task status (except for a failed task).
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
assets |
array | Yes | 1-100 items |
assets[].asset_id |
string | Yes | 1-32 characters |
assets[].kind |
enum | Yes | image or video |
assets[].url |
string | No | HTTP(S) URL, <= 512 chars. url or error is required |
assets[].error |
string | No | 1-1000 chars, marks the asset failed |
Request Body
{
"assets": [
{ "asset_id": "A001", "kind": "image", "url": "https://example.com/frames/A001.png" },
{ "asset_id": "A002", "kind": "video", "url": "https://example.com/clips/A002.mp4" },
{ "asset_id": "A003", "kind": "image", "error": "I could not produce this frame" }
]
}
Response
{ "success": true, "results": [ { "asset_id": "A001", "kind": "image", "ok": true } ], "status": "images_done" }
HTTP 200 even if every item failed — always inspect ok.
Errors
| Status | When | Body |
|---|---|---|
400 |
Empty array, > 100 items, non-HTTP url, missing url and error | Zod issue list |
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
404 |
Unknown task | {"success":false,"error":"task not found"} |
409 |
Task is outside the mutable window | {"success":false,"error":"task does not accept assets (status: storyboard)"} |
POST /mv/task/{mv_id}/assets/{asset_id}/regenerate
Pay to produce a new version of one platform-owned asset. Charges immediately. The task status does not change (except for a failed task); the new version runs in place.
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
kind |
enum | Yes | image or video |
prompt |
string | No | 1-4096 chars, replaces the prompt for this run. Leave blank to re-run the original prompt |
Request Body
{
"kind": "image",
"prompt": "Same shot, but shot from a low angle at golden hour"
}
Response
{ "success": true, "asset_id": "A001", "kind": "image", "cost": 0.02, "status": "videos_done" }
Errors
| Status | When | Body |
|---|---|---|
400 |
kind missing/invalid, prompt too long | Zod issue list |
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
402 |
Not enough balance for this regenerate | {"success":false,"error":"Insufficient balance"} |
403 |
Asset is self-supplied (model: "none") | {"success":false,"error":"this asset is generated by you, use POST /mv/task/{mv_id}/assets instead"} |
404 |
Unknown task, or unknown asset_id for that kind | {"success":false,"error":"asset not found"} |
409 |
Task is outside the mutable window | {"success":false,"error":"task does not accept regenerate (status: storyboard)"} |
POST /mv/task/{mv_id}/advance
Drive one manual gate: storyboard_done -> images_generation, images_done -> videos_generation, videos_done -> editing, or completed -> editing (re-cut after swapping assets). Free. Body is optional.
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
to |
string | No | A status name, checked against the one valid next step |
Request Body
{
"to": "videos_generation"
}
An empty body, or no body at all, works too — the gate only has one legal next step.
Response
{ "success": true, "mv_id": "...", "status": "videos_generation" }
Errors
| Status | When | Body |
|---|---|---|
400 |
to is not a real status name, or the body is invalid JSON | {"success":false,"error":"unknown status: whoops"} |
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
404 |
Unknown mv_id, or a task owned by someone else | {"success":false,"error":"task not found"} |
409 |
The task is not at a gate, or to names anything else | {"success":false,"error":"task is not ready to advance (status: images_generation)"} |
409 |
An asset of the half being left behind is failed or still awaiting_user. The task stays where it is; fill in the gap and call again | {"success":false,"error":"image assets are incomplete (3 failed of 12); regenerate them before advancing","status":"images_done","assets":{"pending":0,"processing":0,"completed":9,"failed":3,"awaiting_user":0,"total":12}} |
POST /mv/events/{mv_id}
Append a custom event to the task's progress stream.
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
type |
string | Yes | 1-64 characters |
message |
string | object | array |
Request Body
{
"type": "progress",
"message": { "step": "review", "note": "Frames approved by the client" }
}
Response
{ "success": true, "event": { "mv_id": "...", "type": "progress", "message": "...", "created_at": "..." } }
Errors
| Status | When | Body |
|---|---|---|
400 |
type or message missing/too long | Zod issue list |
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
404 |
mv_id is not a UUID, unknown, or owned by someone else | {"success":false,"error":"task not found"} |
500 |
The event could not be appended | {"success":false,"error":"failed to append event"} |
GET /mv/events/{mv_id}
Read the progress event stream as a bare JSON array, oldest first. Free.
Response
[ { "mv_id": "...", "type": "status", "message": "pending", "created_at": "..." } ]
No success wrapper, on success or failure — branch on the HTTP status code.
Errors
| Status | When | Body |
|---|---|---|
401 |
Missing or unknown api-key | {"message":"Unauthorized"} |
404 |
mv_id is not a UUID, unknown, or owned by someone else | {"error":"task not found"} |
Webhook
There are three ways to track an MV task's progress: poll GET /mv/task/{mv_id}, read the event stream from the previous section, or configure a webhook. We recommend webhooks in production — your server gets pushed every status change instead of having to poll or hold a connection open. The event stream is better suited to development and debugging: convenient for a quick look while you're building, but its 24-hour lifetime and unauthenticated read make it a poor fit for anything you depend on long-term.
Note: A webhook is configured in your Dashboard (one URL per account), not per task — configure it once and events from every MV task, and other task types, are pushed directly to your endpoint.
Delivery format
Every delivery is a POST with a fixed envelope:
{
"event": "mv.status",
"type": "mv",
"delivery_id": "9f2c1a3e-abcd-4d11-8f00-0123456789ab",
"created_at": "2026-08-26T09:31:02.000Z",
"data": { "mv_id": "...", "type": "status", "message": "videos_done", "created_at": "..." }
}
| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | FoxAI-Webhook/1 |
| X-Webhook-Event | Same as event in the body |
| 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, the top-level type tells you which kind of result a delivery carries. It is mv for the entire music video pipeline, no matter which stage fired:
| type | Result |
|---|---|
| mv | Music video pipeline event |
| asr-serverless | Music transcription task reached a terminal state |
| ping | Test delivery triggered from the Dashboard |
Note: Do not confuse the two type fields: the top-level one is always mv and identifies the product, while data.type is the event's own subtype (status, progress, asset, context, error).
MV events
The event name is mv. plus the stream's type:
| Webhook event | Corresponding stream type |
|---|---|
| mv.status | status |
| mv.progress | progress |
| mv.asset | asset |
| mv.context | context |
| mv.error | error |
data is byte-for-byte the same object you'd read from GET /mv/events/{mv_id}, so you can reuse the same parsing logic.
Retry policy
- 10 second timeout per attempt.
- Retries at 5s, 10s, 20s — up to 4 attempts total, wrapping up within about 35 seconds.
- Only retried on network errors, timeouts, HTTP 408, 429 and 5xx. Every other 4xx is not retried.
- The same delivery keeps the same X-Webhook-Delivery (the delivery_id in the body) across retries — use it to deduplicate.
Security
Warning: The current version does not include a signature — anyone who knows your webhook URL can forge a request. Add a long random path segment to the URL, and always treat an incoming webhook as a wake-up call: confirm with GET /mv/task/{mv_id} before trusting it, rather than acting on the payload directly.
For the same reason, creating or updating a webhook URL rejects localhost, private IPs, and URLs containing a username/password.
Limits & failure handling
Input limits
| Limit | Value |
|---|---|
| audio_duration | 0-360 seconds; 360 is also the platform's hard cap on every task |
| File size of audio or an image | <= 30 MB |
| prompt | 1-4096 characters |
| Clip duration_seconds | integer, clamped to 3-15 seconds |
Retries and partial failure
- A single sub-task (one image or one clip) retries up to 3 times before it is marked failed.
- A single failed asset does not fail the whole MV — you can upload a replacement or pay to regenerate it.
- If every asset in a stage fails, the whole task is marked failed; charges already made for the director fee and first frames are not refunded, and no video fee is charged.
- A task parked at a manual gate (storyboard_done / images_done / videos_done) has no time limit.
Event stream limits
| Limit | Value |
|---|---|
| Stream lifetime | 24 hours, refreshed on every append |
| Events per stream | 1000; older events are dropped |
| Bytes per event | 32 KB; larger events are truncated with truncated: true |
Webhooks
See the Webhook section above for the delivery format and retry policy.
Suggested local schema
If you plan to persist these tasks and assets in your own system instead of calling the API live every time, here is a suggested MySQL 8 schema written from the caller's point of view. It is a starting point, not a spec — add or drop columns to fit your own needs.
Tasks table
Store the whole params object from GET /mv/task/{mv_id} as-is in a JSON column instead of exploding it into a dozen columns — that way a new field on params upstream does not force a migration on your side. Lock status down with a CHECK constraint (supported since MySQL 8.0.16):
CREATE TABLE mv_tasks (
mv_id CHAR(36) NOT NULL PRIMARY KEY,
status VARCHAR(20) NOT NULL,
aspect_ratio VARCHAR(16) NOT NULL,
resolution VARCHAR(16) NOT NULL,
params JSON NOT NULL,
identity_prompt TEXT NULL,
final_cut_url VARCHAR(512) NULL,
final_cut_watermarked_url VARCHAR(512) NULL,
created_at DATETIME(3) NULL,
updated_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
synced_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
CONSTRAINT chk_mv_tasks_status CHECK (status IN (
'pending','processing','storyboard','creating','storyboard_done','images_generation',
'images_done','videos_generation','videos_done','editing','completed','failed','unknown'
))
) ENGINE=InnoDB;
CREATE INDEX idx_mv_tasks_status ON mv_tasks (status);
Assets table
Store image and video as JSON, mirroring the same sub-objects the API returns. A MySQL 8 virtual generated column pulls out the field you'll filter on most — status — into an indexable column, with no redundant field to keep in sync by hand:
CREATE TABLE mv_assets (
mv_id CHAR(36) NOT NULL,
asset_id VARCHAR(32) NOT NULL,
tool VARCHAR(16) NULL,
image JSON NULL,
video JSON NULL,
image_status VARCHAR(16) GENERATED ALWAYS AS (image->>'$.status') VIRTUAL,
video_status VARCHAR(16) GENERATED ALWAYS AS (video->>'$.status') VIRTUAL,
synced_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (mv_id, asset_id),
CONSTRAINT fk_mv_assets_task FOREIGN KEY (mv_id) REFERENCES mv_tasks (mv_id) ON DELETE CASCADE,
CONSTRAINT chk_mv_assets_tool CHECK (tool IS NULL OR tool IN ('i2v', 'lip-sync'))
) ENGINE=InnoDB;
CREATE INDEX idx_mv_assets_image_status ON mv_assets (image_status);
CREATE INDEX idx_mv_assets_video_status ON mv_assets (video_status);
Webhook delivery dedup table
The Webhook section above notes that a retried delivery keeps the same delivery_id. Pair this table with INSERT IGNORE or ON DUPLICATE KEY UPDATE so the same status change never gets processed twice:
CREATE TABLE mv_webhook_deliveries (
delivery_id CHAR(36) NOT NULL PRIMARY KEY,
mv_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_mv_webhook_deliveries_mv_id ON mv_webhook_deliveries (mv_id);
Sync strategy
- Insert a row into mv_tasks as soon as POST /mv/task returns its mv_id.
- On every webhook, or whenever polling turns up a status change, upsert mv_tasks and mv_assets with ON DUPLICATE KEY UPDATE keyed on mv_id / asset_id — no need to check whether the row already exists first.
- Drive background jobs off the indexes on status, image_status and video_status: find tasks stuck at storyboard_done / images_done / videos_done that need an advance call, or assets stuck at awaiting_user that need an upload from you.
Note: This is only a starting point — adjust the columns and indexes to fit your own workload. Beyond your api-key, there is no reason to store any other sensitive credential in this local database.
Billing Rules
MV generation pricing consists of First Frame Image fees and an Audio Duration Video fee, plus a director fee that works as a deposit. Nothing is reserved: creating a task only checks your balance; a flat director fee ($0.20) is charged when the storyboard delivers the asset list; each first frame is charged when the platform generates it successfully; the video fee for the whole MV is charged only once the task reaches videos_done, and at that moment the director fee is refunded in full.
First frames can either be generated by the platform (charged per image) or self-supplied (free). Video generation is charged once per MV based on the billable audio duration. The platform runs the whole production pipeline: beat analysis, storyboarding, scene direction and video generation.
Rate Card
| Item | Rate |
|---|---|
| Task Creation | Free |
| Minimum Balance Requirement | Balance must exceed $5.00 (and the estimated video fee if a duration is specified) |
| First Frame Generation (foxaihub-image) | $0.02 per successfully generated image |
| Self-Supplied First Frame (none) | Free ($0.00) |
| Director Fee (deposit) | $0.20 per MV, charged when the asset list is delivered and refunded in full when the task reaches videos_done |
| Video Generation (768p) | $0.015 per second of billable audio, billed once per MV when the task reaches videos_done |
| Video Generation (1080p) | $0.03 per second of billable audio, billed once per MV when the task reaches videos_done |
| Single Asset Regenerate | Charged immediately per image or video clip duration |
| Stage Advance and Final Editing | Free |
| All Query Endpoints (GET) | Free |
| User-Uploaded Assets | Free |
When You Are Charged
Creating a task charges nothing; it only checks your balance. If an audio_duration is specified, the video fee is pre-estimated and the balance gate becomes max(minBalance, estimated video fee).
After audio preprocessing determines the actual billable length (up to 360 seconds), the video fee for the whole MV is computed and checked against your balance. If your balance cannot cover it, the task fails; nothing is charged at this step.
- Director fee: a flat fee charged once per MV at the moment the storyboard delivers the asset list (GET /mv/task/{mv_id} starts returning assets). A task that fails before the asset list exists is never charged it.
- Video fee: charged once per MV, at the moment the task reaches videos_done (no clip is still generating). It is charged in full even if some clips failed. If the task never reaches videos_done (it stays parked at a gate, or every clip fails), no video fee is charged.
- Director fee refund: when the video fee is charged at videos_done, the director fee is refunded to your balance in full. A task that never reaches videos_done keeps paying only the director fee plus the first frames already generated.
- Before you advance from images_done to videos_generation, your balance must cover the video fee, otherwise the call returns 402.
- First frame fee: each frame is charged when the platform generates it successfully. Failed frames are free.
- If your balance runs short at the moment of a charge, it is charged down to zero, the task keeps going, and the remainder is recorded as an outstanding amount.
- Self-supplied assets (source: user) incur no fees.
Single Asset Regenerations
During generation and review stages, regenerating an individual shot's image or video clip incurs an immediate charge based on the rate card. It is non-refundable regardless of final video composition.
Cost Examples
In the examples below, the $0.20 director fee is charged up front and refunded when the task reaches videos_done, so it is not part of the final totals.
Example 1: Fully platform-generated 768p MV (24 shots with platform frames, 90s billable audio):
| Item | Calculation | Amount |
|---|---|---|
| First Frame Images | 24 images x $0.02 | $0.48 |
| Video Generation (once per MV) | 90s x $0.015 | $1.35 |
| Actual Total Cost | $0.48 + $1.35 | $1.83 |
Example 2: Self-supplied frames + 768p video + 1x 5s clip regenerate (60s audio):
| Item | Calculation | Amount |
|---|---|---|
| First Frame Images | Self-supplied (image_model: none) | $0 |
| Video Generation (once per MV) | 60s x $0.015 | $0.9 |
| Clip Regenerate (billed immediately) | 1 x 5s x $0.015 | $0.075 |
| Actual Total Spent | $0.9 + $0.075 | $0.975 |
Transaction History
In your dashboard transaction history, MV tasks generate the following entries:
| Transaction Entry | Timing and Description |
|---|---|
| MV Generation | Video fee for the whole MV, charged when the task reaches videos_done |
| Director fee / Director fee refund | Charged when the asset list is delivered; refunded as a negative entry when the task reaches videos_done |
| First frame image model (foxaihub-image) | Each first frame generated successfully, and single first frame regenerations (billed immediately) |
| MiniMax H3 | Single video clip regeneration (billed immediately) |