Verify the model before you spend
Section titled “Verify the model before you spend”This server-side Node.js example uses the public minimax-h3 task contract, not MiniMax’s upstream API. It creates one video and incurs usage. Confirm the model is present in GET /v1/media-models, check its request_schema against the complete input, and review your account model pricing. The MiniMax H3 guide and model page explain the current contract and calculator.
The example uses 5 seconds, 768P and 16:9. These are request selections, not a fixed-pixel, speed or quality guarantee. Another model can use different names, values or combinations. Do not change only the model ID and assume the input remains valid.
Run or resume one task
Section titled “Run or resume one task”Use Node.js 24 or later. Set API_KEY in the server environment; never put it in client code or source control. Save the script as video-task.mjs and run node video-task.mjs. It saves the accepted ID to a local record before polling. Keep that record: an HTTP 202 is acceptance, not the final video.
To resume, set TASK_ID to the original ID and run the script again; this skips submission. OUTPUT_DIR optionally selects the local download directory. Existing output files are not overwritten. If a download was already completed, use a fresh directory or download only missing files rather than generating again.
import { mkdir, open, rm, writeFile } from 'node:fs/promises';import { Readable, Transform } from 'node:stream';import { pipeline } from 'node:stream/promises';import { setTimeout as delay } from 'node:timers/promises';import { join } from 'node:path';
const origin = 'https://newrouters.com';const key = process.env.API_KEY;if (!key) throw new Error('Set API_KEY on your server');const headers = { Authorization: `Bearer ${key}` };const outputDir = process.env.OUTPUT_DIR || 'video-output';let taskId = process.env.TASK_ID;
if (!taskId) { // One submission only. Never automatically retry an uncertain POST. const response = await fetch(`${origin}/v1/tasks`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'minimax-h3', input: { prompt: 'A paper boat floating on calm water, fixed camera.', seconds: 5, resolution: '768P', aspect_ratio: '16:9', }, }), signal: AbortSignal.timeout(30000), }); if (response.status !== 202) throw new Error(`Submit HTTP ${response.status}; inspect before retrying`); const accepted = await response.json(); if (typeof accepted.id !== 'string' || !/^task_[0-9A-HJKMNP-TV-Z]{26}$/.test(accepted.id)) throw new Error('Missing task ID; investigate before resubmitting'); taskId = accepted.id; console.log(`TASK_ID=${taskId}`); await writeFile(`task-${taskId}.json`, JSON.stringify({ id: taskId }), { flag: 'wx', mode: 0o600 });}if (!/^task_[0-9A-HJKMNP-TV-Z]{26}$/.test(taskId)) throw new Error('Invalid TASK_ID');
// Ten minutes is this client's polling budget, not a service completion promise.const deadline = Date.now() + 10 * 60 * 1000;let completed;while (Date.now() < deadline) { const response = await fetch(`${origin}/v1/tasks/${encodeURIComponent(taskId)}`, { headers, signal: AbortSignal.timeout(Math.max(1, Math.min(15000, deadline - Date.now()))), }); if (!response.ok) throw new Error(`Poll HTTP ${response.status}; resume with TASK_ID=${taskId}`); const task = await response.json(); if (task.status === 'failed') throw new Error(`Task failed: ${task.error?.code || 'unknown'}; inspect ${taskId}`); if (task.status === 'succeeded') { completed = task; break; } if (!['pending', 'processing'].includes(task.status)) throw new Error(`Unexpected state; inspect ${taskId}`); await delay(Math.min(3000, Math.max(0, deadline - Date.now())));}if (!completed) throw new Error(`Polling deadline reached; resume with TASK_ID=${taskId}, do not resubmit`);const assets = completed.result?.assets;if (!Array.isArray(assets) || !assets.length || assets.some(url => typeof url !== 'string')) { throw new Error(`No valid result assets; inspect ${taskId}`);}await mkdir(outputDir, { recursive: true });for (const [index, rawUrl] of assets.entries()) { const url = new URL(rawUrl); if (url.protocol !== 'https:' || url.username || url.password) throw new Error('Unexpected asset URL'); // No API authorization header is forwarded to the asset host. const response = await fetch(url, { redirect: 'error', signal: AbortSignal.timeout(60000) }); if (!response.ok || !response.body) throw new Error(`Asset HTTP ${response.status}; retry download, not generation`); const type = response.headers.get('content-type')?.split(';')[0]; const extension = type === 'video/mp4' ? 'mp4' : type === 'video/webm' ? 'webm' : 'bin'; const path = join(outputDir, `${taskId}-${index}.${extension}`); const file = await open(path, 'wx', 0o600); // Never overwrite an existing output. let bytes = 0; const limit = new Transform({ transform(chunk, _encoding, callback) { bytes += chunk.length; callback(bytes > 100 * 1024 * 1024 ? new Error('Asset exceeds local 100 MiB limit') : null, chunk); } }); try { await pipeline(Readable.fromWeb(response.body), limit, file.createWriteStream()); } catch (error) { await file.close(); await rm(path, { force: true }); throw error; } console.log(`Saved ${path}`);}Know what a timeout means
Section titled “Know what a timeout means”pending and processing are non-final states; succeeded has outputs and failed has an error. Polling runs against the same task ID. The 30-second submission timeout, 10-minute polling deadline, 3-second interval and 100 MiB download cap are local example policies, not platform limits or service-level promises.
A timeout or connection loss during POST can leave the outcome unknown. Stop and inspect task history before submitting anything else. If the ID was returned but saving the local record failed, keep the printed ID and resume with TASK_ID. A polling deadline does not cancel generation or prove that no charge occurred. Read-only polling errors stop this example; investigate the status and resume the same ID when appropriate.
Save the output safely
Section titled “Save the output safely”The public response uses result.assets as an array of URL strings. Download only after succeeded. The script streams each file with a size cap, does not forward API credentials, rejects redirects and removes a partially written file on download failure. Unknown content types get .bin; inspect them before use. Generated asset links are not a durable archive: keep files you are authorized to retain.
For a production downloader, restrict asset destinations using your deployment’s trusted media hosts and enforce outbound network policy. Do not turn arbitrary user URLs into a server-side download endpoint. A failed download is not a reason to recreate a paid video task.
Review motion, composition and subject consistency before accepting the clip. Reconcile task charges using the batch budget guide; return to the media learning path for input and workflow choices.