Image Generation
POST /v1/images/generations
OpenAI-compatible image generation. The primary model is gpt-image-2. Two modes are supported:
- Synchronous — the request blocks until the image is ready and returns it directly (best for low concurrency / interactive use).
- Asynchronous — submit and immediately receive a job ID, then poll for the result (best for high volume / avoiding long-held connections).
Available models
| Model | Resolution tiers | Notes |
|---|---|---|
gpt-image-2 |
1K / 2K / 4K | Primary model; general-purpose generation and editing. |
gpt-image-2.5 |
1K | 1K tier only. |
gpt-image-2.5-flare |
1K / 2K / 4K | Everyday generation, faster turnaround. |
gpt-image-2.5-sunburst |
1K / 2K / 4K | Precision editing; follows edit instructions closely. |
Every model above supports both usage modes:
- Text-to-image —
POST /v1/images/generations(synchronous) orPOST /v1/images/generations/async(asynchronous). - Image-to-image —
POST /v1/images/edits(synchronous) orPOST /v1/images/edits/async(asynchronous), sendingimageandpromptas multipart form fields. The size / resolution parameters are exactly the same as for text-to-image.
Compatibility: the model names
gpt-image-2-2Kandgpt-image-2-4Kare still accepted so existing integrations keep working; they are equivalent togpt-image-2withresolutionset to2K/4K. New integrations should usegpt-image-2plus parameters.
Choosing the resolution tier
gpt-image-2 supports three resolution tiers — 1K / 2K / 4K — under one model name, selected by parameters (no need to switch models). Two equivalent ways:
resolution(recommended): set the tier directly to1K/2K/4K; set the aspect ratio withsize, or omit it (defaults to a 1:1 square).size: pass the pixel dimensions for that tier (see the table below); the gateway derives both the tier and the aspect ratio from it.
| Goal | Request body |
|---|---|
| 1K (default) | omit the size parameters, or "resolution": "1K" |
| 2K square | "resolution": "2K", or "size": "2048x2048" |
| 2K 16:9 landscape | "resolution": "2K", "size": "2560x1440", or just "size": "2560x1440" |
| 4K square | "resolution": "4K", or "size": "2880x2880" |
| 4K 16:9 landscape | "resolution": "4K", "size": "3840x2160", or just "size": "3840x2160" |
gpt-image-2.5-flare / gpt-image-2.5-sunburst work the same way; gpt-image-2.5 supports 1K only.
Synchronous: POST /v1/images/generations
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | Model ID, e.g. gpt-image-2. |
prompt |
string | Yes | Text prompt describing the image. |
n |
integer | No | Number of images. Only 1 is currently supported (default 1). |
size |
string | No | Image size in WxH form, e.g. 1024x1024, 2560x1440, 3840x2160, or auto. It selects both the aspect ratio and the resolution tier (see the tier table below). A size that is not in the tier table is classified by pixel area: ≤ 2,000,000 → 1K; ≤ 5,000,000 → 2K; ≤ 9,000,000 → 4K; anything larger returns 400. |
resolution |
string | No | Resolution tier: 1K / 2K / 4K (case-insensitive; image_size is accepted as an alias). Takes precedence over the tier implied by size; combine it with size to fix the aspect ratio while choosing the tier separately. If it names a tier the model does not support, the request returns 400. |
quality |
string | No | Quality level. For the gpt-image-2.5 family: low / medium / high / xhigh / max / auto. For gpt-image-2 it is optional and does not currently change the output. This parameter does not affect the resolution tier. |
response_format |
string | No | Output form: b64_json (default — the image inline as base64 in the JSON) or url (a download link valid for ~2 hours — see "Response" below). Any other value returns 400. |
Tier precedence: resolution > the tier implied by size > default 1K.
Tip: for large images (2K/4K) or high concurrency, send
"response_format": "url"— the response body is a few hundred bytes and the client downloads the image only when needed.
Resolution tier table
| Aspect ratio | 1K | 2K | 4K |
|---|---|---|---|
| 1:1 | 1024×1024 | 2048×2048 | 2880×2880 |
| 16:9 | 1280×720 | 2560×1440 | 3840×2160 |
| 9:16 | 720×1280 | 1440×2560 | 2160×3840 |
| 3:2 | 1248×832 | 2496×1664 | 3504×2336 |
| 2:3 | 832×1248 | 1664×2496 | 2336×3504 |
| 4:3 | 1152×864 | 2304×1728 | 3264×2448 |
| 3:4 | 864×1152 | 1728×2304 | 2448×3264 |
| 5:4 | 1120×896 | 2240×1792 | 3200×2560 |
| 4:5 | 896×1120 | 1792×2240 | 2560×3200 |
| 21:9 | 1456×624 | 3024×1296 | 3696×1584 |
The model renders at its native resolution for the chosen tier and aspect ratio, so the pixel dimensions of the returned image can differ slightly from the requested
size.
Billing
The billing unit and unit price for image generation are defined by the "Pricing" page and the Portal pricing page of the site you use. The usage block in the response reflects the token usage of this generation and can be used for reconciliation; higher tiers produce larger images and therefore cost more.
Examples
cURL
curl https://api.aiin1.ai/v1/images/generations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-image-2",
"prompt": "a red fox running in snow, studio lighting",
"n": 1
}'cURL (16:9 at the 2K tier, selected with size)
curl https://api.aiin1.ai/v1/images/generations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-image-2",
"prompt": "a red fox running in snow, studio lighting",
"size": "2560x1440",
"n": 1
}'cURL (tier selected with resolution)
curl https://api.aiin1.ai/v1/images/generations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-image-2.5-flare",
"prompt": "a red fox running in snow, studio lighting",
"size": "1024x1024",
"resolution": "2K",
"n": 1
}'Python
import openai
client = openai.OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.aiin1.ai/v1",
)
resp = client.images.generate(
model="gpt-image-2",
prompt="a red fox running in snow, studio lighting",
n=1,
)
b64 = resp.data[0].b64_json # base64-encoded PNGThe OpenAI SDK has no native resolution field — pass it via extra_body:
resp = client.images.generate(
model="gpt-image-2",
prompt="a red fox running in snow, studio lighting",
size="1024x1024",
n=1,
extra_body={"resolution": "2K"},
)Image-to-image: POST /v1/images/edits
Upload the source image and the prompt as multipart/form-data. The response has the same shape as synchronous text-to-image (inline base64).
| Parameter | Type | Required | Description |
|---|---|---|---|
image |
file | Yes | Input image — PNG / JPEG / WEBP, at most 25 MB each and 32 MB for the whole request. For multiple reference images repeat image[] (or image), up to 8 images. |
prompt |
string | Yes | The edit instruction — what to change. |
model |
string | Yes | Model ID, e.g. gpt-image-2 or gpt-image-2.5-sunburst (best at following edit instructions). |
n |
integer | No | Number of images. Only 1 is currently supported. |
size / resolution / quality |
string | No | Same meaning as for text-to-image — see the parameter table and tier table above. |
The
maskparameter is not supported yet and returns an error if sent; describe the region to change inpromptinstead.An edit usually takes 1–3 minutes and can take longer at peak times. Set the client read timeout to at least 15 minutes, otherwise the client may disconnect before the image is ready — or use the asynchronous edit route described at the end of this page.
Reference images can also be passed as image URLs or data URLs (JSON form, no file upload), on both the synchronous and asynchronous routes — see "Reference images by URL" at the end of this page.
cURL (single image)
curl https://api.aiin1.ai/v1/images/edits \
-H "Authorization: Bearer YOUR_API_KEY" \
-F image=@input.png \
-F model=gpt-image-2.5-sunburst \
-F prompt="turn the sky into a starry night, keep the fox unchanged" \
-F resolution=2KcURL (multiple reference images, 16:9 landscape)
curl https://api.aiin1.ai/v1/images/edits \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "image[]=@product.png" \
-F "image[]=@background.jpg" \
-F model=gpt-image-2 \
-F prompt="place the product from the first image onto the scene in the second image" \
-F size=2560x1440Python
import base64
import openai
client = openai.OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.aiin1.ai/v1",
timeout=900, # edits are slow — keep the read timeout at 15 minutes or more
)
with open("input.png", "rb") as f:
resp = client.images.edit(
model="gpt-image-2.5-sunburst",
image=f, # multiple reference images: image=[f1, f2]
prompt="turn the sky into a starry night, keep the fox unchanged",
extra_body={"resolution": "2K"},
)
with open("output.png", "wb") as out:
out.write(base64.b64decode(resp.data[0].b64_json))Response
The image is returned inline as base64:
{
"created": 1730000000,
"data": [
{
"b64_json": "iVBORw0KGgoAAAANSUhEUgAA...",
"revised_prompt": "..."
}
],
"usage": {
"input_tokens": 18,
"input_tokens_details": {
"image_tokens": 0,
"text_tokens": 18
},
"output_tokens": 7024,
"output_tokens_details": {
"image_tokens": 7024,
"text_tokens": 0
},
"total_tokens": 7042
}
}To display in a browser, prefix it: data:image/png;base64,<b64_json>.
With "response_format": "url" the response carries a download link instead of base64:
{
"created": 1730000000,
"data": [
{
"url": "https://<bucket>.r2.cloudflarestorage.com/...&X-Amz-Signature=...",
"revised_prompt": "..."
}
],
"usage": { "...": "same as above" }
}The link is valid for about 2 hours — download or re-host it within that window; the image is deleted after 48 hours. In the rare case the image was generated but the link could not be created, the request returns 502 and is not billed; simply retry.
Asynchronous: POST /v1/images/generations/async
The request body is identical to synchronous text-to-image (model / prompt / n / size / resolution / quality, with the same meanings as above), but it returns a job ID immediately instead of blocking. Use this for batch generation or when the client shouldn't hold a long connection open.
This section covers async text-to-image. For async image-to-image see "Asynchronous image-to-image" at the end of this page.
1. Submit
curl https://api.aiin1.ai/v1/images/generations/async \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-image-2",
"prompt": "a red fox running in snow",
"size": "2560x1440",
"n": 1
}'Returns 202 Accepted:
{
"id": "nximg_xxxxxxxx",
"status": "queued",
"created": 1730000000,
"object": "image.generation.async"
}2. Poll: GET /v1/images/generations/async/{job_id}
curl https://api.aiin1.ai/v1/images/generations/async/nximg_xxxxxxxx \
-H "Authorization: Bearer YOUR_API_KEY"status transitions: queued → processing → succeeded (or failed). Poll every 1–2 seconds; generation usually takes seconds to tens of seconds.
In progress:
{ "id": "nximg_xxxxxxxx", "status": "processing", "created": 1730000000 }Succeeded:
{
"id": "nximg_xxxxxxxx",
"status": "succeeded",
"created": 1730000000,
"data": [
{
"url": "https://<bucket>.r2.cloudflarestorage.com/...&X-Amz-Signature=...",
"revised_prompt": "..."
}
],
"usage": {
"input_tokens": 18,
"input_tokens_details": {
"image_tokens": 0,
"text_tokens": 18
},
"output_tokens": 7024,
"output_tokens_details": {
"image_tokens": 7024,
"text_tokens": 0
},
"total_tokens": 7042
}
}Failed:
{
"id": "nximg_xxxxxxxx",
"status": "failed",
"created": 1730000000,
"error": { "type": "upstream_error", "message": "generation failed", "code": "..." }
}About
url: unlike the synchronous route (inline base64), the async route returns a signed download URL on success (valid for ~2 hours). Download or re-host it within the validity window; an expired URL requires regenerating. Failed jobs are not billed.
Field reference
| Field | Description |
|---|---|
id |
Job ID (nximg_ prefix), used for polling. Bound to your API key's organization — others cannot access it. |
status |
queued / processing / succeeded / failed. |
data[].url |
Signed image download URL on success (~2h validity). |
data[].revised_prompt |
Model-revised prompt (may be empty). |
usage |
Returned on success, in the same shape as the synchronous response. |
Asynchronous image-to-image: POST /v1/images/edits/async
The asynchronous version of POST /v1/images/edits. The request is exactly the same (multipart/form-data file upload, or the JSON form with image URLs; parameters as in the "Image-to-image" section above), but it returns a job ID immediately; poll for the result once the image is ready. Edits are slow (usually 1–3 minutes), so this route is recommended for batch work or when the client shouldn't hold a connection open.
Differences from the synchronous edit route:
- Up to 8 input images, at most 25 MB each and 32 MB for the whole request.
maskis not supported and returns 400.- On success it returns a signed download URL (valid for ~2 hours), not inline base64.
1. Submit a job
curl https://api.aiin1.ai/v1/images/edits/async \
-H "Authorization: Bearer YOUR_API_KEY" \
-F image=@input.png \
-F model=gpt-image-2 \
-F prompt="turn the sky into a starry night, keep the fox unchanged" \
-F resolution=2KFor multiple reference images repeat image[] (or image), as with the synchronous route.
Returns 202 Accepted:
{
"id": "nximg_xxxxxxxx",
"status": "queued",
"created": 1730000000,
"object": "image.edit.async"
}2. Poll for the result: GET /v1/images/edits/async/{job_id}
curl https://api.aiin1.ai/v1/images/edits/async/nximg_xxxxxxxx \
-H "Authorization: Bearer YOUR_API_KEY"The response is exactly the same as for async text-to-image (queued → processing → succeeded / failed; on success data[].url is the download link and usage is included). Poll every 2–5 seconds. Job IDs are shared by both async routes, so GET /v1/images/generations/async/{job_id} also works.
Python example
import time
import requests
API = "https://api.aiin1.ai/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
# 1. Submit (multiple reference images: files=[("image[]", f1), ("image[]", f2)])
with open("input.png", "rb") as f:
job = requests.post(
f"{API}/images/edits/async",
headers=HEADERS,
files=[("image", ("input.png", f, "image/png"))],
data={
"model": "gpt-image-2",
"prompt": "turn the sky into a starry night, keep the fox unchanged",
"resolution": "2K",
},
timeout=120,
).json()
# 2. Poll
while True:
r = requests.get(f"{API}/images/edits/async/{job['id']}", headers=HEADERS, timeout=30).json()
if r["status"] in ("succeeded", "failed"):
break
time.sleep(3)
if r["status"] == "succeeded":
img = requests.get(r["data"][0]["url"], timeout=120).content # URL valid for ~2 hours
open("output.png", "wb").write(img)
else:
print(r["error"])Reference images by URL (JSON form)
Applies to POST /v1/images/edits (synchronous) and POST /v1/images/edits/async (asynchronous). When your images already live in object storage, a CDN or an image host, there is no need to download and re-upload them — put the links straight into a JSON request body.
How it works: the gateway first downloads and validates every reference image, and only starts generating once all of them pass. If any image has a problem, the request returns 400 before generation and is not billed. This prevents the case where a broken link still produces an image unrelated to your reference — and you get charged for it.
Request format
- Header:
Content-Type: application/json(cannot be combined with multipart; a request uses one form or the other). - Body parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
model |
string | Yes | Model ID, e.g. gpt-image-2. |
prompt |
string | Yes | The edit instruction. With several reference images you can refer to "the first image", "the second image", in the same order as the images array. |
images |
array | Yes | Reference images, up to 8; see the table below for how each item can be written. |
size / resolution / quality |
string | No | Same meaning as for text-to-image — see the parameter table and tier table above. |
response_format |
string | No | b64_json (default) or url. Synchronous route only; the asynchronous route always returns a link. |
n |
integer | No | Only 1 is currently supported. |
mask is not supported and returns 400.
How each item in images can be written
All of the following are accepted, and they can be mixed within one array:
| Form | Example |
|---|---|
| Image URL (string) | "https://cdn.example.com/product.png" |
| data URL (string) | "data:image/png;base64,iVBORw0KGgo..." |
| OpenAI-style object | {"image_url": "https://cdn.example.com/product.png"} |
| OpenAI-style nested object | {"image_url": {"url": "https://cdn.example.com/product.png"}} |
You can also use the image field instead of images; its value may be a single string or an array.
URL requirements
| Item | Requirement |
|---|---|
| Scheme and port | https:// only, on the default port 443; http:// and explicit port numbers are not supported. |
| Address | Must be a domain name reachable directly from the public internet — not an IP address, localhost or an internal address. |
| Access | The gateway downloads without any cookies, login session or custom headers. Images that require a login or are hotlink-protected (Referer checks) will fail; for private objects, provide a signed temporary URL (e.g. an S3 / OSS / COS / R2 presigned URL). |
| Redirects | Redirects are not followed. Short links and share links that redirect are rejected — pass the final image URL. |
| Format | PNG / JPEG / WEBP only. The format is detected from the file content, not from the URL extension or the server's Content-Type; GIF, HEIC, SVG and BMP are rejected. |
| Size | At most 25 MB per image and 64 MB for all reference images together. |
| Download timeout | 5 seconds to connect and 30 seconds in total per image; a timeout is an error. |
| Signed-URL validity | Synchronous route: the link only has to be valid when the request is sent. Asynchronous route: reference images are downloaded at submit time, so the link only has to be valid when you submit — it does not need to cover queueing and generation. |
Tip: compress reference images before sending them, ideally to 2 MB or less each (for example a JPEG whose longer side is at most 2048 pixels). Smaller images are submitted faster and succeed more often, especially at peak times.
data URL requirements
- The form must be
data:image/png;base64,...,data:image/jpeg;base64,...ordata:image/webp;base64,.... - The base64 must be valid, and the declared type must match the actual content (declaring
image/pngfor JPEG content is rejected). - data URLs count toward the 32 MB whole-request limit, and base64 adds about a third, so keep each original image under roughly 20 MB; prefer links for large images.
Converting a local file to a data URL:
import base64
def to_data_url(path: str) -> str:
data = open(path, "rb").read()
if data.startswith(b"\x89PNG"):
mime = "image/png"
elif data.startswith(b"\xff\xd8\xff"):
mime = "image/jpeg"
elif data[:4] == b"RIFF" and data[8:12] == b"WEBP":
mime = "image/webp"
else:
raise ValueError("only PNG / JPEG / WEBP are supported")
return f"data:{mime};base64,{base64.b64encode(data).decode()}"Examples
cURL: synchronous, two reference images (one URL, one data URL), link output
curl https://api.aiin1.ai/v1/images/edits \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-image-2",
"prompt": "place the product from the first image into the scene of the second image, keeping the product unchanged",
"images": [
"https://cdn.example.com/product.png",
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
],
"size": "2560x1440",
"response_format": "url"
}'Python: synchronous
import requests
API = "https://api.aiin1.ai/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
r = requests.post(
f"{API}/images/edits",
headers=HEADERS,
json={
"model": "gpt-image-2",
"prompt": "turn the sky into a starry night, keep everything else unchanged",
"images": ["https://cdn.example.com/photo.jpg"],
"response_format": "url",
},
timeout=900, # edits are slow — keep the read timeout at 15 minutes or more
)
d = r.json()
if r.status_code != 200:
raise RuntimeError(d["error"]["message"]) # e.g. "images[0]: host cdn.example.com answered HTTP 404"
img = requests.get(d["data"][0]["url"], timeout=120).content # link valid for ~2 hours
open("output.png", "wb").write(img)Python: asynchronous
import time
import requests
API = "https://api.aiin1.ai/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
r = requests.post(
f"{API}/images/edits/async",
headers=HEADERS,
json={
"model": "gpt-image-2",
"prompt": "turn the sky into a starry night, keep everything else unchanged",
"images": ["https://cdn.example.com/photo.jpg"],
},
timeout=120,
)
job = r.json()
if r.status_code != 202: # a bad reference image is rejected with 400 at submit time
raise RuntimeError(job["error"]["message"])
while True:
res = requests.get(f"{API}/images/edits/async/{job['id']}", headers=HEADERS, timeout=30).json()
if res["status"] in ("succeeded", "failed"):
break
time.sleep(3)Error reference
A problem with a reference image returns HTTP 400 with error.type = invalid_request_error. For an error about one particular image, message starts with images[index]: (0-based, the position in the images array) followed by the reason; errors about the request as a whole (too many images, mask sent, no image given) have no index prefix. Errors show the host only, never the full URL.
Reason in message |
Meaning | What to do |
|---|---|---|
only https:// URLs are accepted |
The link is not https | Use an https link |
only the default https port 443 is allowed |
The link contains a port number | Remove the port and use a standard https address |
URL host must be a domain name, not an IP |
The link uses an IP address | Use a domain name |
URL host is not a public hostname / resolves to a non-public address |
localhost, an internal name, or a domain that resolves to an internal address | Use a publicly reachable address |
could not resolve host ... |
The domain cannot be resolved | Check the domain name |
answered with a redirect (3xx) |
The link redirects | Use the final address after the redirect |
answered HTTP 403 / answered HTTP 404, etc. |
Access denied or file not found (typically hotlink protection, login required, or an expired signed link) | Make sure the link opens directly and anonymously; for private files use an unexpired presigned link |
timed out fetching from host ... / could not fetch from host ... |
Download timed out or the connection failed | Make sure the server is up and fast enough, or use a data URL instead |
content is not a PNG, JPEG or WebP image |
The downloaded content is not a supported image (e.g. an HTML page, GIF or HEIC) | Make sure the link points at the image file itself and convert it to PNG / JPEG / WEBP |
image larger than ... bytes / images exceed ... bytes in total |
Over 25 MB for one image or over 64 MB in total | Compress the images or send fewer |
data URL must be data:image/(png|jpeg|webp);base64, / data URL is not valid base64 / data URL content is not a valid image/... |
Wrong data URL form, invalid base64, or the declared type does not match the content | Build it as described under "data URL requirements" above |
too many images (max 8) |
More than 8 reference images | Send 8 or fewer |
mask is not supported with a JSON body |
mask was sent |
Remove mask and describe the area to change in prompt |
images is required (a list of image URLs or data URLs) |
No image given, or images is an empty array |
Send at least one reference image |
Billing
- A reference image fails validation: no image is generated and nothing is billed.
- Validation passes and generation succeeds: billed exactly the same as the file-upload form.