Fluxion docs
Everything you need to generate video from a prompt, in the app or over the API.
Introduction
Fluxion turns text and images into video. Pick a model, describe the shot, set duration, aspect ratio, and resolution, then generate. You can work in the playground or call any model over HTTP.
Quickstart
MiniMax-H3-Fast follows material you give it rather than inventing a scene from words, so a generation starts with an image or a clip. Store one, then submit and poll.
# 1. Store an image. The response carries a reference to use below.
curl -sS https://api.fluxion-sys.ai/v1/assets \
-H "Authorization: Bearer $FLUXION_API_KEY" \
-F file=@start.jpg
# -> {"id":"0a7304de…","type":"image","reference":"$REFERENCE_0A7304DE", …}
# 2. Submit; the response carries the video id.
curl -sS -X POST https://api.fluxion-sys.ai/v1/videos \
-H "Authorization: Bearer $FLUXION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3-Fast",
"prompt": "A slow push in on the scene in Image 1, golden hour",
"seconds": 6,
"resolution": "768P",
"aspect_ratio": "16:9",
"metadata": { "reference_image": ["$REFERENCE_0A7304DE"] }
}'
# 3. Poll until "status": "completed", then download the MP4.
curl -sS https://api.fluxion-sys.ai/v1/videos/$VIDEO_ID \
-H "Authorization: Bearer $FLUXION_API_KEY"
curl -sS -L -o out.mp4 https://api.fluxion-sys.ai/v1/videos/$VIDEO_ID/content \
-H "Authorization: Bearer $FLUXION_API_KEY"Not every model needs a reference — each model's own page says whether it does, and the ones that generate from a prompt alone take the same request without the metadata block.
Authentication
Create a key under Profile → API keys and send it as a bearer token on every request:
export FLUXION_API_KEY="sk-xxxxxxxxxxxxxxxxxxxx"Never ship a key in client-side code. Keys spend your balance, so rotate or revoke them from the same page if one leaks.
Models overview
Each model has its own strengths, supported durations, resolutions, and per-second pricing. Browse them in the model catalog.
Per-model reference
What each model takes, and what a second of it costs. This is read from the catalogue as your account sees it, so it lists exactly the models you can generate with - including any you have early access to.
Loading the model list…
Generating video
Generation is asynchronous. POST /v1/videos holds the cost and returns a video id with "status": "queued"; GET /v1/videos/{id} reports progress until it is completed or failed; GET /v1/videos/{id}/content streams the MP4 and honours Range requests. A failed job returns what it held.
You do not have to keep asking. Add a webhook to the request and we will call you once it is done — see Getting called back. If you do poll, poll about twice a second; there is nothing to gain above that, and our own example once suggested eight times a second, which was our mistake.
For image-to-video, pass image_url, or post the same fields as multipart/form-data with an image part. Longer clips and higher resolutions cost more.
Getting called back
Add a webhook to any generation and we will POST to it once the job finishes, instead of you asking whether it has. Works on every video model. The submit response is unchanged — an id, and "status": "queued".
curl -sS -X POST https://api.fluxion-sys.ai/v1/videos \
-H "Authorization: Bearer $FLUXION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "MiniMax-H3-Fast",
"prompt": "A slow push in on the scene in Image 1, golden hour",
"seconds": 5,
"metadata": {"reference_image": ["$REFERENCE_0A7304DE"]},
"webhook": "https://your-service.example.com/hooks/fluxion"
}'Your URL must be https and must resolve to a public address. It is checked when you submit, so a typo is a 400 on the generation rather than a callback that never comes.
What arrives:
POST /hooks/fluxion
X-Fluxion-Event: video.completed
X-Fluxion-Delivery: 9f2c1d8a4b6e5f70a1b2c3d4e5f60718
X-Fluxion-Timestamp: 1790319372
X-Fluxion-Signature: v1=361d09af770274eb9...
X-Fluxion-Attempt: 1
{
"id": "evt_4f1c...",
"type": "video.completed",
"created_at": 1790319372,
"data": {
"id": "task_EbiFlqOdlvoxQ0CmFd2jzT4jbE0dKWWU",
"object": "video",
"status": "completed",
"model": "MiniMax-H3-Fast",
"seconds": 5,
"resolution": "768P",
"content": "/v1/videos/task_EbiFlqOdlvoxQ0CmFd2jzT4jbE0dKWWU/content"
}
}type is video.completed or video.failed; a failure carries error and no content.
The callback carries no link to the video. It tells you the clip exists and where to ask for it; the bytes are still GET /v1/videos/{id}/content with your key. That is deliberate — a callback body ends up in your logs, your queue and your error tracker, and a URL that downloads your video without a credential does not belong in any of those. You are trading a poll loop for one authenticated request, not for zero.
Measured on MiniMax-H3-Fast, twenty clips: the callback went out 4.38 s after the submit call started, against 4.73 s for polling twice a second — and used one request instead of 6.6. On models that take minutes the difference is the whole integration rather than a third of a second: callers waiting on Seedance-2.5 were averaging 95 status requests per generation.
Verifying a callback
Anyone who learns your callback URL can POST to it, so check the signature before you trust the body. Your signing secret is per account:
curl -sS https://api.fluxion-sys.ai/v1/webhooks/secret \
-H "Authorization: Bearer $FLUXION_API_KEY"
# -> {"secret":"whsec_...","algorithm":"hmac-sha256",
# "signed_material":"<x-fluxion-timestamp>.<raw request body>"}import hashlib, hmac, time
def verify(secret, body: bytes, timestamp: str, signature: str, tolerance=300):
if abs(time.time() - int(timestamp)) > tolerance:
return False # too old to accept
expected = "v1=" + hmac.new(
secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)HMAC-SHA256 over the timestamp, a literal ., and the exact bytes of the body — a re-serialisation of the parsed JSON will not match. Use a constant-time compare, and keep the tolerance check: the timestamp is inside the signed material, so a captured body cannot be re-signed with a fresh one, but rejecting old timestamps closes the window for replaying it with its original one. POST /v1/webhooks/secret/rotate issues a new secret and invalidates the old one immediately.
You may see an event twice. A delivery that times out after your handler committed is indistinguishable from one that never arrived, so we retry it. X-Fluxion-Delivery is the same for every attempt at the same event — store it and ignore one you have already handled. Answer 2xx as soon as you have the event and do your work afterwards; your endpoint has ten seconds.
When one does not arrive
Every attempt is recorded, including your endpoint's own answer, so most questions about a missing callback are answerable without asking us.
curl -sS https://api.fluxion-sys.ai/v1/webhooks/deliveries \
-H "Authorization: Bearer $FLUXION_API_KEY"
# -> {"deliveries":[{"task_id":"task_...","state":"waiting","attempts":3,
# "event":"video.completed","last_status":502,
# "last_error":"the endpoint answered 502 with an empty body",
# "next_attempt_at":"2026-09-25T19:17:01Z","delivered_at":null}]}state is waiting, delivered, or abandoned. Retries are exponential with jitter — 5s, 10s, 20s, doubling to an hour — for 24 hours. Anything outside 2xx is a failure, including a 3xx: redirects are not followed.
If you were down longer than that, ask again. This works from any state, including delivered — if you lost the event on your side, ask rather than reconstruct:
# send it again; the 24 hour window starts over
curl -sS -X POST https://api.fluxion-sys.ai/v1/webhooks/deliveries/task_.../redeliver \
-H "Authorization: Bearer $FLUXION_API_KEY"
# or point it somewhere else and retry in one call
curl -sS -X PUT https://api.fluxion-sys.ai/v1/webhooks/deliveries/task_.../url \
-H "Authorization: Bearer $FLUXION_API_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://your-new-host.example.com/hooks/fluxion"}'If a host of yours stops answering entirely we pause on it for a while rather than keep attempting every queued callback against it — a minute at first, doubling to at most half an hour. GET /v1/webhooks/endpoints shows whether that has happened, which is worth knowing because “we stopped calling you” and “your generations stopped finishing” look identical from your side. Asking for a redelivery clears the pause. Pauses consume no attempts and do not extend the 24 hours.
Nothing is lost while you are down. A callback is a convenience, not the record: the clip is stored and GET /v1/videos/{id} returns it for as long as you keep it, whether or not a callback ever arrived.
Working to a deadline
A callback removes the poll loop but not the wait. Where a model offers metadata.generation_time_budget_s — the per-model reference above says which — you can also cap the generation itself, and the two together let you hold a round trip to a deadline. The budget is a planning target, not a hard stop: the clip is planned to fit it, working from less of your reference detail at the same length and size, and a budget that cannot be met is refused with the smallest one that can. The price is the same either way.
The budget covers generation only — not the submit call, the queue, or the notification. Measured, with one reference reused so the provider already holds it: a budget of 3.0 gave 3.14–3.34 s from submit to callback. Roughly, budget ≈ your target − 0.5 s, and measure it against your own deadline rather than trusting the arithmetic.
{
"model": "MiniMax-H3-UltraFast",
"prompt": "A slow push in on the scene in Image 1",
"seconds": 5,
"metadata": {
"reference_image": ["$REFERENCE_0A7304DE"],
"generation_time_budget_s": 3.0
},
"webhook": "https://your-service.example.com/hooks/fluxion"
}Two things matter as much as the budget. Reuse a reference rather than uploading a new one per request — the provider fetches an image it has not seen before during your submit call, which cost 0.3 s of the first request and nothing on the ones after it. And keep your handler fast: acknowledge, then work.
Generating images
An image is not a job. POST /v1/images/generations holds open for the seconds the render takes and returns the picture, so there is nothing to poll and no id to poll it with. The route is OpenAI-shaped, so an existing images client works by pointing it here.
size is the shape as well as the resolution (“2048x1152”) — there is no separate aspect ratio. For image-to-image, pass image: one value or a list, each a public https URL, a data: URI, or the reference of one of your assets. On models that charge for inputs, the first is free and the rest are billed per image; the per-model reference above gives the figures.
Two defaults worth knowing. The provider would mark the corner “AI generated”; we send watermark: false unless your request says otherwise, exactly as for video. And the url that comes back is the provider's own and expires — every image is also kept in your own storage and appears under Library, so that URL is never the only copy. Ask for "response_format": "b64_json" to get the bytes inline instead.
One image per request: n above 1 is refused rather than silently ignored, because this provider makes one picture per call.
Parameters
prompt, text description of the shot (required).image_url, optional image to animate (image-to-video).seconds, clip length; each model lists the values it accepts.aspect_ratio, e.g. 16:9, 9:16, 1:1.resolution, per model; each model's page lists the ones it accepts.audio, generate sound, on models that support it.seed, for reproducible output, on models that support it.
Image generation takes a different set: prompt, size, image, response_format, watermark and seed. There is no duration, no aspect ratio and no audio.
Assets
Files you store with us and reuse. Three calls, and the same objects the Library page shows — so whatever you build sees exactly what you see.
Three calls
An asset is a file you store with us and reuse: an image, a clip, a piece of audio. Upload it once, then name it in as many generations as you like.
| POST /v1/assets | Store a file. Returns its id. |
| GET /v1/assets | What you have: id, name, type, whether it is a portrait. |
| DELETE /v1/assets/{id} | Remove it, here and at the video provider. |
Creating takes multipart/form-data. Only file is required.
| Field | Type | Meaning |
|---|---|---|
| file | binary | The image, video or audio file. |
| name | string | What to call it. Defaults to the filename. |
| portrait | boolean | Register this image as a reusable character. Images only. |
curl -sS https://api.fluxion-sys.ai/v1/assets -H "Authorization: Bearer $FLUXION_API_KEY" -F file=@lead-singer.jpg -F name="Ana"
{
"id": "0a7304deacbf42e6bc8d908d6c67ba8d",
"name": "Ana",
"type": "image",
"bytes": 184213,
"portrait": false,
"portrait_status": null,
"portrait_error": null,
"created_at": "2026-09-21T22:00:02Z",
"reference": "$REFERENCE_0A7304DE"
}Listing returns exactly the same object for every asset — nothing is trimmed, so whatever you can read after creating one you can read again here. ?type= and ?portrait=true narrow it.
curl -sS "https://api.fluxion-sys.ai/v1/assets?portrait=true" -H "Authorization: Bearer $FLUXION_API_KEY"
{
"assets": [
{
"id": "0a7304deacbf42e6bc8d908d6c67ba8d",
"name": "Ana",
"type": "image",
"bytes": 184213,
"portrait": true,
"portrait_status": null,
"portrait_error": null,
"created_at": "2026-09-21T22:00:02Z",
"reference": "$REFERENCE_0A7304DE"
}
]
}| Field | Type | Meaning |
|---|---|---|
| id | string | The asset. Use it to delete one. |
| name | string | What you called it, or the filename. |
| type | enum | image, video or audio. |
| bytes | integer | Size on disk. Storage is charged by the gigabyte-month. |
| portrait | boolean | Whether the video provider has accepted it as a reusable character. |
| portrait_status | enum | pending, processing or failed while it is being registered; null once portrait is true, and null if it never was one. |
| portrait_error | string | The provider's reason, when registering failed. |
| created_at | string | RFC 3339, when it was stored. |
| reference | string | What to write in a generation request. Does not expire. |
Deleting removes it from your library and, if it was a portrait, from the video provider — immediately, and that part cannot be undone. A file some generation was made from is refused with 409 until you add ?confirm=1, because deleting it means that generation can never be re-run.
Portraits
A portrait is an image registered with the video provider so it can be reused as a character. It is not a different kind of asset — it is one of yours, with portrait: true on it.
What registering buys is not a new kind of input: the model takes the same image either way. The difference is that models generating people intercept reference material that looks like a deepfake or an infringement, and a registered image is let through where the same file sent as a plain link is stopped. It also keeps a character recognisably the same across clips.
curl -sS https://api.fluxion-sys.ai/v1/assets -H "Authorization: Bearer $FLUXION_API_KEY" -F file=@ana.jpg -F name="Ana" -F portrait=trueIt is a yes or no. There is no kind to choose: the provider has two libraries and only one of them can be reached at all, so an invented character and a real person go to the same place. By registering one you are asserting you have the right to use the likeness, and we keep a record of that with the file.
Registering is asynchronous and not instant. The asset exists straight away and works as an ordinary file; portrait turns true when the provider accepts it, usually within a few minutes. Until then portrait_status says where it has got to — pending, processing, or failed with their own reason in portrait_error, most often their content review. Poll GET /v1/assets?portrait=true until it appears.
Two things worth knowing before registering in bulk. Each portrait takes a slot in a quota we buy from the provider, so register the characters you will reuse rather than everything you upload. And deleting one reaches the provider immediately and cannot be undone — uploading the same face again is one call, but it has to be prepared from scratch.
Using an asset in a generation
A generation takes the asset's reference, not its id. Every response above carries one, ready to paste in.
# 1. what do I have?
curl -sS https://api.fluxion-sys.ai/v1/assets -H "Authorization: Bearer $FLUXION_API_KEY"
# 2. generate, writing those references straight into the request
curl -sS https://api.fluxion-sys.ai/v1/videos -H "Authorization: Bearer $FLUXION_API_KEY" -H 'Content-Type: application/json' -d '{
"model": "Seedance-2.5",
"prompt": "The woman in Image 1 walks through the market in Image 2, smiling.",
"seconds": 10,
"resolution": "720p",
"aspect_ratio": "16:9",
"metadata": {
"reference_image": ["$REFERENCE_0A7304DE", "$REFERENCE_31CA18C7"]
}
}'A reference does not expire. It names one of your assets, and what it stands for — a registered portrait, or a fresh link to your file — is worked out when the request is submitted. Store it in your own config if you like; it keeps working.
The prompt names them by position. Image 1, Image 2, Video 1, Audio 1 — counting within each type, in the order you put them in the list. Never put a reference or an id in the prompt itself: it is not recognised, and the model reads it as words.
A reference that names nothing is refused with 404 before the job is submitted, so a typo costs you a message rather than a generation.
The same values go in metadata.reference_video, metadata.reference_audio and metadata.first_frame_image. You can still pass any public https URL of your own instead — assets exist so you do not have to host anything, not because the API insists on them.
Image generation takes the same references. There the field is simply image, at the top level rather than under metadata, and it counts the same way — the first entry is Image 1:
curl -sS https://api.fluxion-sys.ai/v1/images/generations -H "Authorization: Bearer $FLUXION_API_KEY" -H 'Content-Type: application/json' -d '{
"model": "Seedream-5.0-Pro",
"prompt": "The room in Image 1, restyled with the palette of Image 2",
"size": "2048x1152",
"image": ["$REFERENCE_0A7304DE", "$REFERENCE_31CA18C7"]
}'API reference
Video models share one endpoint, /v1/videos, and image models share /v1/images/generations; either way the model is selected with the request's model field. See the per-model reference, with its exact sizes, durations and pricing, from any model's playground under the API tab.
Rate limits
Requests are limited per account. Bursting returns HTTP 429 with a Retry-After hint; retry with exponential backoff. Poll a job every few seconds rather than in a tight loop.
Errors
Errors come back as { "error": { "message", "type", "code" } } with a request id in the message, which is worth logging.
400, invalid parameters, e.g. a resolution the model does not support.401, missing or invalid API key.403, not enough credits for the request.429, rate limited.503, no capacity for that model right now; retry shortly.
Billing
Pay per second of generated video, priced per model and resolution. Credits are held when a job is submitted and returned if it fails. Top up and set a low-balance alert from Billing. Credits never expire.
API keys
Create, name, and revoke keys under Profile → API keys, where each one also shows when it was last used and how much it has spent. Treat keys like passwords: they spend your credits.