apixly.ai

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.

bash
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"
  }'
json
{
  "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.

bash
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"
json
{
  "created": 1782500000,
  "data": [{ "b64_json": "<BASE64_IMAGE_DATA>" }]
}

Request parameters

FieldRequiredDescription
modelyesImage model ID, e.g. gpt-image-2
promptyesText description of the image
asyncnotrue switches to async job mode (submit → poll → download); omit for synchronous. Required if callback_url is present (otherwise 400)
nnon=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.
sizenoe.g. 1024x1024
qualitynolow / medium / high (model-dependent)
imagenoEdits only: source image file(s), multipart image[]; with a JSON body use images[] of {image_url} instead (data: URLs accepted, file_id rejected)
callback_urlnoTerminal-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
masknoEdits only: multipart mask file or JSON mask.image_url; source and mask support depend on the model.
backgroundnoModel-dependent: auto / opaque / transparent; not a gateway async flag.
output_formatnoModel-dependent image file format: png / jpeg / webp.
output_compressionnoInteger compression/quality setting when supported by the selected model.
input_fidelitynoEdit fidelity option when supported by the model.
stream / partial_imagesnoSSE / 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.

bash
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.

bash
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"
bash
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.

FieldMeaning
statusqueued → in_progress → succeeded | failed; a succeeded job whose results expired later returns "expired"
data[].urlOne temporary download link per result, valid ~10 minutes after success
usageToken usage snapshot when the provider reported it: input_tokens / output_tokens / total_tokens (may include image_output_tokens)
error.messageThe failure reason when status = failed
bash
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)
json
{
  "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.
FieldDescription
HTTP header X-Webhook-SignatureSignature: t=<unix seconds>,v1=hex(HMAC-SHA256(your API key, t + "." + raw body)). Verification optional but recommended
HTTP header X-Webhook-AttemptDelivery attempt number, 1–4
objectAlways "image.generation.job"
task_id / idJob ID (string) — identical to the id returned at submission; deduplicate by it
modelThe model you requested
status"succeeded", "failed" or "expired" — same enum as the polling endpoint; "expired" means the result aged out before delivery
data[].urlSuccess 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.codeFailure only: human-readable reason; content-moderation rejections carry code "content_policy_violation"
usageToken usage snapshot when the provider reported it (input_tokens / output_tokens / total_tokens, may include image_output_tokens)
created_at / completed_atUnix seconds: submission time / terminal time
bash
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"
  }'
json
// 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.
python
# 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.

bash
# 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"
  }'
python
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.

ModelRequest contract
gpt-image-2 · gpt-image-2.5-flare · gpt-image-2.5-sunburstUse /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.1n=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-proEditing 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-qualityUse 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.