What it is
Video generation runs MiniMax H3, an open-weight model that makes video with sound: dialogue, music and sound effects in the same clip. Every clip is 768p (768 pixels on the short side), 24 frames per second, an MP4 file with its audio track, 4 to 15 seconds long.
- Text to video. A prompt gives a clip with its own audio.
- First and last frame. The clip starts on your image, ends on another, or both.
- Reference images. People, products or places from up to 9 images appear in the clip.
- Video editing. Up to 3 reference videos and a prompt describing the change, such as an object added or replaced.
Aspect ratios: 21:9, 16:9 (the default for
text to video), 4:3, 1:1, 3:4,
9:16. With a first or last frame the frame sets the
ratio. With references the default is adaptive: the
ratio nearest the first reference.
Three ways in, with the same API keys and the same credit balance:
https://tasks.anex.sh- The MiniMax-compatible API. A MiniMax client works after you change its base URL and key.
https://run.anex.shandhttps://queue.run.anex.sh- The fal-compatible API, synchronous and queued. fal's client libraries work with it.
- Batch
- Many clips in one JSONL job on
https://api.anex.sh/batch/v1, estimated and approved before it runs.
In ComfyUI, the anex.sh video nodes make the same clips inside a workflow.
Models and names
Each surface calls a model by the name its clients expect. All three models run in fp8 (8-bit floating point): the published weights are quantized to fp8 as the model loads.
| model | steps | modes | MiniMax model | fal app id | batch model |
|---|---|---|---|---|---|
| MiniMax H3 | 50 | text to video, first and last frame | MiniMax-H3 | minimax/h3/text-to-video, minimax/h3/image-to-video | minimax-h3 |
| MiniMax H3 Ref2VA (4-step) | 4 | reference images, video editing | MiniMax-H3 with a reference, or MiniMax-H3-Ref2VA-SMD-4step | minimax/h3/reference-to-video, zjwfufu/smd-minimax-h3-ref2va-4step/reference-to-video | minimax-h3-ref2va |
| FastH3-8-Step-V2 | 8 | text to video | FastH3-8-Step-V2 | fastvideo/fasth3-8-step-v2/text-to-video | fasth3-8-step-v2 |
MiniMax's API has one model name and picks the task from the content
you send. So a MiniMax-H3 request with a
reference_image or reference_video item runs
on MiniMax H3 Ref2VA (4-step): H3's reference-to-video variant with the
4-step distillation published as
zjwfufu/SMD-MiniMax-H3-Ref2VA-4step.
Text and frame requests under MiniMax-H3 run on MiniMax H3
at 50 steps. MiniMax-H3-Ref2VA-SMD-4step calls the 4-step
model by its own name; a request under that name without a reference
is refused with a 400. On fal, each app takes only its own kind of
request: a reference app called without a reference answers 422.
The exact weights:
MiniMaxAI/MiniMax-H3
(its first-and-last-frame and reference variants) and
FastVideo/FastVideo-FastH3-8-Step-V2,
an 8-step distillation of H3 by FastVideo. A seed is
honoured by FastH3-8-Step-V2 and the 4-step reference model; MiniMax
H3 ignores it.
MiniMax-compatible API
Base URL https://tasks.anex.sh, with an API key from the
console's Keys page sent as Authorization: Bearer <api-key>.
Paths, bodies, statuses and errors follow MiniMax's H3 (v2) video API.
| method | path | does |
|---|---|---|
| POST | /v2/video_generation | create a task; answers {"task_id": "..."} |
| GET | /v2/query/video_generation/{task_id} | one task, as {"task": {...}} |
| GET | /v2/query/video_generation | your tasks, newest first: page_num, page_size (1 to 100, default 20), filter.status, filter.task_ids, filter.model |
| DELETE | /v2/video_generation/{task_id} | a queued task is cancelled at no charge; a succeeded or failed one is deleted with its clip; a running or cancelled one is refused |
Create and poll
curl -X POST https://tasks.anex.sh/v2/video_generation \
-H "Authorization: Bearer $ANEX_API_KEY" -H 'Content-Type: application/json' \
-d '{"model": "MiniMax-H3",
"content": [{"type": "text", "text": "A red fox trots across fresh snow at dawn, its breath steaming."}],
"resolution": "768P", "duration": 6, "ratio": "16:9"}'
{"task_id":"318407566120873452"}
import time, requests
BASE = "https://tasks.anex.sh"
HEADERS = {"Authorization": "Bearer <api-key>"}
task_id = requests.post(f"{BASE}/v2/video_generation", headers=HEADERS, json={
"model": "MiniMax-H3",
"content": [{"type": "text", "text": "A red fox trots across fresh snow at dawn."}],
"resolution": "768P", "duration": 6, "ratio": "16:9",
}).json()["task_id"]
while True:
task = requests.get(f"{BASE}/v2/query/video_generation/{task_id}",
headers=HEADERS).json()["task"]
if task["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(10)
print(task["content"]["url"] if task["status"] == "succeeded" else task.get("error"))
model, content, resolution and
duration are required. content holds exactly
one text item (up to 7,000 characters) and any media
items; duration is a whole number of seconds from 4 to
15; resolution is 768P. An integer
seed is optional, honoured by the models that take one.
{"type": "image_url", "image_url": {"url": "https://..."}, "role": "first_frame"}
{"type": "image_url", "image_url": {"url": "https://..."}, "role": "last_frame"}
{"type": "image_url", "image_url": {"url": "https://..."}, "role": "reference_image"}
{"type": "video_url", "video_url": {"url": "https://..."}, "role": "reference_video"}
An image_url without a role is the first frame when it is
the only media item. Frames and references never mix in one request.
A url is an https URL, a base64 data URI, or
input:<id> for an upload.
Statuses: queued, running,
succeeded, failed, cancelled.
A succeeded task carries the clip's link in content.url,
freshly signed on every read and valid for an hour, and its billed
units in usage. A failed task carries
error.code and error.message.
{"task": {"id": "318407566120873452", "model": "MiniMax-H3", "status": "succeeded",
"created_at": 1761750000, "updated_at": 1761750210,
"content": {"url": "https://..."},
"resolution": "768P", "duration": 6, "ratio": "16:9",
"usage": {"total_seconds": 6, "input_seconds": 0, "output_seconds": 6,
"input_image_count": 0},
"task_type": "generation", "modality": "video"}}
Callbacks
Add callback_url to the create body. Before the task is
created we POST {"challenge": "<value>"} to it; your
endpoint must answer 2xx with {"challenge": "<value>"}
within 3 seconds, or the create is refused with a 400. After that it
receives the task object, {"task": {...}} as a query
returns it, when the task starts running and when it ends. Callbacks
are unsigned, as on MiniMax: match the task id against the ones you
created. A delivery that does not get a 2xx is retried on the same
schedule as job webhooks.
Differences from MiniMax
- Output is
768Ponly. - MiniMax file ids (
mm_file://) and the files API are not available: send an https URL, a data URI or an upload, and read the clip fromcontent.url. - Reference audio (
audio_url,reference_audio) is not available. - A frame request ignores
ratio: the frame sets it, and the task reports the resolved ratio. - An organization may have 30 tasks and fal requests in flight together; past that a create answers 429.
fal-compatible API
https://queue.run.anex.sh serves fal's queue (submit,
status, result, cancel, webhooks) and https://run.anex.sh
the synchronous call. Send your API key as
Authorization: Key <api-key>.
With fal's Python client
export FAL_RUN_HOST=run.anex.sh # the client derives queue.run.anex.sh from it
export FAL_KEY=<api-key>
import fal_client
result = fal_client.subscribe("minimax/h3/text-to-video", arguments={
"prompt": "A red fox trots across fresh snow at dawn.",
"duration": 6,
"aspect_ratio": "16:9",
})
print(result["video"]["url"])
fal's JavaScript client works through its proxyUrl option
set to https://run.anex.sh/proxy, with your API key as its
credentials; files it uploads go to our storage through the same
proxy.
Queue
curl -X POST https://queue.run.anex.sh/minimax/h3/text-to-video \
-H "Authorization: Key $ANEX_API_KEY" -H 'Content-Type: application/json' \
-d '{"prompt": "A red fox trots across fresh snow at dawn.", "duration": 6}'
{"status":"IN_QUEUE","request_id":"<id>",
"response_url":"https://queue.run.anex.sh/minimax/h3/requests/<id>",
"status_url":"https://queue.run.anex.sh/minimax/h3/requests/<id>/status",
"cancel_url":"https://queue.run.anex.sh/minimax/h3/requests/<id>/cancel",
"queue_position":0,"logs":null,"metrics":{}}
GET status_url: 202 withIN_QUEUEorIN_PROGRESS, 200 withCOMPLETED.GET response_url: the result once completed, a 400 before.PUT cancel_url: 200 when the request left the queue (no charge), 202 when it is already running (it finishes and is billed), 400 when it has ended.
{"video": {"url": "https://run.anex.sh/media/...mp4", "content_type": "video/mp4",
"file_name": "<id>.mp4", "file_size": 2380112},
"seed": 42, "expanded_prompt": null, "timings": {"inference": 31.2}}
video.url needs no key and works until the clip is deleted,
30 days after the request ends. seed is present only on
the models that honour one.
Synchronous call
POST https://run.anex.sh/{app_id} holds the connection
until the clip is ready and answers with the result, for up to 10
minutes, or less with an X-Fal-Request-Timeout header in
seconds. When the wait runs out it answers 504
request_timeout: a request still queued is dropped at no
charge, and one already running finishes and stays readable on the
queue host. With "sync_mode": true the result carries the
clip itself as a data:video/mp4;base64,... URL.
Input
| field | apps | values |
|---|---|---|
prompt | all | required, up to 7,000 characters |
duration | text to video | required, 4 to 15 seconds; a fraction rounds to the nearest second |
duration | image and reference to video | whole seconds, 5 to 15, default 5 |
aspect_ratio | text and reference to video | one of the ratios above; adaptive (default) on reference to video |
image_url, end_image_url | image to video | first and last frame, either or both; with neither the request is text to video at 16:9 |
reference_image_urls, reference_video_urls | reference to video | up to 9 images and 3 videos, at least one item |
seed | all | an integer |
resolution | all | 768P |
sync_mode, enable_safety_checker | all | true or false |
Audio inputs (target_audio_url,
reference_audio_urls) are refused with a 422.
Webhooks
Submit to the queue with ?fal_webhook=https://..., or
webhook_url= in fal_client.submit. When the
request ends we POST
{request_id, gateway_request_id, status, payload, error, payload_error},
with status OK and the result as
payload, or ERROR. Each delivery is signed as
fal signs: Ed25519 over
request_id\nuser_id\ntimestamp\nhex(sha256(body)), in the
X-Fal-Webhook-* headers. Verify it against our keys at
https://run.anex.sh/.well-known/jwks.json (fal's own key
set does not match). user_id is your organization id.
A delivery that does not get a 2xx is retried on the same schedule as
job webhooks.
import base64, hashlib, time
import requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
KEYS = requests.get("https://run.anex.sh/.well-known/jwks.json").json()["keys"]
def verify(headers, body: bytes) -> bool:
timestamp = headers["X-Fal-Webhook-Timestamp"]
if abs(time.time() - int(timestamp)) > 300:
return False
message = "\n".join([headers["X-Fal-Webhook-Request-Id"],
headers["X-Fal-Webhook-User-Id"], timestamp,
hashlib.sha256(body).hexdigest()]).encode()
signature = bytes.fromhex(headers["X-Fal-Webhook-Signature"])
for key in KEYS:
raw = base64.urlsafe_b64decode(key["x"] + "=" * (-len(key["x"]) % 4))
try:
Ed25519PublicKey.from_public_bytes(raw).verify(signature, message)
return True
except Exception:
pass
return False
Batch JSONL
For many clips, submit a JSONL job through the
batch API with one of the batch model ids
above. Each line targets /v1/videos:
{"custom_id": "fox-1", "method": "POST", "url": "/v1/videos",
"body": {"prompt": "A red fox trots across fresh snow at dawn.", "duration": 6, "aspect_ratio": "16:9"}}
{"custom_id": "edit-1", "method": "POST", "url": "/v1/videos",
"body": {"prompt": "Replace the mug on the desk with a glass of orange juice.", "duration": 8,
"reference_videos": [{"url": "https://example.com/desk.mp4"}]}}
A body takes prompt, duration (whole
seconds, 4 to 15), aspect_ratio and seed,
plus the media keys of the job's model: first_frame and
last_frame (one item each) on minimax-h3;
reference_images and reference_videos (lists)
on minimax-h3-ref2va, where every line needs at least one
reference. An item is {"url": "https://..."} or
{"input_id": "<id>"}. The model is the job's: one
job runs one model.
The job is estimated and waits for approval like any other; the approved amount is the most it can cost. Each row of the result file carries its clip's link:
{"custom_id": "fox-1", "error": null,
"response": {"status_code": 200, "body": {
"status": "completed", "duration": 6, "aspect_ratio": "16:9",
"width": 1344, "height": 768, "fps": 24, "seed": null,
"usage": {"output_seconds": 6, "input_seconds": 0, "input_image_count": 0},
"video": {"url": "https://api.anex.sh/media/...mp4", "content_type": "video/mp4",
"file_size": 2380112}}}}
(Trimmed to the fields that matter here.) Download each clip with a
plain GET of video.url: it needs no key, redirects to the
file, and works until the job's data is deleted, 30 days after the job
finishes. A failed row has body: null and an
error with code and message.
Images and videos you supply
| images (frames and references) | reference videos | |
|---|---|---|
| count per request | 1 first frame, 1 last frame, or up to 9 reference images | up to 3, 15 s in total |
| formats | JPEG, PNG, WEBP, HEIC, HEIF | MP4 or MOV; H.264 or H.265 video; AAC, MP3 or no audio |
| size | 30 MB each | 50 MB each |
| dimensions | 256 to 5,760 pixels a side, width / height from 0.4 to 2.5 | |
| length and rate | 2 to 15 s each, 23.976 to 60 frames per second | |
Send each file one of three ways:
- An https URL
- Downloaded once by our media fetcher, following up to 5 redirects, within 60 seconds.
- A data URI
data:image/png;base64,...(alsoimage/jpeg,image/webp,image/heic,image/heif,video/mp4,video/quicktime). A request body is at most 64 MB on both compatible APIs, data URIs included.- An upload
- See below. Name it as
input:<id>in a compatible request, or{"input_id": "<id>"}in a batch line.
curl -X POST https://api.anex.sh/batch/v1/inputs \
-H "Authorization: Bearer $ANEX_API_KEY" -H 'Content-Type: application/json' \
-d '{"kind": "media", "content_type": "video/mp4"}'
{"id":"<id>","status":"awaiting_upload","upload_url":"https://...",
"fields":{...},"expires_at":"..."}
Upload the file with a form POST to upload_url, every entry
of fields first and the file last as file,
as for an input file, within the hour.
The id then stays valid for 24 hours and any number of requests may
use it. fal's JavaScript client uploads through
https://run.anex.sh/proxy by itself.
fal_client.upload_file in Python sends files to fal's own
storage, which we cannot read: pass a URL, a data URI
(fal_client.encode_file makes one) or an
input:<id> instead.
Pricing
| model | per second of video made | per second of reference video | per image |
|---|---|---|---|
| MiniMax H3 | $0.068 | - | first 5 free, then $0.034 |
| MiniMax H3 Ref2VA (4-step) | $0.051 | $0.051 | first 5 free, then $0.034 |
| FastH3-8-Step-V2 | $0.017 | - | - |
- A clip bills the duration you asked for. A reference video bills the part of it the model reads: its length or the clip's, whichever is shorter, summed over the videos and rounded to whole seconds, at most 15.
- Images count frames and reference images together, per request.
- 2K costs $0.11 per second on MiniMax H3 and its Ref2VA 4-step model, for the video made and for a reference video alike. Taking a finished 768p clip to 2K costs $0.0425 per second of 2K video.
- Failed, refused and cancelled-while-queued requests cost nothing. There is no minimum price.
- On the compatible APIs the price is held from your credits when the request is created, at its upper bound (each reference video counted at the clip's full length), and settled to the measured units when the clip is delivered: never above the hold. A batch job's approved estimate is its cap the same way.
Credits, holds and invoicing work as for every model: see pricing details.
Screening
Every request is checked against the use rules of the MiniMax H3 licence before anything is generated: the prompt when the request arrives, and its images and frames from its videos before the clip is made. A refused request makes no clip and is not billed. Batch jobs refuse row by row; the rest of the job runs. If the check cannot run, the request is refused and you can send it again. The rules are in our terms.
- MiniMax-compatible
- 422
unprocessable_entity_error, "video description contains sensitive content (1026)"; a refusal after the create fails the task witherror.codecontent_policy_violation. - fal-compatible
- 422 with a
detailitem of typecontent_policy_violationand itscategories. - Batch
- The row fails with status 422 and
error.codecontent_policy_violation, naming the rule.
Machine-readable marks
Generated text and video carry machine-readable marks that identify
them as AI-generated, as the EU AI Act requires. We measured marked and
unmarked output side by side, in English and Czech, and the marks did
not change its quality. Where text is marked, the temperature is at least 0.01, so a temperature of 0 may run at 0.01, and
top_p, top_k and min_p are held
to the same minimum of randomness (top_p at least 0.01,
top_k at least 2, min_p at most 0.99). Embeddings and decision answers are not marked. Anyone can
check text or a video for our marks at
POST https://api.anex.sh/batch/v1/detect/text and
POST https://api.anex.sh/batch/v1/detect/video; text needs
about 1,000 tokens for a reliable answer. See
checking content for our marks.
EU data residency
An organization that keeps its data in the EEA (European Economic Area) uses the EU hosts, where its clips are made and stored:
- MiniMax-compatible
https://tasks-eu.anex.sh- fal-compatible
FAL_RUN_HOST=run-eu.anex.sh, so the queue ishttps://queue.run-eu.anex.sh- Uploads and batch
https://api-eu.anex.sh/batch/v1
A request sent to the other region's host answers 421 with a message naming the right one, such as "this organization's data is kept in the EU region; use https://tasks-eu.anex.sh". Webhook signatures check against the same keys. See data residency.
Errors
MiniMax-compatible errors are
{"type": "error", "error": {"type", "message", "http_code"}, "request_id"},
with MiniMax's code in brackets at the end of the message. fal-compatible
errors are {"detail", "error_type"} with the type also in
X-Fal-Error-Type, or for a 422 a detail list of
{loc, msg, type}.
| status | when | MiniMax type (code) | fal error_type |
|---|---|---|---|
| 400 | invalid request, unknown model, a file refused at submit, failed callback check; MiniMax: EU data routing not available to the organization; fal: result read before it is ready, cancel after the end | bad_request_error (2013) | request_not_completed, request_already_completed, bad_request |
| 401 | missing or unknown API key | authorized_error (1004) | unauthorized |
| 402 | MiniMax: balance or credit limit reached | insufficient_balance_error (1008) | |
| 403 | organization suspended; fal: balance or credit limit reached, EU data routing not available to the organization | authorized_error (1004) | insufficient_balance, credit_limit_reached, user_locked, forbidden |
| 404 | unknown task, request or app | not_found_error (2013) | request_not_found, not_found |
| 408 | a large body was not received in time | bad_request_error (2013) | request_timeout |
| 413 | body over 64 MB | bad_request_error (2013) | request_too_large |
| 421 | the organization's data is kept in the other region | bad_request_error (2013) | misdirected_request |
| 422 | refused by screening; fal: an invalid field or file | unprocessable_entity_error (1026) | content_policy_violation, validation types |
| 429 | 30 requests in flight, or too many large bodies at once (retry after Retry-After) | rate_limit_error (1045) | concurrent_requests_limit, rate_limited |
| 499 | fal: the result of a cancelled request | request_cancelled | |
| 500 | screening unavailable (fal; send again), or our error | server_error (1000) | downstream_service_unavailable, internal_server_error |
| 503 | screening unavailable (MiniMax; send again), or a service we need is down | server_error (1000) | service_unavailable, webhooks_unavailable |
| 504 | fal: the synchronous wait ran out, or the clip took too long | request_timeout, generation_timeout |
A task or row that fails after it was accepted names the file and the
limit in its message, with one of these codes:
media_invalid_source, media_fetch_failed,
media_too_large, media_unsupported_format,
media_dimensions_out_of_range,
media_duration_out_of_range, media_unreadable,
content_policy_violation. On fal the same failures are a
422 whose loc names the field and item, typed
file_download_error, file_too_large,
unsupported_image_format,
unsupported_video_format, image_too_small,
image_too_large, image_load_error,
video_duration_too_short or
video_duration_too_long.