How to Generate AI Video with the MiniMax (Hailuo) API
6 min read • September 30, 2024 (June 22, 2026)
Table of contents
- Introduction
- Supported models
- Pricing
- Generate a video in two API calls
- Image-to-video and references
- Batch-generate with a script
- Examples
- Frequently asked questions
- Conclusion
Introduction
MiniMax (the AI lab behind Hailuo AI) puts a frontier roster behind one account — its own Hailuo 01 / 02 / 2.3 video, plus Veo 3.1 and Sora 2 hosted through MiniMax — and you can drive every one of them from code. useapi.net fronts it with a third-party MiniMax API that runs your own MiniMax / Hailuo account over a standard REST endpoint, and the same account and token also reach a deep image lineup — Midjourney V7 / NiJi 7, Nano Banana 2 / Pro, GPT Image 1.5 / 2.0, Seedream 4.5 / 5.0, and image-01.
This guide focuses on video and images. MiniMax’s standalone text-to-speech and music endpoints have been retired — Speech 2.5 and Music 2.0 now run only through the MiniMax Agent (POST /agent). For a dedicated music API, see Mureka.
Supported models
Pick a model per request with the model field on POST /videos/create. If you omit model, the API defaults to T2V-01 for text prompts and I2V-01 for image prompts (Hailuo 01, 720p, 6 sec).
| Model | Family | Type | Resolution / duration | Notes |
|---|---|---|---|---|
T2V-01, I2V-01, I2V-01-live | Hailuo 01 | Text / image-to-video | 720p, 6 sec | Defaults · I2V-01-live live-photo effect |
T2V-01-Director, I2V-01-Director | Hailuo 01 | Text / image-to-video | 720p, 6 sec | Camera-movement control via [<movement>] in the prompt |
S2V-01 | Hailuo 01 | Subject reference | 720p, 6 sec | Character / subject consistency |
02 | Hailuo 02 | Text / image-to-video | 512p / 768p / 1080p, 6–10 sec | Native 1080p · end frame (768p/1080p) |
T2V-2.3, I2V-2.3 | Hailuo 2.3 | Text / image-to-video | 768p / 1080p, 6–10 sec | — |
I2V-2.3-Fast | Hailuo 2.3 | Image-to-video | 768p / 1080p, 6–10 sec | Faster, lower cost |
Sora-2 | Sora 2 (OpenAI) | Text / image-to-video | 720p, 4 / 8 / 12 sec | aspectRatio 16:9 / 9:16 · I2V needs exact 1280×720 or 720×1280 |
Veo-3.1, Veo-3.1-Fast | Veo 3.1 (Google DeepMind) | Text / image-to-video | 720p / 1080p / 4K, 8 sec | Native audio · end frame · aspectRatio 16:9 / 9:16 |
Veo-3.1-S2V, Veo-3.1-S2V-Fast | Veo 3.1 | Subject reference | 720p / 1080p / 4K, 8 sec | Up to 14 reference images |
Seedance-2.0 | Seedance 2.0 (ByteDance) | Text / image-to-video | 480p / 720p / 1080p, 4–15 sec | Omni multi-modal references · 6 aspect ratios |
Seedance-2.0-Fast | Seedance 2.0 | Text / image-to-video | 480p / 720p, 4–15 sec | Faster, no 1080p |
Hailuo 02/2.3, Sora 2, and Veo 3.1 take an options string for resolution and duration (e.g. 1080p-6sec, 720p-8sec, veo-1080p-8sec). Seedance models set resolution and duration directly. See POST /videos/create for the full per-model matrix.
The same account and token also drive Dreamina-class image generation on POST /images/create (default image-01) — listed here for reference:
| Model | Resolution | Max refs | Notes |
|---|---|---|---|
image-01 | HD | 1 (characterFileId) | Default · character-focused |
midjourney-v7 | n/a | 14 | Always 4 images per request |
midjourney-niji7 | n/a | 14 | Anime-focused · always 4 images |
nano-banana-2 | 512P / 1K / 2K / 4K | 14 | Lower-cost flash variant · @Image<N> mentions |
nano-banana-pro | 1K / 2K / 4K | 14 | High resolution · @Image<N> mentions |
gpt-image-1.5 | Low / Medium / High | 3 | OpenAI-sourced |
gpt-image-2 | 1K / 2K / 4K | 16 | GPT Image 2.0 · quality low/medium/high · @Image<N> mentions |
seedream-4.5 | 2K / 4K | 14 | @Image<N> mentions |
seedream-5.0 | 2K / 3K | 14 | @Image<N> mentions |
For the full image workflow, see the images/create endpoint and the MiniMax API overview.
Pricing
You keep your normal MiniMax / Hailuo website subscription for the underlying account — Hailuo has a free tier and paid plans — and add a single flat $15/month to useapi.net that covers API access to every supported service, with no per-generation surcharge from us. Generations draw from your MiniMax account’s own credit balance at MiniMax’s standard rates, which vary by model, resolution, and duration. The MiniMax API overview and the videos/create page carry per-model credit estimators.
This is the consumer-account route. The official MiniMax API bills per generation at developer rates on a separate developer account, while useapi.net automates the consumer account you already pay for at the website subscription price.
You can connect as many MiniMax accounts as you like, and the API load-balances every batch across them automatically — pick the account with free capacity, fall back to the next, and keep paid accounts running while free ones queue. The script below uses GET /scheduler/available to do exactly that, so there is no per-account email argument to pass.
🚀 With a paid account you can expect roughly:
- 1 generation completed within 2…3 minutes
- 10 generations in about 20…30 minutes
- 30 generations in about 60…90 minutes
Free accounts take longer, and you may need to connect several of them to get a reasonable number of generations per day.
Generate a video in two API calls
You need a useapi.net API token and a connected MiniMax account — export the token so the curl examples below run as-is:
export USEAPI_TOKEN="user:1234-..."
Generation is asynchronous — the create call returns a videoId immediately, then you poll until the video is ready.
1. Submit the job — POST https://api.useapi.net/v1/minimax/videos/create:
curl -X POST "https://api.useapi.net/v1/minimax/videos/create" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A majestic mountain landscape with snow-capped peaks and flowing rivers, camera slowly panning right",
"model": "02",
"options": "1080p-6sec"
}'
When more than one account is connected, add "account": "<account>" to pick one — otherwise the API selects an available account for you. The response returns immediately with a videoId:
{
"id": "1122334455667798899",
"videoId": "user:1234-minimax:987654321-video:998877665544332211",
"task": {
"batchID": "998877665544332211",
"videoIDs": ["1122334455667798899"]
}
}
2. Poll for the result — GET https://api.useapi.net/v1/minimax/videos/{videoId}:
curl "https://api.useapi.net/v1/minimax/videos/VIDEO_ID" \
-H "Authorization: Bearer $USEAPI_TOKEN"
The job is done when statusFinal is true. Success is status: 2 (statusLabel: "completed"); the MP4 is in downloadURL (the clean, non-watermarked master on paid accounts) with videoURL (watermarked) as a fallback:
{
"status": 2,
"statusLabel": "completed",
"statusFinal": true,
"percent": 100,
"videoURL": "<watermarked url>",
"downloadURL": "<clean url, paid accounts>",
"videoId": "user:1234-minimax:987654321-video:..."
}
A video typically finishes in 2…3 minutes depending on model, resolution, and duration. Prefer not to poll? Pass a replyUrl in the create body to receive a webhook callback when the job completes. The create call may return 412 (insufficient credits), 422 (moderated prompt), 429 (queue full — wait 10…30 s and retry). On the poll, a 404 most often means the finished video was moderated and removed. See the endpoint docs for the full list.
Image-to-video and references
To animate a still image — or pass a subject/character reference — upload it first with POST /files (raw bytes, an image Content-Type, .png / .jpeg, and .webp uploaded as image/jpeg). It returns a fileID you pass to POST /videos/create:
# 1. Upload the start frame → returns { "fileID": "user:...-file:..." }
curl -X POST "https://api.useapi.net/v1/minimax/files/?account=987654321" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: image/jpeg" \
--data-binary @first_frame.jpeg
# 2. Create the video from that frame
curl -X POST "https://api.useapi.net/v1/minimax/videos/create" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "the character turns and smiles, camera pushes in",
"model": "I2V-2.3",
"options": "1080p-6sec",
"fileID": "FILE_ID_FROM_STEP_1"
}'
Image-to-video models (I2V-01*, I2V-2.3*, S2V-01) require a fileID. On 02, Veo 3.1, Sora 2, and Seedance 2.0 the image is optional. The 02 and Veo 3.1 models also take an end_frame_fileID for a first→last transition (it must be a different upload than fileID), and the Seedance and Veo S2V models accept extra references (fileID2…) for omni and subject-reference modes. See POST /videos/create for which model accepts which reference.
You can also generate a start frame with POST /images/create and feed the returned image fileID straight into a video — a common GPT Image 1.5 → Sora 2 workflow shown in the Examples below.
Batch-generate with a script
Finding the right shot takes many attempts, and running them by hand is tedious. The Node.js script below reads a list of prompts from videos.json, uploads any reference images, submits each job load-balanced across every connected account via GET /scheduler/available, then polls and downloads every finished MP4 — so you can queue a batch and come back to the winners.
You can provide an image instead of or in addition to a text prompt. When a file is given, the prompt is optional. It often helps to ask ChatGPT, Claude, or Gemini to draft a batch of prompt variations, then keep what works.
You need Node.js v21 or newer (no dependencies to install). Put videos.json and videos.mjs in the same folder and run node ./videos.mjs API_TOKEN, where API_TOKEN is your useapi.net API token. The script discovers your connected MiniMax accounts and load-balances the batch automatically — no per-account email argument is needed.
Expand videos.json
[
{
"file": "./blonde.jpeg",
"prompt": "A beautiful blonde woman with striking blue eyes posing confidently for a magazine cover."
},
{
"prompt": "From below shot of a cat catholic priest performing an exorcism on a demonic cat, a parody on The Exorcist movie."
},
{
"model": "02",
"options": "1080p-6sec",
"prompt": "Zoom in on a cute red shiny robot holding a white banner with text 'useapi.net', Pixar animation style."
},
{
"model": "Seedance-2.0",
"resolution": "1080",
"duration": 8,
"aspectRatio": "16:9",
"prompt": "Cinematic tracking shot through a neon-lit alley at night, rain reflections on wet pavement."
}
]
Each prompt object may set an optional model plus the parameters that model supports — options (resolution/duration for Hailuo 02/2.3, Sora 2 and Veo 3.1), resolution and duration (Seedance 2.0), aspectRatio (Sora 2, Veo, Seedance), and end_frame_fileID where supported. Omit them to fall back to the Hailuo 01 defaults. See POST /videos/create for the full per-model matrix. Currently .png and .jpeg images are supported (rename a .webp to .jpeg).
Expand videos.mjs script
/*
Script version 3.0, June 15, 2026
Script to generate videos using prompts with MiniMax API by useapi.net 🚀
For more details visit https://useapi.net/docs/api-minimax-v1
Installation Instructions:
==========================
You need Node.js v21 or newer installed to run this script. Download and install Node.js from:
- Windows, macOS, Linux: https://nodejs.org/
After installation, verify by running the following commands in a terminal:
node -v
Running the Script:
===================
Usage: node videos.mjs <API_TOKEN> [PROMPTS_FILE]
Replace API_TOKEN with your actual useapi.net API token, see https://useapi.net/docs/start-here/setup-useapi
If optional PROMPTS_FILE not provided videos.json will be used.
Example #1:
--------
node videos.mjs user:1234-abcdefhijklmnopqrstuv
This command executes the script using API token user:1234-abcdefhijklmnopqrstuv
Example #2:
--------
node videos.mjs user:1234-abcdefhijklmnopqrstuv myprompts.json
This command executes the script using API token user:1234-abcdefhijklmnopqrstuv and load prompts from myprompts.json file.
Changelog:
==========
- October 23, 2024: Response code 596 handling added https://useapi.net/docs/api-minimax-v1/post-minimax-videos-create#responses.
- October 25, 2024: The param prompt is optional and no longer needed if a fileID is provided.
- November 4, 2024: Retry on 502 and 504.
- June 15, 2026: Pass through per-prompt model parameters (model, options, resolution, duration, aspectRatio, end_frame_fileID, …) so you can target Hailuo 02/2.3, Sora 2, Veo 3.1 and Seedance 2.0. Any field other than the local "file" upload path is forwarded to videos/create as-is.
*/
import readline from 'node:readline';
import fs from 'fs/promises';
import { writeFile } from 'node:fs/promises';
import { Readable } from 'node:stream';
// Constants
const RESULTS_FILE = 'videos_results.txt';
const ERRORS_FILE = 'videos_errors.txt';
const SLEEP_429 = 20 * 1000; // in milliseconds
const SLEEP_DOWNLOAD = 30 * 1000; // in milliseconds
const urlAccounts = 'https://api.useapi.net/v1/minimax/accounts';
const urlAvailable = 'https://api.useapi.net/v1/minimax/scheduler/available';
const urlCreate = 'https://api.useapi.net/v1/minimax/videos/create';
const urlDownload = 'https://api.useapi.net/v1/minimax/videos/';
const urlUploadFile = 'https://api.useapi.net/v1/minimax/files/?account=';
// To upload .webp rename it to .jpeg
const supportedFileExtensions = ['png', 'jpeg']
// account: { filename: fileID }
const uploadedFiles = {};
let availableAccountsCount = 0;
// Track accounts without any credits left
const outOfCredits = [];
// Utility to sleep for given milliseconds
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Function to fetch configured MiniMax API accounts
async function fetchAccounts(apiToken) {
const response = await fetch(urlAccounts, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
if (!response.ok) {
console.error(`⛔ Error fetching accounts (HTTP ${response.status}): ${response.statusText}`);
process.exit(1);
}
return response.json();
}
const elapsedTimeSec = (start) => (Date.now() - start) / 1000;
async function uploadFile(apiToken, account, filename) {
if (!uploadedFiles[account])
uploadedFiles[account] = {};
// Check if already uploaded for provided account
if (uploadedFiles[account].hasOwnProperty(filename))
return uploadedFiles[account][filename];
const startTime = Date.now();
console.log(`⬆️ Account ${account} uploading file…`, filename);
const body = new Blob([await fs.readFile(filename)]);
const fileExt = filename.split('.').pop();
const response = await fetch(`${urlUploadFile}${account}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`,
'Content-Type': `image/${fileExt}`
},
body
});
if (response.ok) {
const json = await response.json();
console.log(`🆗 fileID (${elapsedTimeSec(startTime)} sec)`, json.fileID);
uploadedFiles[account][filename] = json.fileID;
}
else {
console.error(`❗ Unable to upload file HTTP ${response.status} (${elapsedTimeSec(startTime)} sec)`, await response.text());
// Do not attempt to upload failed file again
uploadedFiles[account][filename] = undefined;
}
return uploadedFiles[account][filename];
}
// Function to submit a prompt.
// `fields` is the prompt object minus the local `file` upload path — it carries
// `prompt` plus any optional model parameters (model, options, resolution,
// duration, aspectRatio, end_frame_fileID, …) and is forwarded to videos/create as-is.
async function submitPrompt(apiToken, filename, promptIndex, fields) {
const { prompt } = fields;
console.log(`\n👉 Prompt #${promptIndex}: ${prompt ?? '(image only)'}`);
const availableResponse = await fetch(urlAvailable, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
const availableJSON = await availableResponse.json();
console.log(`Currently executing ${availableJSON.executing.length} generation(s).`);
const available = availableJSON.available
.filter(a => !outOfCredits.includes(a.account));
console.log(`Available accounts ${available.length}:`, available.map(a => `${a.account} (${a.available})`).join(', '));
if (available.length == 0) {
console.log(`🔄️ Waiting for currently running generations to complete …`);
return 429;
}
const account = available[0].account;
const fileID = filename ? await uploadFile(apiToken, account, filename) : undefined;
const info = `Prompt #${promptIndex} account ${account}`;
console.log(`${info} …`);
const createResponse = await fetch(urlCreate, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`
},
body: JSON.stringify({ account, fileID, ...fields })
});
const createBody = await createResponse.text();
if (createResponse.status == 200) {
const json = JSON.parse(createBody);
const videoId = json.videoId;
if (videoId) {
await fs.appendFile(RESULTS_FILE, `${videoId},${prompt}\n`);
return 200;
} else {
const error = `No videoId found in HTTP 200 response`;
console.log(`❓ ${info}: ${error}`, createBody);
await fs.appendFile(ERRORS_FILE, `${error},${prompt}\n`);
return 500;
}
} else {
let returnStatus = createResponse.status;
switch (createResponse.status) {
case 502: // Happens when MiniMax website is too busy
case 504: // Happens when MiniMax website is too busy
case 429:
console.log(`🔄️ ${info}: retry on HTTP ${createResponse.status}`);
returnStatus = 429;
break;
case 422:
console.log(`🛑 ${info}: MODERATED prompt`, createBody);
await fs.appendFile(ERRORS_FILE, `${createResponse.status},${prompt}\n`);
break;
case 412:
console.log(`🛑 ${info}: account run out of credits`, createBody);
outOfCredits.push(account);
break;
default:
console.log(`❗ ${info}: FAILED with HTTP ${createResponse.status}`, createBody);
await fs.appendFile(ERRORS_FILE, `${createResponse.status},${prompt}\n`);
}
return returnStatus;
}
}
// Function to download videos based on VIDEO IDs
async function download(apiToken) {
try {
const resultsContent = await fs.readFile(RESULTS_FILE, 'utf8');
const lines = resultsContent.trim().split('\n');
for (const line of lines) {
const [videoId, prompt] = line.split(',');
const videoFilename = `${videoId.replace(/:/g, '_')}.mp4`;
console.log(`👉 ${videoId}`);
try {
await fs.access(videoFilename);
console.log(`⚠️ ${videoFilename} already exists. Skipping download.`);
continue;
} catch {
// File does not exist, proceed with downloading
}
while (true) {
const response = await fetch(`${urlDownload}${videoId}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
if (!response.ok) {
console.log(`🛑 MODERATED ${videoId} (HTTP ${response.status}):\n${prompt}\n`, await response.text());
break;
}
const taskResponseBody = await response.json();
const { status, statusFinal, statusLabel, downloadURL, videoURL, percent } = taskResponseBody;
if (statusFinal) {
const url = downloadURL ?? videoURL;
if (url) {
console.log(`✅ Downloading ${url} to ${videoFilename}`);
try {
const videoResponse = await fetch(url);
if (!videoResponse.ok) {
console.error(`⛔ Unable to download ${videoId} (HTTP ${videoResponse.status}):\n${prompt}\n`, url);
break;
}
const stream = Readable.fromWeb(videoResponse.body);
await writeFile(videoFilename, stream);
} catch (err) {
console.error(`⛔ Error during download: ${err}`);
}
} else
console.error(`🛑 Unable to download ${videoId} status (${status} ${statusLabel}):\n${prompt}\n`);
break;
} else {
console.log(`⌛ ${videoId} status (${status} ${statusLabel}) and is still in progress (${percent}%), waiting…`);
await sleep(SLEEP_DOWNLOAD);
}
}
}
} catch (error) {
console.log(`⛔ Error during download:`, error.stack || error);
}
}
// Main function
async function main() {
const apiToken = process.argv[2];
const promptFile = process.argv[3] || 'videos.json'; // Default to 'videos.json' if not provided
if (!apiToken) {
console.error('Usage: node videos.mjs <API_TOKEN> [PROMPTS_FILE]');
process.exit(1);
}
console.log('Script v3.0');
console.log('Node version is: ' + process.version);
try {
if (await fileExists(RESULTS_FILE)) {
let user_input;
while (!['y', 'n'].includes(user_input)) {
user_input = (await promptUser(`❔ ${RESULTS_FILE} file detected. Do you want to download the results now? (y/n): `))?.toLowerCase();
if (user_input == 'y') {
await download(apiToken);
await fs.unlink(RESULTS_FILE);
}
}
}
const start = new Date();
try {
console.info('START EXECUTION', start);
await execute(apiToken, promptFile); // Pass the promptFile to execute function
}
finally {
console.info('COMPLETED', new Date());
console.info('EXECUTION ELAPSED', diffInMinutesAndSeconds(start, new Date()));
}
try {
console.info('START DOWNLOAD', start);
await download(apiToken);
}
finally {
console.info('TOTAL ELAPSED', diffInMinutesAndSeconds(start, new Date()));
}
} catch (error) {
console.error('⛔ Error during execution:', error.stack || error);
}
}
async function execute(apiToken, promptFile) {
const accounts = await fetchAccounts(apiToken);
const videoAccounts = Object.values(accounts)
.filter(a => a.supportVideo) // Accounts with video support
.filter(a => !a.error); // Active accounts without error
console.info(`Configured active MiniMax API video accounts`, videoAccounts.length);
if (videoAccounts.length <= 0) {
console.error(`⛔ No configured active video accounts found. Please refer to https://useapi.net/docs/start-here/setup-useapi`);
process.exit(1);
}
availableAccountsCount = videoAccounts.length;
const promptData = await fs.readFile(promptFile, 'utf8');
const prompts = JSON.parse(promptData);
console.log(`Total number of prompts to process`, prompts.length);
let warnings = [];
// First pass: check for warnings
for (let i = 0; i < prompts.length; i++) {
const { file, prompt } = prompts[i];
if (!prompt && !file) {
warnings.push(`⚠️ Skip empty prompt with empty file at index ${i}`);
continue;
}
if (file) {
try {
await fs.access(file);
} catch {
warnings.push(`⚠️ Specified file '${file}' does not exist. Skip prompt ${i}`);
continue;
}
const ext = file.split('.').pop();
if (!supportedFileExtensions.includes(ext)) {
warnings.push(`⚠️ File ${file} extension ${ext} not supported. Skip prompt ${i}`);
continue;
}
}
}
if (warnings.length > 0) {
warnings.forEach(warning => console.warn(warning));
console.error(`⛔ Execution stopped due to warnings.`);
process.exit(1);
}
for (let i = 0; i < prompts.length; i++) {
const { file, ...fields } = prompts[i];
while (true) {
const responseCode = await submitPrompt(apiToken, file, i + 1, fields);
if (responseCode == 429)
await sleep(SLEEP_429);
else
if (responseCode == 412) {
// Check if there's no accounts left at all
if (availableAccountsCount == outOfCredits.length) {
console.error(`⛔ All configured video accounts run out of credits`);
process.exit(1);
}
} else
if (responseCode == 596) {
console.error(`⛔ Your hailuoai.video account has been placed on hold, which may last a few hours. It may be a good idea to pause operations until then.`);
process.exit(1);
} else
break;
}
}
}
// Utility function to check if a file exists
async function fileExists(path) {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
// Function to prompt user input
async function promptUser(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => rl.question(query, answer => {
rl.close();
resolve(answer);
}));
}
function diffInMinutesAndSeconds(date1, date2) {
const diffInSeconds = Math.floor((date2 - date1) / 1000);
return `${Math.floor(diffInSeconds / 60)} minutes ${diffInSeconds % 60} seconds`;
};
main();
All generated videos download locally so you can review them once they are ready. If you would rather not read the docs, the script is all you need — but each endpoint also has a Try It console right in the browser.
Examples
The clips and images below are real generations produced through this MiniMax API, straight from our blog walkthroughs.
Hailuo 02 — text-to-video with an end frame, 02 1080p 6 sec
— from MiniMax: End Frame Parameter for Video
Hailuo 2.3 — image-to-video, I2V-2.3 768p 10 sec
— from MiniMax: 2.3 Video Generation Models
Sora 2 — image-to-video from a start frame, Sora-2 9:16
— from MiniMax: Sora, Veo, and Hailuo Models
Veo 3.1 — subject reference, Veo-3.1-S2V 16:9 with native audio
— from MiniMax: Sora 2 and Veo 3.1
Midjourney V7 — multi-reference image, midjourney-v7 4:3

— from MiniMax: Sora, Veo, and Hailuo Models
Nano Banana 2 — multi-reference image, nano-banana-2 3:4 2K

— from MiniMax: Nano Banana 2 Image Model
GPT Image 1.5 — text-to-image, gpt-image-1.5 16:9

— from 17 AI Image Models: The Showdown
Frequently asked questions
Is there a MiniMax (Hailuo) API? Yes — a few ways. MiniMax offers an official developer API, and third-party resellers expose Hailuo too, all billed per generation at developer rates. useapi.net is the consumer-account route: programmatic access to every MiniMax / Hailuo model by driving your own account at the website subscription price through a standard REST endpoint.
Which video models can I generate? MiniMax’s own Hailuo 01 (T2V-01 / I2V-01 and the Director / live / S2V-01 variants), Hailuo 02 (02), and Hailuo 2.3 (T2V-2.3, I2V-2.3, I2V-2.3-Fast), plus Sora 2 (Sora-2), Veo 3.1 (Veo-3.1, Veo-3.1-Fast, and the S2V subject-reference variants), and Seedance 2.0 (Seedance-2.0, Seedance-2.0-Fast) hosted through MiniMax — 17 video models in all. Pass the name in the model field of POST /videos/create. See Supported models above.
Can I turn an image into a video (image-to-video)? Yes. Upload the still with POST /files and pass the returned fileID to POST /videos/create with an image-to-video model such as I2V-2.3 or 02. The 02 and Veo 3.1 models also take an end_frame_fileID for a first→last transition. See Image-to-video and references above.
Can I generate images (Midjourney, Nano Banana, Seedream) too? Yes — the same account and flat $15/month drive POST /images/create: image-01, midjourney-v7 / midjourney-niji7, nano-banana-2 / nano-banana-pro, gpt-image-1.5 / gpt-image-2 (GPT Image 2.0), and seedream-4.5 / seedream-5.0. See Supported models above.
Is there a MiniMax text-to-speech or music API? MiniMax’s standalone TTS and music endpoints have been retired. Speech 2.5 and Music 2.0 now run only through the MiniMax Agent (POST /agent), where you pass the model in the models list. For a dedicated music API, use Mureka.
How much does the MiniMax API cost? You keep your normal MiniMax / Hailuo website subscription, plus a flat $15/month to useapi.net for API access to all services. Generations draw from your MiniMax account’s own credits at MiniMax’s standard rates, which vary by model, resolution, and duration — the videos/create and images/create pages carry per-model credit estimators. See Pricing above.
How do I batch-generate across multiple accounts? Connect every account once at Setup MiniMax, then run the script above — it polls GET /scheduler/available and load-balances each job onto an account with free capacity, so you never pass a per-account email. Free accounts process one video at a time, so connecting several lifts your throughput.
Conclusion
Visit our Discord Server or
Telegram Channel for any support questions and concerns.
The full runnable example is in the minimax-api GitHub repo.