Image generation
Generate and edit images synchronously through the OpenAI-compatible Images API, or asynchronously with async: true — recommended for slow, high-quality models to avoid long-request timeouts.
Synchronous generation
The simplest path: one request, base64 result under data[].b64_json.
Each entry in data[] is one image — decode b64_json from base64 and save it as a file. Editing returns the same shape.
curl https://apixly.ai/v1/images/generations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "A cat astronaut floating in space, digital art",
"size": "1024x1024"
}'{
"created": 1782500000,
"data": [{ "b64_json": "<BASE64_IMAGE_DATA>" }]
}Synchronous editing
Besides multipart, /v1/images/edits also accepts a JSON body: put source images in images[] as {"image_url": "https://…"} (a data: URL works too, i.e. base64). images[].file_id is NOT supported — the API rejects it with an explicit error. A mask can be passed as mask.image_url the same way. JSON is often the more convenient shape for async editing.
curl https://apixly.ai/v1/images/edits \
-H "Authorization: Bearer YOUR_API_KEY" \
-F model="gpt-image-2" \
-F image="@photo.png" \
-F prompt="Add a wizard hat and a soft magical glow" \
-F size="1024x1024"{
"created": 1782500000,
"data": [{ "b64_json": "<BASE64_IMAGE_DATA>" }]
}Request parameters
| Field | Required | Description |
|---|---|---|
| model | yes | Image model ID, e.g. gpt-image-2 |
| prompt | yes | Text description of the image |
| async | no | true switches to async job mode (submit → poll → download); omit for synchronous. Required if callback_url is present (otherwise 400) |
| n | no | n=1. JSON aspect_ratio, resolution=1K/2K/4K and seed are supported by the Gemini image route. size maps to a supported aspect/resolution, not guaranteed exact pixels. mask is unsupported on Gemini/chat-converted routes: unsupported_image_mask; async submission may return 202 then failed. quality/background/output_format/output_compression/input_fidelity/partial_images are not universal Gemini controls. Use /v1/images/generations or /v1/images/edits. n and quality/size/output options depend on the selected upstream; do not assume GPT Image 2.5 variants inherit every GPT Image 2 option. GPT Image 2 has no input_fidelity control. Use async=true for durable polling and fetch result URLs before expires_at. |
| size | no | e.g. 1024x1024 |
| quality | no | low / medium / high (model-dependent) |
| image | no | Edits only: source image file(s), multipart image[]; with a JSON body use images[] of {image_url} instead (data: URLs accepted, file_id rejected) |
| callback_url | no | Terminal-state webhook URL (optional, requires async:true): public http(s) address; we POST one JSON notification when the job reaches succeeded / failed / expired. See the webhook section below |
| mask | no | Edits only: multipart mask file or JSON mask.image_url; source and mask support depend on the model. |
| background | no | Model-dependent: auto / opaque / transparent; not a gateway async flag. |
| output_format | no | Model-dependent image file format: png / jpeg / webp. |
| output_compression | no | Integer compression/quality setting when supported by the selected model. |
| input_fidelity | no | Edit fidelity option when supported by the model. |
| stream / partial_images | no | SSE / partial-image options are model-dependent; async task submission is separate. Do not assume incremental previews for every adapter. |
Async: submit a job
Add "async": true to the same endpoint. You get 202 Accepted with a job id instead of waiting on a long connection. The flow is submit → poll → download.
curl https://apixly.ai/v1/images/generations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "a cute robot",
"n": 1,
"size": "1024x1024",
"quality": "high",
"async": true
}'
# => 202 Accepted
# {"id":"123","object":"image.generation.job","status":"queued","model":"gpt-image-2","created":1782500000}Async editing: file or URL + webhook
POST /v1/images/edits edits an existing image. Upload photo.png with multipart image and async=true. Do not set Content-Type manually with curl -F. Replace YOUR_API_KEY and use a model available to your key. callback_url is optional; omit it to use polling only.
For JSON editing, pass source images as images: [{"image_url": "https://…"}] and async: true. Replace the example URL with a direct, publicly accessible image URL (no login); data: URLs are also accepted. JSON URL editing depends on the model/channel; file upload is the preferred compatibility option. file_id is not supported. Optional masks use multipart [email protected] or JSON mask.image_url; mask support depends on the model.
curl https://apixly.ai/v1/images/edits \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "model=gpt-image-2" \
-F "[email protected]" \
-F "prompt=Replace the background with white; preserve the product." \
-F "async=true" \
-F "callback_url=https://your-server.example.com/webhooks/image"curl https://apixly.ai/v1/images/edits \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-3.1-flash-image-preview",
"prompt": "Replace the background with white; preserve the product.",
"images": [{"image_url": "https://your-server.example.com/photo.png"}],
"async": true,
"callback_url": "https://your-server.example.com/webhooks/image"
}'Both examples return HTTP 202 and an id. Even for edits, query GET /v1/images/generations/{id}, not /v1/images/edits/{id}. Poll every 3–5 seconds or receive the webhook described below. Save the id; another POST creates a new task and may incur another charge.
Async: poll and download
Poll the job until it reaches a final state, then fetch each result URL.
| Field | Meaning |
|---|---|
| status | queued → in_progress → succeeded | failed; a succeeded job whose results expired later returns "expired" |
| data[].url | One temporary download link per result, valid ~10 minutes after success |
| usage | Token usage snapshot when the provider reported it: input_tokens / output_tokens / total_tokens (may include image_output_tokens) |
| error.message | The failure reason when status = failed |
curl https://apixly.ai/v1/images/generations/JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY"
# status: queued -> in_progress -> succeeded | failed
# on success each result is a temporary URL under data[].url, valid ~10 minutes after success
# a succeeded job polled after its results aged out returns status "expired"
# succeeded with EMPTY data[] = links temporarily unavailable, retry the poll (NOT a failure){
"id": "123",
"object": "image.generation.job",
"status": "succeeded",
"model": "gpt-image-2",
"data": [{ "url": "<TEMPORARY_DOWNLOAD_URL>" }]
}Results expire about 10 minutes after the task succeeds; download data[].url immediately. The URL includes its access token and needs no Authorization header. If succeeded has missing or empty data, keep polling the same id; do not automatically resubmit. If links remain unavailable, retain the id and contact support before the result expires. A local timeout does not cancel the task. The Python 3 example below uses only the standard library and stops waiting after a bounded interval; resume GET queries with the saved id. Expired results cannot be recovered.
Webhook callback (optional)
With async:true, callback_url enables a terminal-state JSON POST for generation and editing: task_id, status (succeeded / failed / expired), data[].url on success and error on failure. Notifications may be delivered more than once; deduplicate by task_id. Without callback_url, use polling.
After verifying and durably recording the notification, quickly return HTTP 200/204. Each delivery has a 10-second timeout. Network errors, timeouts and non-2xx responses trigger up to 3 retries (approximately 10 seconds / 1 minute / 5 minutes; 4 attempts total). Redirects are not followed. GET remains the source of truth if a callback is missing or lacks data.
- callback_url must be a public http(s) URL — private/internal addresses are rejected with 400; https is recommended. Requires async:true (400 otherwise).
- status "expired" means the job succeeded earlier but its result already aged out before delivery — resubmit if you still need the image.
- data[].url links are freshly signed per delivery but expire about 10 minutes AFTER THE JOB SUCCEEDED — redeliveries do not extend that — so download immediately.
- Billing is fully independent of the webhook: a successful generation is charged as usual whether or not the callback can be delivered; callback failures never change task state or billing.
- The payload carries no key identifier — if you use multiple API keys, dedicate one callback_url per key or map task_id back to the creating key. Webhooks use created_at / completed_at while image polling uses "created".
- The first delivery normally goes out within seconds of the terminal state (the dispatcher scans about every 5 seconds), so with prompt handling the ~10-minute URL window is ample; "expired" deliveries only happen when the result aged out before a delivery finally succeeded.
| Field | Description |
|---|---|
| HTTP header X-Webhook-Signature | Signature: t=<unix seconds>,v1=hex(HMAC-SHA256(your API key, t + "." + raw body)). Verification optional but recommended |
| HTTP header X-Webhook-Attempt | Delivery attempt number, 1–4 |
| object | Always "image.generation.job" |
| task_id / id | Job ID (string) — identical to the id returned at submission; deduplicate by it |
| model | The model you requested |
| status | "succeeded", "failed" or "expired" — same enum as the polling endpoint; "expired" means the result aged out before delivery |
| data[].url | Success only: one download URL per image, freshly signed per delivery, valid ~10 minutes after the job succeeded (redeliveries do not extend it) |
| error.message / error.code | Failure only: human-readable reason; content-moderation rejections carry code "content_policy_violation" |
| usage | Token usage snapshot when the provider reported it (input_tokens / output_tokens / total_tokens, may include image_output_tokens) |
| created_at / completed_at | Unix seconds: submission time / terminal time |
curl https://apixly.ai/v1/images/generations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "a cute robot",
"size": "1024x1024",
"async": true,
"callback_url": "https://your-server.example.com/webhooks/image"
}'// POST to your callback_url on the terminal state. Headers we send:
// Content-Type: application/json User-Agent: sub2api-webhook/1.0
// X-Webhook-Signature: t=<unix>,v1=<hex hmac> X-Webhook-Attempt: 1..4
{
"object": "image.generation.job",
"task_id": "456",
"id": "456",
"model": "gpt-image-2",
"status": "succeeded", // succeeded | failed | expired
"created_at": 1785774335,
"completed_at": 1785774396,
"usage": { "input_tokens": 123, "output_tokens": 4096, "total_tokens": 4219 },
"data": [
{ "url": "https://apixly.ai/v1/images/content?ref=..." }
]
}
// data[].url is valid ~10 minutes AFTER THE JOB SUCCEEDED (redeliveries do not
// extend it) — download immediately. "usage" appears only when the provider
// reported token usage; it may also include "image_output_tokens".Webhook signature & how to verify it
The signing secret is simply the API key you created the task with — there is no separate webhook secret to provision. Every delivery carries a signature header like X-Webhook-Signature: t=1754500000,v1=8f2a…c9, where t is the unix timestamp at send time and v1 is the signature: the hex HMAC-SHA256 of the string t + "." + <raw request body bytes>, keyed with your API key.
Verification is optional: skip it by ignoring the header — deliveries behave the same. To verify, follow these four steps (recommended in production):
- Step 1: read t (timestamp) and v1 (signature value) from the header.
- Step 2: compute HMAC-SHA256 keyed with your API key over t + "." + the raw request body bytes, hex-encoded. Use the exact bytes received — do not parse and re-serialize the JSON first.
- Step 3: compare your result with v1 using a constant-time comparison: match = verified; mismatch, or no signature header at all = discard.
- Step 4: check that t is close to the current time (≤ 5 minutes recommended); discard stale requests to block replays.
# Optional: verify X-Webhook-Signature (recommended — reject unsigned
# requests and stale timestamps if you enable verification).
import hashlib, hmac, time
def verify(header: str, body: bytes, api_key: str, max_age: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t or not v1:
return False
if abs(time.time() - int(t)) > max_age: # replay protection
return False
mac = hmac.new(api_key.encode(), f"{t}.".encode() + body, hashlib.sha256)
return hmac.compare_digest(mac.hexdigest(), v1)The signing key is the API key that created the task — keep it server-side only. If that key is deleted before a task finishes, its callbacks are still delivered but WITHOUT a signature header. So if you reject unsigned requests, accept that deleting a key drops its in-flight callbacks; otherwise handle unsigned callbacks by looking the task_id up yourself before trusting them. The payload carries no key identifier — either dedicate one callback_url per key, or map task_id back to the key that created it.
Model-specific notes
Do not send response_format for gpt-image-* models — they return base64 natively and strict upstreams reject the parameter. Async results always arrive as URLs under data[].url.
Complete examples
Two copy-paste-ready end-to-end examples: an e-commerce product shot (quality tier + exact size); an async job driven by one script from submit to downloading every result.
# Use case: e-commerce product shot — quality tier + exact size
curl https://apixly.ai/v1/images/generations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-image-2",
"prompt": "studio product photo of a matte black thermos on white marble, soft window light, subtle shadow, 45-degree angle, no text",
"size": "1024x1536",
"quality": "high"
}'import json
import time
import urllib.request
from pathlib import Path
BASE = "https://apixly.ai"
API_KEY = "YOUR_API_KEY"
def request_json(path, payload=None):
body = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(
BASE + path, data=body,
headers={"Authorization": "Bearer " + API_KEY,
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=20) as response:
return json.load(response)
# Submit once. Never automatically retry a POST after a network timeout.
try:
job = request_json("/v1/images/generations", {
"model": "gpt-image-2", "prompt": "a cozy reading nook, warm light",
"n": 1, "size": "1024x1024", "async": True,
})
job_id = job["id"]
if not isinstance(job_id, str) or not job_id.isdigit():
raise ValueError("invalid job id")
except Exception as error:
raise SystemExit(f"Submission not confirmed: {error}. Do not auto-resubmit; check with support.")
print(f"Save this job id: {job_id}", flush=True)
# Editing jobs use this same GET endpoint.
deadline = time.monotonic() + 600
status, images = None, []
try:
for attempt in range(120):
if time.monotonic() >= deadline:
break
result = request_json(f"/v1/images/generations/{job_id}")
status = result.get("status")
print(f"{job_id}: {status}")
if status in ("failed", "expired"):
raise ValueError(json.dumps(result, ensure_ascii=False))
if status not in ("queued", "in_progress", "succeeded"):
raise ValueError("invalid status response")
images = result.get("data") or []
if status == "succeeded" and images:
for index, item in enumerate(images, 1):
with urllib.request.urlopen(item["url"], timeout=30) as response:
Path(f"image_{job_id}_{index}.bin").write_bytes(response.read())
print("Downloaded. File format depends on the model; inspect before renaming.")
break
time.sleep(5)
else:
raise TimeoutError("polling limit reached")
if not (status == "succeeded" and images):
raise TimeoutError("local waiting limit reached")
except Exception as error:
raise SystemExit(f"Job {job_id}: {error}. Keep this id; query GET again or contact support. Do not auto-resubmit.")Model-specific request contracts
Discover available IDs with GET /v1/models using your key. The public price catalog is not a guarantee of current routing. Existing valid requests need no changes; optional fields are model-specific.
| Model | Request contract |
|---|---|
| gpt-image-2 · gpt-image-2.5-flare · gpt-image-2.5-sunburst | Use /v1/images/generations or /v1/images/edits. n and quality/size/output options depend on the selected upstream; do not assume GPT Image 2.5 variants inherit every GPT Image 2 option. GPT Image 2 has no input_fidelity control. Use async=true for durable polling and fetch result URLs before expires_at. If your key exposes legacy gpt-image-1/gpt-image-1.5, input_fidelity supports low/high on the Responses image route; gpt-image-1-mini supports low only. Invalid legacy values return 400 before generation. This does not enable those models for your key. |
| gemini-3-pro-image · gemini-3-pro-image-preview · gemini-3.0-pro-image · gemini-3.0-pro-image-preview · gemini-3.1-flash-image · gemini-3.1-flash-image-preview · gemini-nano-banana-2.1 | n=1. JSON aspect_ratio, resolution=1K/2K/4K and seed are supported by the Gemini image route. size maps to a supported aspect/resolution, not guaranteed exact pixels. mask is unsupported on Gemini/chat-converted routes: unsupported_image_mask; async submission may return 202 then failed. quality/background/output_format/output_compression/input_fidelity/partial_images are not universal Gemini controls. |
| seedream-5-0 · seedream-5-0-pro | Editing uses n=1. quality is removed by this adapter. mask is prompt-guided black/white reference editing, not a guarantee that pixels outside the mask stay unchanged. watermark defaults to false. Use supported size/ratio settings; public IDs are stable aliases, not dated supplier IDs. |
| grok-imagine-image · grok-imagine-image-quality | Use Images endpoints with prompt and supported source images. size/quality/mask/output controls are upstream-specific; generic request-field acceptance does not prove the model applies them. Check key model access before selecting the quality variant. |