Generate an image with Node.js

A dependency-free Node.js example with native fetch, bounded polling, explicit failures and no automatic POST retries.

Technical review:

1. Keep generation on your server

Use Node.js 22 or later and save the downloadable file as maxapi-image.mjs. It uses built-in fetch and ES modules, with no SDK installation. Set MAXAPI_KEY in the process environment. In a website integration, your browser should call your own authenticated backend, and that backend should call MaxAPI. Never put the key in a NEXT_PUBLIC_ variable, static HTML, client JavaScript or a URL query string.

node maxapi-image.mjs

2. Understand the request contract

The example submits a single 1K image task using google/gemini-3.1-flash-lite-image. runImage is also exported so a backend worker can supply its own payload. Preserve model-specific size and quality rules when replacing the model: a parameter accepted by a Gemini image route is not automatically valid for GPT Image. Check the selected model API tab before changing the payload. Leaving route_mode unset does not override a route pinned to your API key.

import { runImage, examplePayload } from "./maxapi-image.mjs";
const result = await runImage({
  key: process.env.MAXAPI_KEY,
  payload: { ...examplePayload, prompt: "A red ceramic bowl on a cream background." },
  onTask: task => console.error("Save task:", JSON.stringify(task)),
});

3. Handle timeouts without duplicate work

AbortSignal.timeout bounds each fetch, including response-body reading, to 30 seconds by default. Polling is limited separately to 120 checks, spaced five seconds apart; this is not a strict overall ten-minute deadline. An HTTP error or failed task throws instead of being returned as a successful image. Do not wrap runImage in a generic retry loop: a lost POST response does not prove the job was never created. Persist the ID and poll_url through onTask, then resume by passing pollURL or setting MAXAPI_POLL_URL.

4. Keep credentials on the API origin

The client accepts polling URLs only on the configured API origin and under the image task paths. It rejects URLs containing credentials or unexpected parameters and refuses redirects. These checks prevent a task response from forwarding your key to a different host. The final output contains URLs, not downloaded image bytes. Download assets separately without the bearer key; validate your own download policy and keep image URLs out of public logs.

Download the complete example

maxapi-image.mjs

maxapi-image.mjs
// Node.js 22+. No dependencies. Server-side only; never expose MAXAPI_KEY in a browser.
// Tested against a local fixture, not a paid upstream model.
import { pathToFileURL } from 'node:url';
const ACTIVE = new Set(['submitted', 'queued', 'pending', 'running', 'processing']);
export const examplePayload = {
  model: 'google/gemini-3.1-flash-lite-image',
  prompt: 'A single blue ceramic cup on a plain cream background, soft studio light.',
  resolution: '1K', aspect_ratio: '1:1', n: 1, async: true,
};
export function safeURL(base, path) {
  const origin = new URL(base), url = new URL(path, origin);
  if (origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password ||
      !(origin.protocol === 'https:' || (origin.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(origin.hostname))) ||
      url.origin !== origin.origin || url.username || url.password || url.hash ||
      !/^\/v1\/images\/(generations|edits)(\/[^/?]+)?$/.test(url.pathname) || url.search) {
    throw new Error('Unsafe API or polling URL; no credentials sent.');
  }
  return url.href;
}
export async function runImage({ key, payload = examplePayload, base = 'https://api.maxapi.dev', pollURL,
  intervalMs = 5000, maxPolls = 120, requestTimeoutMs = 30000, onTask = () => {} }) {
  if (!key) throw new Error('Set MAXAPI_KEY in your server environment.');
  if (!Number.isInteger(maxPolls) || maxPolls < 1 || maxPolls > 720 || !Number.isFinite(intervalMs) || intervalMs < 0 ||
      !Number.isInteger(requestTimeoutMs) || requestTimeoutMs < 1) throw new Error('Invalid polling limits.');
  async function request(path, body) {
    let response;
    try {
      response = await fetch(safeURL(base, path), { method: body ? 'POST' : 'GET', redirect: 'error',
        headers: { Authorization: `Bearer ${key}`, ...(body ? { 'Content-Type': 'application/json' } : {}) },
        body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(requestTimeoutMs) });
      let result;
      try { result = await response.json(); } catch { throw new Error(`HTTP ${response.status}: non-JSON response; inspect request history before resubmitting.`); }
      if (!result || typeof result !== 'object' || Array.isArray(result)) throw new Error('Invalid JSON response object.');
      if (!response.ok || result.error) {
        const code = /^[a-zA-Z0-9_-]{1,80}$/.test(result.error?.code ?? '') ? result.error.code : 'api_error';
        throw new Error(`HTTP ${response.status}: ${code}; no automatic retry.`);
      }
      return result;
    } catch (error) {
      if (!response) throw new Error('Network, URL, redirect or timeout error; outcome may be unknown. No automatic resubmission.', { cause: error });
      throw error;
    }
  }
  // A saved polling URL resumes the original task without submitting another generation.
  let current = pollURL ? await request(pollURL) : await request('/v1/images/generations', payload);
  let path = pollURL ?? current.poll_url;
  if (path) { path = safeURL(base, path); onTask({ id: current.id, poll_url: path }); }
  for (let count = 0; ; count++) {
    if (current.status === 'succeeded' || (!current.status && Array.isArray(current.data) && current.data.length)) return current;
    if (!ACTIVE.has(current.status)) throw new Error('Task failed, cancelled or returned an unknown state. Check task history; no resubmission.');
    if (!path) throw new Error('Pending task has no polling URL. Check history; do not resubmit automatically.');
    if (count >= maxPolls) throw new Error('Polling limit reached; task was NOT cancelled. Resume using the saved polling URL.');
    await new Promise(resolve => setTimeout(resolve, intervalMs));
    current = await request(path);
  }
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  try {
    const result = await runImage({ key: process.env.MAXAPI_KEY, pollURL: process.env.MAXAPI_POLL_URL,
      onTask: task => console.error('Save to resume:', JSON.stringify(task)) });
    // Do not print base64, prompts, the API key, or the entire response into shared logs.
    console.log(JSON.stringify({ id: result.id, status: result.status ?? 'succeeded', urls: (result.data ?? []).flatMap(item => item.url ? [item.url] : []) }, null, 2));
  } catch (error) { console.error(error.message); process.exitCode = 1; }
}

Model-specific parameters

Public configuration snapshot; not live availability. Follow each model page for its parameters, supported routes and reference limits.

References

Continue your integration

Manage API keysAPI referenceCheck current prices