One API key and one request format for image and video generation models. Images come back in the response; videos run as tasks that you poll or receive by webhook.
Create a key in Console → API keys. The full key is shown only once, so store it right away, for example as MLMUX_API_KEY.
Call an image or video endpoint with a model ID and a prompt. Usage is charged to the account that owns the key.
Image requests return URLs in the response. Video requests return a task: poll its poll_url or subscribe to a webhook.
Send your key in the Authorization: Bearer header on every request to https://api.mlmux.com. Keys start with sk-. The API does not accept keys in other headers or in the query string.
To check that a key works, list the models it can call:
https://api.mlmux.com/v1/modelscurl https://api.mlmux.com/v1/models \
-H "Authorization: Bearer $MLMUX_API_KEY"{
"object": "list",
"data": [
{ "id": "nano-banana-2", "object": "model", "owned_by": "platform" },
{ "id": "model/seedance-2.5-max", "object": "model", "owned_by": "platform" }
]
}All endpoints are under https://api.mlmux.com. Request and response bodies are JSON unless noted.
| Method | Path | Description |
|---|---|---|
GET | /v1/models | List the model IDs you can call. |
POST | /v1/images/generations | Generate images from a prompt. |
POST | /v1/images/edits | Edit or combine input images. Accepts JSON or multipart. |
GET | /v1/images/generations/{task_id} | Get an async image task. Edits use /v1/images/edits/{task_id}. |
POST | /v1/videos/generations | Create a video task. |
GET | /v1/videos/generations/{task_id} | Get a video task. |
GET | /v1/tasks/{task_id} | Get any task by ID. |
POST | /v1/tasks/{task_id}/cancel | Cancel a task that has not finished. |
POST | /v1/uploads | Upload a reference file and get a URL. |
GET | /v1/account/balance | Get your credit balance. |
Set route_mode in the request body to choose the upstream tier. A request stays in its tier and never silently moves to another one; if the tier is turned off for the model, the request fails with route_mode_disabled. When the field is omitted, MLMux uses the route modes set on the API key, then the model default, which is official-cheap.
| Value | Description |
|---|---|
official | The provider's official route, billed at the provider's list price. |
official-cheap | A lower-cost official route. Default for new requests and API keys. |
Usage is charged in credits, and 1 credit equals 1 US dollar. Video models are billed per output token; image models are billed per upstream token or per image, depending on the model. See the pricing page for rates on each route.
Task responses show estimated_charge_credits when the task is created and actual_charge_credits after it succeeds, both in credits. Webhook payloads show actual_charge_nano_credits as an integer, where 1,000,000,000 nano-credits equal 1 credit.
Check your remaining balance with GET /v1/account/balance.
Video requests, and image requests sent with "async": true, return HTTP 202 with a task. Poll the task's poll_url every few seconds until its status is succeeded, failed, expired, or canceled.
| Status | Description |
|---|---|
queued | Accepted and waiting to start. |
submitted | Sent to the upstream provider. |
running | Generating. progress runs from 0 to 100. |
succeeded | Finished. The response now includes the output URLs. |
failed | Failed. message explains why. |
expired | Did not finish within the task's time limit. |
canceled | Canceled through the cancel endpoint. |
https://api.mlmux.com/v1/tasks/{task_id}/cancelTasks that are queued, submitted, or running can be canceled, and their held credits are released. The response is {"id": "task_…", "status": "canceled"}. Tasks in any other status return 409 task_not_cancelable.
Async video and image generation requests accept an Idempotency-Key header of up to 160 characters. Sending the same key with the same body again returns the original task instead of creating a new one. Sending the same key with a different body returns 409 idempotency_key_reused. Synchronous requests and image edits do not accept this header.
To use a local file as a reference, upload it as the multipart field file. Images, videos, and audio up to 50 MB are accepted and kept for 3 days. Pass the returned url in any URL field of a generation request.
https://api.mlmux.com/v1/uploadscurl https://api.mlmux.com/v1/uploads \
-H "Authorization: Bearer $MLMUX_API_KEY" \
-F "file=@start-frame.jpg"{
"url": "https://.../uploads/3f8a....jpg",
"asset_id": 1024,
"mime_type": "image/jpeg",
"size_bytes": 482113,
"asset_type": "image"
}Create an endpoint in Console → Webhooks and choose the events to receive. When a request or task finishes, MLMux sends a POST request with a JSON body to your URL.
| Event | Description |
|---|---|
request.succeeded | A request or task succeeded. |
request.failed | A request or task failed. data.error_code and data.error_message explain why. |
webhook.test | Sent when you test the endpoint in Console. |
{
"event_id": "evt_5b0d...",
"event_type": "request.succeeded",
"created_at": "2026-10-26T08:00:00Z",
"data": {
"request_id": "task_task_01a9...",
"task_id": "task_01a9...",
"public_model_id": "model/seedance-2.5-max",
"api_method": "videos.generations.create",
"image_count": 0,
"image_resolution": "",
"video_seconds": 8,
"video_resolution": "720p",
"actual_charge_nano_credits": 1790000000,
"latency_ms": 94210
}
}Each delivery is signed with the endpoint's signing secret (whsec_…), which is shown once when you create or rotate it. The X-MLMux-Signature header holds a Unix timestamp t and a signature v1: the hex HMAC-SHA256 of the timestamp, a period, and the raw request body. Compute the same value, compare it, and reject old timestamps.
import crypto from "node:crypto";
// header: X-MLMux-Signature: t=1767225600,v1=5f8c...
export function verifyMLMuxSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}Return any 2xx status within 10 seconds. MLMux makes up to 6 delivery attempts over about 11 hours. Use X-MLMux-Event-ID to ignore duplicate deliveries.
Errors share one JSON shape. Branch on code rather than message. Include request_id, which is also returned in the X-Request-ID header, when you contact support.
{
"error": {
"message": "Invalid API key",
"type": "invalid_request_error",
"code": "invalid_api_key",
"request_id": "req_9f2c..."
}
}| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The body is not valid JSON or a field has the wrong type, such as an unknown route_mode on an image generation. |
| 400 | invalid_value | A value such as resolution, size, aspect_ratio, or n is not supported by the model. |
| 400 | model_not_found | The model ID does not exist or is not available. |
| 400 | invalid_video_request | A video mode, reference count, ratio, resolution, or duration is invalid. |
| 400 | content_policy_violation | The prompt or an input was blocked by content moderation. |
| 401 | invalid_api_key | The API key is missing, invalid, expired, or disabled. |
| 402 | insufficient_credits | The balance cannot cover the estimated charge. Add credits and try again. |
| 402 | spend_limit_exceeded | The request would exceed a spend limit set in Console. |
| 402 | api_key_monthly_limit_exceeded | The request would exceed this API key's monthly budget. |
| 403 | email_verification_required | Verify the account email address before using the API. |
| 403 | model_not_allowed | This API key is restricted to other models. |
| 403 | forbidden_asset_reference | An asset:// reference does not belong to this account. |
| 404 | task_not_found | The task does not exist or belongs to another account. |
| 409 | idempotency_key_reused | The Idempotency-Key was already used with a different body. |
| 409 | task_not_cancelable | The task has already finished. |
| 429 | rate_limit_exceeded | Too many requests per minute for this key or account. |
| 429 | capacity_exhausted | Every upstream on the selected route is busy. |
| 502 | model_unavailable | The selected route has no available upstream for this model. |
| 502 | generation_failed | The upstream provider failed to generate an output. |
| 503 | generation_unavailable | The upstream provider is temporarily unavailable. |
| 503 | route_mode_disabled | The selected route mode is turned off for this model. Send another route_mode. |
| 504 | generation_timeout | The upstream provider timed out. |
429 responses include a Retry-After header. Wait that many seconds before retrying, and retry 5xx errors with exponential backoff.