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
| Term | Meaning | Implementation rule |
|---|---|---|
| Submit | Send the image intent and generation parameters. | Persist the raw response and task ID before doing anything else. |
| Task ID | The stable handle for later status queries. | Use task_id or id from the response; never synthesize one locally. |
| Polling | Repeatedly query the task until it reaches a terminal state. | Use a bounded interval, maximum attempts, and backoff for transient errors. |
| Terminal state | A final success, failure, or cancellation state. | Do not stop just because progress reaches 100. |
| Result | The final image URL or base64 payload. | Download or decode immediately, then store your own durable copy. |
| Task record | Your 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+ withrequests. - 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
- Set the API key and choose a small, single-image request.
- Submit with async mode and save the returned task ID.
- Poll every five seconds, up to a bounded maximum.
- On terminal success, extract the result URL or base64 payload.
- Download or decode it, inspect the file, and record the winning request parameters.
1. Submit an image task
| Field | Type | Required | Description |
|---|---|---|---|
model | string | Required | Image model ID, such as gpt-image-2; confirm availability with the current model list or console. |
prompt | string | Required | The image intent and constraints. Keep the prompt stable while diagnosing polling behavior. |
size | string | Optional | Use a size supported by the selected model. Start small while validating the workflow. |
quality | string | Optional | Quality preset supported by the selected model. Higher quality may take longer. |
n | integer | Optional | Number of images. Start with 1; increase only after one task is reliable. |
async | boolean | Optional | The examples use true to request asynchronous task handling. If a model exposes a different async switch, follow its live contract. |
modelstringRequiredImage model ID, such asgpt-image-2; confirm availability with the current model list or console.promptstringRequiredThe image intent and constraints. Keep the prompt stable while diagnosing polling behavior.sizestringUse a size supported by the selected model. Start small while validating the workflow.qualitystringQuality preset supported by the selected model. Higher quality may take longer.nintegerNumber of images. Start with1; increase only after one task is reliable.asyncbooleanThe examples usetrueto request asynchronous task handling. If a model exposes a different async switch, follow its live contract.
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.jsonexists and contains the raw response.TASK_IDis non-empty.- You have recorded the model, prompt hash, requested size, and submission timestamp.
2. Poll until a terminal state
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.
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 family | What to do | What not to do |
|---|---|---|
| queued / submitted | Keep the task ID and poll again. | Do not resubmit just because no image exists yet. |
| processing / in_progress | Continue polling and update last_polled_at. | Do not treat an empty result URL as failure. |
| succeeded / success / completed | Extract URL or base64, then download or decode. | Do not mark success if the result payload is empty. |
| failed / failure | Persist the error and decide whether a parameter change is needed. | Do not retry the identical request forever. |
| cancelled / canceled | Record 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.
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.pngVerify 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.
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);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
| Need | Start with | Reason and trade-off |
|---|---|---|
| Fast interactive preview | Sync, n=1, small size | Shortest feedback loop; less orchestration. |
| Long-running or high-resolution output | Async, n=1 | Protects request timeouts; one task is easier to inspect. |
| Several variants for one brief | Stabilize one task, then raise n or queue tasks | Avoid multiplying an untested prompt. |
| Reference-image edit | Async if upload or generation is slow | Keep upload, task state, and final asset handling observable. |
| Batch production | Queue with a concurrency limit | Throughput improves, but usage and retry pressure rise. |
| Unknown model behavior | Run a small probe task | Learn 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
- Inspect the final file against correctness, composition, subject fidelity, format, and cost criteria.
- Identify the single largest defect: wrong subject, crop, style, readability, or technical format.
- Change one relevant input while keeping the task lifecycle code unchanged.
- Submit one new task and compare it with the previous artifact.
- 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.
{
"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_hashlets you detect duplicate work without storing every prompt in a log.attemptscounts polling attempts, not blind resubmissions.result_urlshould be copied into durable storage before the task record is archived.errorshould retain the raw provider message and your normalized error category.
Troubleshooting and recovery
| Symptom | Likely cause | Smallest recovery |
|---|---|---|
| No task ID in the submit response | Rejected request, unexpected envelope, or unsupported async field | Save the raw response, check HTTP status, then inspect the active model contract. |
| Polling returns 404 | Wrong task ID, wrong path, or task not visible on that route | Verify the exact returned ID and endpoint; do not create a replacement task yet. |
| Polling returns 401/403 | Missing key, wrong key, or permission/quota issue | Use the same server-side auth header and verify account access. |
| Many 429 responses | Polling or submissions are too frequent | Back off, reduce concurrency, and keep the same task ID. |
| Progress stays unchanged | Queue pressure or model-side delay | Keep a maximum duration; report task ID rather than spawning duplicates. |
| Success has no URL | Result is nested, base64-only, or response shape changed | Log the raw success payload and extend the extractor for that model. |
| Downloaded file is invalid | Temporary URL expired or response was an error body | Check content type/size, download immediately, and store a durable copy. |
| Cost is higher than expected | Repeated submissions, high quality, n > 1, or account-specific pricing | Inspect 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.