An accepted image-generation request is not a finished image. Your application needs to keep the task ID, check the same task, and handle a failed or interrupted request without accidentally starting another job.
This tutorial builds a small server-side Node.js client for that sequence. It has separate submit and resume commands: one creates a task, while the other only checks an existing task. Start with the supported API model IDs, not a model name copied from a homepage banner.
The tutorial covers text-to-image generation. It does not upload reference files, implement a browser interface, or measure model quality. Validation combines 29 local mock tests with one controlled production image task on September 4, 2026. That task returned one image and recorded a 120-point API charge. This is one dated integration check, not a speed benchmark, a universal price, or a guarantee of availability for another account.
The cover is a model-catalog screenshot captured on September 4, 2026. Treat it as a dated illustration, not a current price quote or guarantee of availability.
Key Takeaways
- Keep the API key on your server, outside browser bundles and public repositories.
- For an image task, read the numeric
data.taskIdfrom the accepted response and keep it for later queries.- Poll the saved task until
readyorfailed. Read image URLs fromdata.imagesonly after success.- A client timeout does not establish that the remote task failed or was canceled. Resume queries when you have a task ID; do not blindly repeat the creation request.
In this guide
- Before you start
- Choose a model and configure your API key
- Submit with cURL
- Poll and read image URLs
- Complete Node.js example
- Handle errors
- Video and chat differences
- Verify your integration
Before you start
Use a private terminal or server environment with Node.js, an API key, and an account configured for API billing. The accompanying example uses built-in fetch and JavaScript modules, with no third-party package installation. Node's global API documentation describes fetch and AbortSignal.timeout, which the client uses to limit an individual request. Its local validation environment is Windows with Node.js v22.22.1; that is a test-environment record, not a claim that every other version has been tested.
You should be comfortable editing a file, setting an environment variable, and reading JSON. Check your installed runtime before continuing:
node --version
The command should print your installed version. It does not contact the generation API. Save the complete client from the later section as examples/pixmind-image.mjs inside a working folder, then run its commands from that folder.
Choose a harmless test prompt without customer data or confidential material. This tutorial uses a ceramic coffee cup on a studio background, so it needs no reference upload. Review the selected model's current API price before submitting. Studio credits, subscriptions, and API billing should not be assumed to be interchangeable; the model catalog explains the applicable API offering.
If you only want to make an image interactively, the Image Agent guide describes that workflow. The client here is for an application that explicitly supplies a model ID and handles the response itself.
Choose a model and configure your API key
Use a model ID supported by the API and parameters supported by that model. The example selects nano-banana-pro, with aspectRatio: "1:1" and resolution: "1K", matching the image request in the API Quickstart. An authenticated GET /models and the production test confirmed that combination for the test account on September 4, 2026. Recheck availability and parameter support for your own account before a live run. Changing only the model string is not enough to guarantee that the rest of a request remains valid.
Create a key in the API console, then provide it to the server process as PIXMIND_API_KEY. Use your private secret-management mechanism where available. The following examples show environment-variable syntax with a placeholder, not a working credential.
For PowerShell:
$env:PIXMIND_API_KEY = "REPLACE_WITH_YOUR_PRIVATE_API_KEY"
$env:PIXMIND_MODEL = "nano-banana-pro"
$env:PIXMIND_PROMPT = "A ceramic coffee cup on a plain studio background, soft side lighting, no text"
For Bash:
export PIXMIND_API_KEY="REPLACE_WITH_YOUR_PRIVATE_API_KEY"
export PIXMIND_MODEL="nano-banana-pro"
export PIXMIND_PROMPT="A ceramic coffee cup on a plain studio background, soft side lighting, no text"
These assignments configure the current shell and processes launched from it. They do not create a task. Protect shell history and recordings when entering a real secret, and never commit that secret to source control. Follow the authentication guidance for separate development and production keys and rotation after exposure.
The base URL for this tutorial is:
https://aihub-admin.aimix.pro/api-platform/v1
Append /generations or /tasks/{taskId} to that base. Do not append a second /v1. Model discovery is available through GET /models; the catalog is the more convenient place to read model-specific capabilities before choosing parameters.
Submit your first image request with cURL
The cURL request is an alternative to the Node.js submit command, not a setup step you must run first. Both submit a generation request. Running both can create two billable tasks. If you submit with cURL, use the Node.js resume command afterward with the returned task ID.
This example uses Bash syntax. In PowerShell, use the Node.js client below instead of pasting Bash line continuations into the terminal.
curl --connect-timeout 10 --max-time 30 \
--request POST \
'https://aihub-admin.aimix.pro/api-platform/v1/generations' \
--header "Authorization: Bearer $PIXMIND_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"model": "nano-banana-pro",
"type": "image",
"prompt": "A ceramic coffee cup on a plain studio background, soft side lighting, no text",
"aspectRatio": "1:1",
"resolution": "1K"
}'
The prompt and parameters in this cURL body are literal values. Changing PIXMIND_PROMPT does not change this JSON; that environment variable is read by the Node.js client. This distinction helps keep the request you intend to send separate from the shell configuration around it.
An abbreviated illustrative accepted response is shown below. Its ID is a placeholder, not the production test's ID; 12345 is not a task you should query.
{
"code": 1000,
"message": "success",
"data": {
"id": "img_12345",
"taskId": 12345
}
}
The tested production media responses wrapped successful data in an object with code, message, data, and timestamp; the illustration omits the timestamp and other fields. For this workflow, success requires both a successful HTTP response and a valid application response. Read data.taskId, not data.id: the latter may contain a prefixed string such as img_12345, while the task-query path uses the numeric ID.
Save that numeric ID immediately. If the request times out before you receive it, stop and reconcile the submission with your account's task records or support. Repeating the POST is not a safe substitute for finding out what happened.
Poll the task and read the image URLs
Query the existing task until it reaches a terminal state. The Async Tasks documentation describes pending, processing, ready, and failed. Only the latter two end this polling workflow.
Submit once
|
Save numeric taskId
|
GET /tasks/{taskId} <--- wait, then query again
| ^
+--- pending / processing ---+
|
+--- ready ---> read images, stop
|
+--- failed ---> report failure, stop
For a manual query in Bash, replace the placeholder ID with your own:
TASK_ID="REPLACE_WITH_YOUR_NUMERIC_TASK_ID"
curl --connect-timeout 10 --max-time 30 \
"https://aihub-admin.aimix.pro/api-platform/v1/tasks/$TASK_ID" \
--header "Authorization: Bearer $PIXMIND_API_KEY"
In the current media response contract, the fields you need have different jobs:
| Field | Meaning for this client |
|---|---|
data.taskId |
Numeric identifier used to query the existing task |
data.status |
Whether to keep waiting, read the result, or stop on failure |
data.images |
Array of image output URLs, used after ready |
data.videoUrl |
Video output field, not the image result array |
A ready response with no usable image URLs is not a successful image result for your application. Surface that mismatch with the task ID so it can be investigated. Do not substitute an example URL or report that an image was downloaded.
The client prints returned image URLs; it does not fetch or archive the image files. If your application needs durable storage, design that as a separate step and confirm the relevant asset-retention and usage terms. Do not infer a permanent storage guarantee from the presence of a URL.
Run the complete Node.js example
Use submit for one new task, or resume for an existing task. Running the script without arguments prints usage and makes no API call. That default prevents an ordinary restart or a quick inspection of the CLI from creating a fresh job.
Save the full source below as examples/pixmind-image.mjs:
Complete Node.js source: pixmind-image.mjs
import { pathToFileURL } from 'node:url';
const BASE_URL = 'https://aihub-admin.aimix.pro/api-platform/v1';
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
export class ApiError extends Error {
constructor(message, { status = 0, transient = false, retryAfterMs = 0 } = {}) {
super(message);
Object.assign(this, { status, transient, retryAfterMs });
}
}
export function parseTaskId(value) {
if (!/^\d+$/.test(String(value))) throw new Error('Use a numeric taskId, not img_...');
const id = Number(value);
if (!Number.isSafeInteger(id) || id <= 0) throw new Error('Invalid taskId');
return id;
}
export function retryAfter(value, now) {
if (!value) return 0;
if (/^\d+(\.\d+)?$/.test(value)) return Number(value) * 1000;
const date = Date.parse(value);
return Number.isFinite(date) ? Math.max(0, date - now) : 0;
}
export function createClient({
apiKey,
fetchImpl = fetch,
now = Date.now,
wait = sleep,
random = Math.random,
requestTimeoutMs = 30_000,
pollTimeoutMs = 300_000,
maxConsecutiveErrors = 5,
} = {}) {
if (typeof apiKey !== 'string' || !apiKey.trim()) throw new Error('Set PIXMIND_API_KEY');
async function request(path, method, payload, timeoutMs = requestTimeoutMs) {
let response;
let body;
try {
response = await fetchImpl(`${BASE_URL}${path}`, {
method,
redirect: 'error',
headers: {
Authorization: `Bearer ${apiKey}`,
...(payload ? { 'Content-Type': 'application/json' } : {}),
},
...(payload ? { body: JSON.stringify(payload) } : {}),
signal: AbortSignal.timeout(Math.max(1, Math.ceil(timeoutMs))),
});
// Read the body inside the timeout/network error boundary as well.
body = await response.text();
} catch {
throw new ApiError('Network error or request timeout', { transient: true });
}
let envelope;
try { envelope = JSON.parse(body); } catch { /* handle below */ }
const transient = response.status === 429 || response.status >= 500;
if (!response.ok) {
// Do not echo arbitrary server bodies, prompts, credentials, or output URLs.
throw new ApiError(`HTTP ${response.status}; inspect the account and request`, {
status: response.status,
transient,
retryAfterMs: retryAfter(response.headers.get('retry-after'), now()),
});
}
if (!envelope || typeof envelope !== 'object') {
throw new ApiError('Expected a JSON API response');
}
if (envelope.code !== 1000 || !envelope.data) {
throw new ApiError('API returned an unsuccessful or incomplete envelope');
}
return envelope.data;
}
async function submit({ model = 'nano-banana-pro', prompt } = {}) {
if (typeof model !== 'string' || !model.trim()) throw new Error('A model is required');
if (typeof prompt !== 'string' || !prompt.trim()) throw new Error('A prompt is required');
try {
const data = await request('/generations', 'POST', {
model, type: 'image', prompt, aspectRatio: '1:1', resolution: '1K',
});
// The public media contract returns a number, not the prefixed display ID.
if (typeof data.taskId !== 'number') throw new Error('Missing numeric taskId');
return parseTaskId(data.taskId);
} catch (error) {
// A timeout or malformed response does not prove that creation failed.
throw new Error(`Submission not confirmed: ${error.message}. No automatic retry was made. Check task records before submitting again.`);
}
}
async function poll(taskId) {
const id = parseTaskId(taskId);
const deadline = now() + pollTimeoutMs;
let attempts = 0;
let errors = 0;
const timedOut = () => new Error(`Stopped waiting for task ${id}; it may still be running. Resume this ID later.`);
while (now() < deadline) {
let retryFloor = 0;
let task;
try {
task = await request(`/tasks/${id}`, 'GET', undefined,
Math.min(requestTimeoutMs, deadline - now()));
errors = 0;
} catch (error) {
if (now() >= deadline) throw timedOut();
if (!(error instanceof ApiError) || !error.transient) throw error;
errors += 1;
if (errors >= maxConsecutiveErrors) {
throw new Error(`Stopped after ${errors} consecutive query errors for task ${id}; resume this ID later.`);
}
retryFloor = error.retryAfterMs;
}
if (now() >= deadline) throw timedOut();
if (task) {
if (task.taskId !== id) throw new Error('Task response ID does not match the requested task');
if (task.status === 'failed') throw new Error(`Task ${id} failed; inspect its record before creating another task.`);
if (task.status === 'ready') {
if (!Array.isArray(task.images) || task.images.length === 0 ||
!task.images.every(url => {
try { return ['https:', 'http:'].includes(new URL(url).protocol); }
catch { return false; }
})) throw new Error(`Task ${id} is ready but has no valid image URLs`);
return task.images;
}
if (!['pending', 'processing'].includes(task.status)) {
throw new Error(`Task ${id} returned an unrecognized status; inspect its record.`);
}
}
// Client policy, not a PixMind latency guarantee or server-side retry feature.
const ceiling = Math.min(10_000, 1_000 * 2 ** Math.min(attempts++, 4));
const delay = Math.max(retryFloor, ceiling * (0.5 + 0.5 * random()));
const remaining = deadline - now();
if (delay >= remaining) {
// Never poll earlier than Retry-After just to fit the local deadline.
await wait(Math.max(0, remaining));
throw timedOut();
}
await wait(delay);
}
throw timedOut();
}
return { submit, poll };
}
export async function main(args = process.argv.slice(2), env = process.env, deps = {}) {
const log = deps.log ?? console.log;
const [mode, rawId] = args;
if (!mode) {
log('Usage: node examples/pixmind-image.mjs submit | resume TASK_ID');
return;
}
if (!((mode === 'submit' && args.length === 1) || (mode === 'resume' && args.length === 2))) {
throw new Error('Use submit, or resume followed by a numeric taskId');
}
const resumeId = mode === 'resume' ? parseTaskId(rawId) : undefined;
const client = createClient({ ...deps, apiKey: env.PIXMIND_API_KEY });
const id = resumeId ?? await client.submit({
model: env.PIXMIND_MODEL || 'nano-banana-pro',
prompt: env.PIXMIND_PROMPT || 'A studio photograph of an unbranded ceramic coffee cup on a plain background',
});
// Save this line in your application record before relying on the polling process.
log(`TASK_ID=${id}`);
log(`Resume without a new generation: node examples/pixmind-image.mjs resume ${id}`);
const images = await client.poll(id);
log(JSON.stringify({ taskId: id, images }, null, 2));
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(error => { console.error(error.message); process.exitCode = 1; });
}
First inspect the command interface without submitting anything:
node examples/pixmind-image.mjs
After checking the key, model parameters, and expected charge, create one task:
node examples/pixmind-image.mjs submit
When creation succeeds, the client prints TASK_ID immediately, before waiting for the image. Copy the actual value somewhere private. This example does not persist it to a file or database, so closing the terminal can lose your recovery reference if you have not saved the output.
To continue querying a task, replace 12345 with that saved value:
node examples/pixmind-image.mjs resume 12345
This command performs task GET requests only. It does not read a new prompt and create another image. A successful end state should produce output URLs for your own task; no fabricated production output is supplied here as a benchmark.
The example uses a 30-second limit per HTTP request and a five-minute polling deadline. GET retries use exponential backoff with jitter, a 10-second local backoff cap, and a valid Retry-After response header when applicable. A longer server-requested wait takes precedence; if it cannot fit within the local deadline, the client stops without querying early. The fifth consecutive transient GET error ends the attempt, allowing four retries in that error streak. These values are client settings, not a service-level agreement or a promise that every image finishes within five minutes.
Choose a total waiting policy suited to your application. A long-running job may outlive a terminal session or web request. In a deployed service, store the task ID with your own job record and expose a separate status endpoint to the browser instead of making a user keep one connection open.
Handle errors without duplicating paid work
Treat creation uncertainty differently from a failed status query. A task-query GET can be repeated without creating another generation. A creation POST whose response was lost may already have started work, so the example never automatically resends it.
| Observation | Next action |
|---|---|
HTTP 400 |
Check the request shape, model ID, and supported parameters before another deliberate attempt. |
HTTP 401 or 403 |
Check authentication and access. Do not put the key into logs while debugging. |
HTTP 404 for a task query |
Check the saved numeric ID and account that owns the task. Do not substitute someone else's task ID. |
HTTP 429 on a task GET |
Wait according to a valid Retry-After header and the client's bounded retry policy. |
| Network error or transient server error on a task GET | Retry the same query with backoff, stopping at the configured limits. |
Task status failed |
Stop querying and report the task failure. A fresh generation requires a separate decision. |
Creation timeout, invalid JSON, or missing taskId |
Treat the creation result as unknown. Check task records before submitting again. |
Do not assume that all errors share one response shape. In the September 4 production check, GET /models without authentication returned HTTP 401 with only code and message; authenticated successful media responses also included data and timestamp. This tutorial therefore does not promise a requestId, retryable flag, or price field on every response. The other error cases above describe client handling, not a claim that each was reproduced against production. Keep the HTTP status and a sanitized application message when present; treat malformed bodies as errors rather than crashing inside JSON.parse.
A polling timeout means the client stopped waiting, not that the remote task was canceled. Keep the ID and resume later. An unfamiliar status is not evidence of success either: this client stops and reports it for inspection. Never fall through to "completed" because a value was absent from a switch statement.
For troubleshooting, retain the task ID, model ID, operation, HTTP status, and approximate time. Avoid recording authorization headers or full request bodies containing private prompts. Check whether diagnostic response bodies or output URLs contain sensitive information before sharing them.
Adapt the pattern for video and chat
Video can use the same submit-and-poll idea, but it needs its own request validation and result reader. Check the selected video model's required input media, duration, resolution, and audio options instead of reusing the image body's fields by assumption. A completed video uses data.videoUrl, not data.images.
The Video Agent guide explains an interactive creative workflow. It can help you define the intended shot before you automate generation, but an Agent feature is not proof that an identically named API parameter exists.
Chat is a separate integration path. The documented /chat/completions route is OpenAI-compatible and returns a chat response, or a stream when streaming is requested. Do not feed that response into the image client's data.taskId parser. The same base URL does not imply the same response structure for every endpoint.
Keep the image client narrow until that path is verified. Add a video adapter or chat client separately, with tests for its own response shape. That separation is easier to reason about than one function that guesses whether a returned object is an image task, video task, or chat message.
Verify your integration and choose the next step
Local mock tests check client behavior without spending API balance. The accompanying suite passed 29 tests in the stated Windows and Node.js environment, including the one-POST submission path, GET-only resume, malformed responses, terminal failure, deadlines, and retry limits. Save the accompanying test file beside the client source, then run the suite from the tutorial folder:
node --test examples/pixmind-image.test.mjs
Mock tests do not establish production availability or billing. A separate controlled check on September 4, 2026 used the same client with the following recorded scope:
| Test item | Observed result |
|---|---|
| Request | One POST /generations, using nano-banana-pro, type: "image", aspectRatio: "1:1", and resolution: "1K" |
| Prompt | A studio photograph of an unbranded ceramic coffee cup on a plain background |
| Recovery reference | Numeric task ID 64114, retained before polling; this is an evidence reference, not an ID for readers to query |
| Completion | Eight GET queries of that same task; final status ready, with one URL in data.images |
| Output inspection | The returned image loaded at 1024 × 1024 pixels and visibly showed a ceramic cup on a plain background |
| Billing | The API pricing response quoted 120 points for this configuration; the task-linked API ledger recorded a 120-point debit |
The check used one existing project API key, a non-sensitive prompt, no reference uploads, and no automatic creation retry. The ledger entry was matched to the task ID rather than inferred from a Studio balance display. Only this one configuration and successful image path were tested live; video, chat, failure billing, refunds, and repeat-run reliability were not. Recheck the price before your own submission rather than treating this historical charge as a standing quote.
Before connecting the example to a real user-facing workflow, check that:
- The key stays on the server and is absent from client bundles, screenshots, and public logs.
- The selected model and every parameter are supported by the current API offering.
- An accepted submission leaves you with a recoverable numeric task ID.
resumemakes no creation POST, including after transient query errors.- A task failure, empty result, and local timeout produce different diagnostic outcomes.
- A lost creation response does not trigger an automatic second submission.
For your own controlled live check, record the account, model, approved cost scope, sanitized request, task ID, terminal response, and actual billing entry. Inspect the output separately from checking that its URL exists. Do not run repeated "smoke tests" without considering that each new submission may create paid work.
When ready, create an API key and choose one submission method. Build from the saved-task workflow before adding queues, uploads, or batch processing. If your next step is integrating generated assets into a creative review process, the Canvas product-ad workflow provides a separate, human-driven example; it is not a promise that Canvas projects can be executed through this API.
Editorial ownership: PixMind Editorial Team is the organizational byline for this tutorial. Its evidence basis is official documentation, the project's media-controller implementation, 29 local mock tests, and the single production check described above, all reviewed on September 4, 2026. Sanitized task and billing records are retained for editorial verification. No individual engineer's credentials, comparative model-quality result, or performance benchmark are claimed.



