Async Image Generation API Guide

A practical guide to submitting asynchronous image tasks, polling status, downloading results, handling failures, and scaling image-generation workers safely.

Purpose and definition of done

This guide is for developers building a backend, CLI, scheduled job, or automation pipeline around the GPT88 image API. It is not a prompt-writing course; it focuses on reliable task orchestration.

You are done when all of the following are true:

  • The submit response produces a task ID that your service persists.
  • Polling can distinguish processing from success and failure.
  • A successful task produces a downloaded file or a deliberately stored result URL.
  • A timeout or transient error can resume with the same task ID without blindly generating again.
  • Usage and failures can be audited later with the task ID and request metadata.

Core concepts

TermMeaningImplementation rule
SubmitSend the image intent and generation parameters.Persist the raw response and task ID before doing anything else.
Task IDThe stable handle for later status queries.Use task_id or id from the response; never synthesize one locally.
PollingRepeatedly query the task until it reaches a terminal state.Use a bounded interval, maximum attempts, and backoff for transient errors.
Terminal stateA final success, failure, or cancellation state.Do not stop just because progress reaches 100.
ResultThe final image URL or base64 payload.Download or decode immediately, then store your own durable copy.
Task recordYour local persistence for orchestration and audit.Store model, prompt hash, timestamps, attempts, status, and error.

Prerequisites

  • A GPT88 API key with access to an image model.
  • One of: cURL plus jq, Node.js 20+, or Python 3.10+ with requests.
  • A writable local directory or object-storage target for the generated image.
  • A known prompt and a small test size. Use one image first.

Shortest successful path

  1. Set the API key and choose a small, single-image request.
  2. Submit with async mode and save the returned task ID.
  3. Poll every five seconds, up to a bounded maximum.
  4. On terminal success, extract the result URL or base64 payload.
  5. Download or decode it, inspect the file, and record the winning request parameters.

1. Submit an image task

POSThttps://img.gpt88.cc/v1/images/generations
  • modelstringRequired
    Image model ID, such as gpt-image-2; confirm availability with the current model list or console.
  • promptstringRequired
    The image intent and constraints. Keep the prompt stable while diagnosing polling behavior.
  • sizestring
    Use a size supported by the selected model. Start small while validating the workflow.
  • qualitystring
    Quality preset supported by the selected model. Higher quality may take longer.
  • ninteger
    Number of images. Start with 1; increase only after one task is reliable.
  • asyncboolean
    The examples use true to request asynchronous task handling. If a model exposes a different async switch, follow its live contract.
submit-async-image.shbash
export GPT88_API_KEY="YOUR_GPT88_API_KEY"
export BASE_URL="https://img.gpt88.cc"

curl -sS -X POST "$BASE_URL/v1/images/generations" \
  -H "Authorization: Bearer $GPT88_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-image-2",
    "prompt": "A premium ecommerce hero image of a glass skincare bottle on a white stone counter, soft morning light, clean luxury composition, no text, no watermark",
    "size": "1024x1024",
    "quality": "high",
    "n": 1,
    "async": true
  }' | tee create.json

# Accept task_id or id, including a nested data envelope.
TASK_ID=$(jq -r '.task_id // .id // .data.task_id // .data.id // empty' create.json)
test -n "$TASK_ID" || { echo "No task ID returned:"; cat create.json; exit 1; }
echo "created task: $TASK_ID"

Verify the step before polling:

  • create.json exists and contains the raw response.
  • TASK_ID is non-empty.
  • You have recorded the model, prompt hash, requested size, and submission timestamp.

2. Poll until a terminal state

GEThttps://img.gpt88.cc/v1/images/generations/{task_id}

Polling is a control loop, not a tight retry loop. Five seconds is a reasonable starting point for a small test; increase it for high-volume workers or when the live service recommends a different interval.

poll-async-image.shbash
MAX_ATTEMPTS=60
INTERVAL_SECONDS=5

for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
  curl -sS "$BASE_URL/v1/images/generations/$TASK_ID" \
    -H "Authorization: Bearer $GPT88_API_KEY" > status.json

  STATUS=$(jq -r '.data.status // .status // .task.status // empty' status.json | tr '[:upper:]' '[:lower:]')
  echo "attempt=$attempt status=$STATUS"

  case "$STATUS" in
    succeeded|success|completed)
      RESULT_URL=$(jq -r '.data.result_url // .result_url // .data.result.data[0].url // .result.data[0].url // .data[0].url // empty' status.json)
      if [ -n "$RESULT_URL" ]; then
        curl -L "$RESULT_URL" -o async-image.png
        echo "saved async-image.png"
        exit 0
      fi
      echo "Task succeeded but no URL was found; inspect status.json for b64_json or a model-specific result." >&2
      exit 2
      ;;
    failed|failure|cancelled|canceled)
      echo "Image task failed:" >&2
      cat status.json >&2
      exit 3
      ;;
  esac

  sleep "$INTERVAL_SECONDS"
done

echo "Polling timed out. Keep TASK_ID=$TASK_ID and resume later." >&2
exit 4
State familyWhat to doWhat not to do
queued / submittedKeep the task ID and poll again.Do not resubmit just because no image exists yet.
processing / in_progressContinue polling and update last_polled_at.Do not treat an empty result URL as failure.
succeeded / success / completedExtract URL or base64, then download or decode.Do not mark success if the result payload is empty.
failed / failurePersist the error and decide whether a parameter change is needed.Do not retry the identical request forever.
cancelled / canceledRecord cancellation and decide whether the user wants a new task.Do not continue polling indefinitely.

3. Save the image result

Prefer downloading a URL into storage you control. If the response only provides b64_json, decode it on the server and write the bytes to a file or object store. Treat a provider URL as a delivery address, not your permanent asset database.

download-result.shbash
RESULT_URL=$(jq -r '.data.result_url // .result_url // .data.result.data[0].url // .result.data[0].url // .data[0].url // empty' status.json)
if [ -n "$RESULT_URL" ]; then
  curl -L "$RESULT_URL" -o async-image.png
else
  B64=$(jq -r '.data.result.data[0].b64_json // .data[0].b64_json // empty' status.json)
  test -n "$B64" || { echo "No URL or base64 image found" >&2; exit 1; }
  printf '%s' "$B64" | base64 -d > async-image.png
fi
file async-image.png

Verify the artifact, not only the HTTP status:

  • The file exists and has a non-zero size.
  • The MIME type and extension agree with the decoded content.
  • The dimensions and crop match the requested output.
  • The file opens in an image viewer and is not an error page saved as .png.

Node.js and Python clients

The following clients deliberately parse several common envelopes. This is useful while an API rollout or model family may return slightly different wrappers; once your production model is fixed, add strict schema validation around the fields you depend on.

async-image.tstypescript
const BASE_URL = "https://img.gpt88.cc";
const API_KEY = process.env.GPT88_API_KEY;

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const first = (...values) => values.find(value => value !== undefined && value !== null && value !== "");

function taskIdOf(payload) {
  return first(payload.task_id, payload.id, payload.data?.task_id, payload.data?.id);
}

function statusOf(payload) {
  return String(first(payload.data?.status, payload.status, payload.task?.status, "")).toLowerCase();
}

function resultOf(payload) {
  return first(
    payload.data?.result_url,
    payload.result_url,
    payload.data?.result?.data?.[0]?.url,
    payload.result?.data?.[0]?.url,
    payload.data?.[0]?.url,
    payload.data?.result?.data?.[0]?.b64_json,
    payload.data?.[0]?.b64_json,
  );
}

async function generateAsyncImage(prompt) {
  const createResponse = await fetch(BASE_URL + "/v1/images/generations", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gpt-image-2",
      prompt,
      size: "1024x1024",
      quality: "high",
      n: 1,
      async: true,
    }),
  });

  const created = await createResponse.json();
  if (!createResponse.ok) throw new Error("Submit failed: " + JSON.stringify(created));

  const taskId = taskIdOf(created);
  if (!taskId) throw new Error("No task ID returned: " + JSON.stringify(created));

  for (let attempt = 1; attempt <= 60; attempt += 1) {
    await sleep(5000);
    const pollResponse = await fetch(BASE_URL + "/v1/images/generations/" + encodeURIComponent(taskId), {
      headers: { Authorization: "Bearer " + API_KEY },
    });
    const payload = await pollResponse.json();
    if (!pollResponse.ok) throw new Error("Poll failed: " + JSON.stringify(payload));

    const status = statusOf(payload);
    if (["succeeded", "success", "completed"].includes(status)) {
      const result = resultOf(payload);
      if (!result) throw new Error("Task succeeded without a URL or base64 result");
      return { taskId, result, raw: payload };
    }
    if (["failed", "failure", "cancelled", "canceled"].includes(status)) {
      throw new Error("Image task failed: " + JSON.stringify(payload));
    }
  }

  throw new Error("Polling timed out. Resume later with task ID: " + taskId);
}

const output = await generateAsyncImage("A clean editorial product image of a silver desk lamp, warm side light, no text");
console.log(output);
async_image.pypython
import base64
import os
import time
from pathlib import Path

import requests

BASE_URL = "https://img.gpt88.cc"
API_KEY = os.environ["GPT88_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

def first(*values):
    return next((value for value in values if value not in (None, "")), None)

def task_id_of(payload):
    data = payload.get("data") or {}
    return first(payload.get("task_id"), payload.get("id"), data.get("task_id"), data.get("id"))

def status_of(payload):
    data = payload.get("data") or {}
    task = payload.get("task") or {}
    return str(first(data.get("status"), payload.get("status"), task.get("status"), "")).lower()

def result_of(payload):
    data = payload.get("data") or {}
    result = data.get("result") or payload.get("result") or {}
    result_data = result.get("data") or data.get("data") or []
    first_item = result_data[0] if result_data else (data[0] if isinstance(data, list) and data else {})
    return first(data.get("result_url"), payload.get("result_url"), first_item.get("url"), first_item.get("b64_json"))

create = requests.post(
    BASE_URL + "/v1/images/generations",
    headers={**HEADERS, "Content-Type": "application/json"},
    json={
        "model": "gpt-image-2",
        "prompt": "A premium ecommerce hero image of a glass skincare bottle, soft morning light, no text",
        "size": "1024x1024",
        "quality": "high",
        "n": 1,
        "async": True,
    },
    timeout=30,
)
create.raise_for_status()
created = create.json()
task_id = task_id_of(created)
if not task_id:
    raise RuntimeError(f"No task ID returned: {created}")

for _ in range(60):
    time.sleep(5)
    response = requests.get(
        BASE_URL + "/v1/images/generations/" + task_id,
        headers=HEADERS,
        timeout=30,
    )
    response.raise_for_status()
    payload = response.json()
    status = status_of(payload)
    if status in {"succeeded", "success", "completed"}:
        result = result_of(payload)
        if not result:
            raise RuntimeError(f"Task succeeded without an image result: {payload}")
        if result.startswith("http"):
            image = requests.get(result, timeout=60)
            image.raise_for_status()
            Path("async-image.png").write_bytes(image.content)
        else:
            Path("async-image.png").write_bytes(base64.b64decode(result))
        break
    if status in {"failed", "failure", "cancelled", "canceled"}:
        raise RuntimeError(f"Image task failed: {payload}")
else:
    raise TimeoutError(f"Polling timed out; keep task ID {task_id}")

Decision guide

NeedStart withReason and trade-off
Fast interactive previewSync, n=1, small sizeShortest feedback loop; less orchestration.
Long-running or high-resolution outputAsync, n=1Protects request timeouts; one task is easier to inspect.
Several variants for one briefStabilize one task, then raise n or queue tasksAvoid multiplying an untested prompt.
Reference-image editAsync if upload or generation is slowKeep upload, task state, and final asset handling observable.
Batch productionQueue with a concurrency limitThroughput improves, but usage and retry pressure rise.
Unknown model behaviorRun a small probe taskLearn accepted fields and response shape before scaling.

Use the narrowest change that solves the current problem: change one prompt field, one size, or one quality setting at a time.

Iteration and evaluation loop

  1. Inspect the final file against correctness, composition, subject fidelity, format, and cost criteria.
  2. Identify the single largest defect: wrong subject, crop, style, readability, or technical format.
  3. Change one relevant input while keeping the task lifecycle code unchanged.
  4. Submit one new task and compare it with the previous artifact.
  5. Keep, revert, or save the winning prompt and parameter bundle as a template.

Production task record

Keep a small durable record for every submitted task. It makes retries, support requests, and usage review much easier.

image-task-record.jsonjson
{
  "task_id": "imgtask_123",
  "model": "gpt-image-2",
  "prompt_hash": "sha256:...",
  "status": "processing",
  "submitted_at": "2026-08-05T10:00:00Z",
  "last_polled_at": "2026-08-05T10:00:15Z",
  "attempts": 3,
  "result_url": null,
  "error": null
}
  • prompt_hash lets you detect duplicate work without storing every prompt in a log.
  • attempts counts polling attempts, not blind resubmissions.
  • result_url should be copied into durable storage before the task record is archived.
  • error should retain the raw provider message and your normalized error category.

Troubleshooting and recovery

SymptomLikely causeSmallest recovery
No task ID in the submit responseRejected request, unexpected envelope, or unsupported async fieldSave the raw response, check HTTP status, then inspect the active model contract.
Polling returns 404Wrong task ID, wrong path, or task not visible on that routeVerify the exact returned ID and endpoint; do not create a replacement task yet.
Polling returns 401/403Missing key, wrong key, or permission/quota issueUse the same server-side auth header and verify account access.
Many 429 responsesPolling or submissions are too frequentBack off, reduce concurrency, and keep the same task ID.
Progress stays unchangedQueue pressure or model-side delayKeep a maximum duration; report task ID rather than spawning duplicates.
Success has no URLResult is nested, base64-only, or response shape changedLog the raw success payload and extend the extractor for that model.
Downloaded file is invalidTemporary URL expired or response was an error bodyCheck content type/size, download immediately, and store a durable copy.
Cost is higher than expectedRepeated submissions, high quality, n > 1, or account-specific pricingInspect usage by task/request ID before changing concurrency.

Practice task and checklist

Generate one square product image with n=1, save it locally, and verify the following:

  • [ ] API key is read from an environment variable.
  • [ ] Submit response is saved before polling begins.
  • [ ] The task ID is persisted and printed for recovery.
  • [ ] Polling stops on success, failure, cancellation, or timeout.
  • [ ] A transient poll error does not create a second task.
  • [ ] The result is downloaded or decoded and its file type is checked.
  • [ ] The raw success/failure response is available for support or debugging.
  • [ ] Actual usage is checked before increasing size, quality, n, or concurrency.

After this exercise works, move the same function into a worker and add a queue. Do not add batch concurrency until one task is reliable and its cost is understood.

Evidence and confidence notes

  • The documented workflow follows the image API entry point already used by the site and the existing asynchronous video task pattern.
  • The lifecycle concepts—task ID, polling, terminal state, result extraction, bounded retries—are high confidence.
  • The exact async switch, status spelling, result envelope, URL lifetime, limits, pricing, and model availability are dynamic; treat the compatibility examples as illustrative and verify the live response.
  • For the release summary, see the Async Image Generation Support Notice; for synchronous fields, see Image Generation API.