GPT Image 2.5 API: Your First Generation and Edit

Use PiAPI's asynchronous GPT Image 2.5 endpoints to submit a generation or edit, save the returned task ID, then poll for its image. The documented async response is a task object with a top-level task_id; completed images are encoded in output.data[0].b64_json.
This guide covers gpt-image-2.5-flare and gpt-image-2.5-sunburst. It uses documented request/response shapes and local synthetic checks; it presents no generated image or measured model performance.
1. Use the asynchronous route and authentication
| Action | Method and path |
|---|---|
| Generate | POST /api/v1/images/generations/async |
| Edit | POST /api/v1/images/edits/async |
| Poll either task | GET /api/v1/task/{task_id} |
The base host is https://api.piapi.ai, and the async authentication header is X-API-Key. Keep the /api/v1 prefix: the synchronous image endpoints use a different path and authentication convention.
Use Python 3.10 or later with your PiAPI key set in the process environment as PIAPI_API_KEY. Keep the key out of source files and shell command arguments.
Save this request as image-generation.json:
{
"model": "gpt-image-2.5-flare",
"prompt": "A red desk lamp on a walnut table beside a rainy window, soft morning light.",
"size": "1024x1024",
"quality": "low"
}You can select gpt-image-2.5-sunburst explicitly with the same request shape. The example deliberately selects a suffixed model: the separate, suffix-free gpt-image-2.5 route has different control semantics, so do not remove the suffix as a shorthand.
For these examples, use 1024x1024, 1024x1536, or 1536x1024 and a documented quality value: auto, low, medium, or high.
2. Submit, save the receipt, and decode the result
Save the following as gpt_image_first_call.py. Each submission gets its own receipt. The save command polls the existing task and decodes the first base64 image to a new PNG file; it does not submit another generation.
"""Small tutorial client. API credentials are read only when a request is made."""
import argparse
import base64
import hashlib
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
API = "https://api.piapi.ai/api/v1"
def api_json(path, payload=None, timeout=30):
key = os.environ.get("PIAPI_API_KEY")
if not key:
raise RuntimeError("Set PIAPI_API_KEY in your environment")
request = urllib.request.Request(
API + path,
data=None if payload is None else json.dumps(payload).encode(),
headers={"X-API-Key": key, "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
body = json.load(response)
except urllib.error.HTTPError as error:
raise RuntimeError(f"API HTTP {error.code}; do not blindly resubmit") from None
if not isinstance(body, dict):
raise RuntimeError("Expected a JSON object")
if "code" in body and body["code"] != 200:
raise RuntimeError(f"API code {body['code']}; inspect the task separately")
return body
def task_object(body):
# Unified video responses wrap data; the documented image async shell does not.
task = body.get("data", body)
if not isinstance(task, dict):
raise RuntimeError("Expected a task object")
return task
def submit_once(path, payload, receipt, request=api_json):
# Reserve the receipt before POST. An interrupted create must be reconciled.
with Path(receipt).open("x") as file:
json.dump({"state": "submission_started", "endpoint": path}, file)
task = task_object(request(path, payload))
task_id = task.get("task_id")
if not isinstance(task_id, str) or not task_id:
raise RuntimeError("Create response has no task_id; reconcile before resubmitting")
print(f"task_id={task_id}", flush=True)
Path(receipt).write_text(json.dumps({"task_id": task_id, "endpoint": path}) + "\n")
return task_id
def wait_for_task(task_id, request=api_json, interval=5, timeout=900):
if interval < 0 or timeout <= 0:
raise ValueError("Invalid polling interval or timeout")
deadline = time.monotonic() + timeout
while (remaining := deadline - time.monotonic()) > 0:
task = task_object(request(
"/task/" + urllib.parse.quote(task_id, safe=""),
timeout=min(30, remaining),
))
state = task.get("status")
if state == "completed":
return task
if state == "failed":
raise RuntimeError(f"Task {task_id} failed; inspect its error response")
if state not in ("pending", "processing", "staged"):
raise RuntimeError(f"Unrecognized task status {state!r}; inspect before continuing")
time.sleep(min(interval, max(0, deadline - time.monotonic())))
raise TimeoutError(f"Client timeout; resume polling saved task {task_id}")
def saved_task_id(receipt):
task_id = json.loads(Path(receipt).read_text()).get("task_id")
if not isinstance(task_id, str) or not task_id:
raise RuntimeError("No saved task_id; reconcile the interrupted create before retrying")
return task_id
PNG = b"\x89PNG\r\n\x1a\n"
def image_bytes(task):
output = task.get("output") or {}
data = output.get("data")
if not isinstance(data, list) or not data or not isinstance(data[0], dict):
raise RuntimeError("Expected output.data[0]")
encoded = data[0].get("b64_json")
if not isinstance(encoded, str) or not encoded:
raise RuntimeError("Expected output.data[0].b64_json")
raw = base64.b64decode(encoded, validate=True)
if not raw.startswith(PNG):
raise RuntimeError("Example expects PNG bytes; inspect returned format")
return raw
def edit_payload(payload, source):
raw = Path(source).read_bytes()
if not raw.startswith(PNG) or len(raw) > 25_000_000:
raise ValueError("Use a PNG no larger than 25 MB for this example")
return {**payload, "image": "data:image/png;base64," + base64.b64encode(raw).decode()}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=["generate", "edit", "save"])
parser.add_argument("receipt")
parser.add_argument("file", help="request JSON for generate/edit; new PNG path for save")
parser.add_argument("--image", help="permitted source PNG for edit")
args = parser.parse_args()
if args.action == "save":
task = wait_for_task(saved_task_id(args.receipt))
raw = image_bytes(task)
with Path(args.file).open("xb") as file:
file.write(raw)
print(f"Saved {args.file}; sha256={hashlib.sha256(raw).hexdigest()}")
return
payload = json.loads(Path(args.file).read_text())
if payload.get("model") not in ("gpt-image-2.5-flare", "gpt-image-2.5-sunburst"):
raise ValueError("Select Flare or Sunburst explicitly")
if payload.get("quality") not in ("auto", "low", "medium", "high"):
raise ValueError("Unsupported quality")
if args.action == "edit":
if not args.image:
parser.error("edit requires --image")
payload = edit_payload(payload, args.image)
path = "/images/edits/async"
else:
path = "/images/generations/async"
submit_once(path, payload, args.receipt)
if __name__ == "__main__":
main()python3 gpt_image_first_call.py generate generation-task.json image-generation.json
python3 gpt_image_first_call.py save generation-task.json generated.pngA pending task has no image to decode. Wait for completed before accessing output.data; handle failed as terminal. PiAPI documents upstream failure details under output.error_body for this async image route. Inspect that response when debugging, without copying credentials or full image payloads into logs.
Save completed images promptly. The script's fifteen-minute wait is a client deadline, not a service latency claim; you can resume polling with the same receipt while the result remains available.
The example requires PNG bytes after decoding. If your response uses another format or shape, stop and inspect it rather than saving arbitrary data under a .png extension. A file signature check also does not replace a full image decode or visual review.
3. Edit the image with a new task
Save the following as image-edit.json:
{
"model": "gpt-image-2.5-flare",
"prompt": "Keep the lamp shape and position. Replace the rainy window with a plain studio backdrop and add a blue notebook beside the lamp.",
"size": "1024x1024",
"quality": "low"
}Then supply the generated file, or another permitted PNG:
python3 gpt_image_first_call.py edit edit-task.json image-edit.json --image generated.png
python3 gpt_image_first_call.py save edit-task.json edited.pngThe script encodes that PNG into a JSON image string beginning with data:image/png;base64,. This is the inline format documented for asynchronous edits. The endpoint also documents multipart file uploads: use image for one file or repeated image[] fields for multiple files. Do not assume that an array of remote URLs from a different client contract is interchangeable with these formats.
Use separate receipts and filenames for generation and editing. Keep the source image and its hash with the edit request so you can reconstruct which input produced the edit. The sample limits its input to PNG and 25 MB; the API documentation also lists JPEG and WebP for file uploads.
FAQ
Does the async API put the task ID under data?
Its documented shell has a top-level task_id. Some PiAPI task routes wrap a task in data; the example handles both object envelopes, while image extraction reads the documented output.data[0].b64_json field.
Can I rerun the generate command after a read timeout?
Use save with the existing receipt to continue polling. If create failed before returning an ID, reconcile the interrupted submission before creating another task. The receipt prevents an accidental repeat submission with the same local name.
Does an edit update the original task?
No. An edit is a new submission with a new task ID and a separate output. Retain both records.
Open the GPT Image 2.5 model page for its current controls. Source checked September 17, 2026: PiAPI GPT Image API documentation, Asynchronous API and Image edits sections.


