Video generation

Clips with sound from MiniMax H3, through MiniMax's API, fal's API or a batch job.

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.sh and https://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.

modelstepsmodesMiniMax modelfal app idbatch model
MiniMax H350text to video, first and last frameMiniMax-H3minimax/h3/text-to-video, minimax/h3/image-to-videominimax-h3
MiniMax H3 Ref2VA (4-step)4reference images, video editingMiniMax-H3 with a reference, or MiniMax-H3-Ref2VA-SMD-4stepminimax/h3/reference-to-video, zjwfufu/smd-minimax-h3-ref2va-4step/reference-to-videominimax-h3-ref2va
FastH3-8-Step-V28text to videoFastH3-8-Step-V2fastvideo/fasth3-8-step-v2/text-to-videofasth3-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.

methodpathdoes
POST/v2/video_generationcreate a task; answers {"task_id": "..."}
GET/v2/query/video_generation/{task_id}one task, as {"task": {...}}
GET/v2/query/video_generationyour 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 768P only.
  • 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 from content.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 with IN_QUEUE or IN_PROGRESS, 200 with COMPLETED.
  • 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

fieldappsvalues
promptallrequired, up to 7,000 characters
durationtext to videorequired, 4 to 15 seconds; a fraction rounds to the nearest second
durationimage and reference to videowhole seconds, 5 to 15, default 5
aspect_ratiotext and reference to videoone of the ratios above; adaptive (default) on reference to video
image_url, end_image_urlimage to videofirst and last frame, either or both; with neither the request is text to video at 16:9
reference_image_urls, reference_video_urlsreference to videoup to 9 images and 3 videos, at least one item
seedallan integer
resolutionall768P
sync_mode, enable_safety_checkeralltrue 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 request1 first frame, 1 last frame, or up to 9 reference imagesup to 3, 15 s in total
formatsJPEG, PNG, WEBP, HEIC, HEIFMP4 or MOV; H.264 or H.265 video; AAC, MP3 or no audio
size30 MB each50 MB each
dimensions256 to 5,760 pixels a side, width / height from 0.4 to 2.5
length and rate2 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,... (also image/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

modelper second of video madeper second of reference videoper image
MiniMax H3$0.068-first 5 free, then $0.034
MiniMax H3 Ref2VA (4-step)$0.051$0.051first 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 with error.code content_policy_violation.
fal-compatible
422 with a detail item of type content_policy_violation and its categories.
Batch
The row fails with status 422 and error.code content_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 is https://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}.

statuswhenMiniMax type (code)fal error_type
400invalid 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 endbad_request_error (2013)request_not_completed, request_already_completed, bad_request
401missing or unknown API keyauthorized_error (1004)unauthorized
402MiniMax: balance or credit limit reachedinsufficient_balance_error (1008)
403organization suspended; fal: balance or credit limit reached, EU data routing not available to the organizationauthorized_error (1004)insufficient_balance, credit_limit_reached, user_locked, forbidden
404unknown task, request or appnot_found_error (2013)request_not_found, not_found
408a large body was not received in timebad_request_error (2013)request_timeout
413body over 64 MBbad_request_error (2013)request_too_large
421the organization's data is kept in the other regionbad_request_error (2013)misdirected_request
422refused by screening; fal: an invalid field or fileunprocessable_entity_error (1026)content_policy_violation, validation types
42930 requests in flight, or too many large bodies at once (retry after Retry-After)rate_limit_error (1045)concurrent_requests_limit, rate_limited
499fal: the result of a cancelled requestrequest_cancelled
500screening unavailable (fal; send again), or our errorserver_error (1000)downstream_service_unavailable, internal_server_error
503screening unavailable (MiniMax; send again), or a service we need is downserver_error (1000)service_unavailable, webhooks_unavailable
504fal: the synchronous wait ran out, or the clip took too longrequest_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.