/omni/media/v1/images/generations/tasksImage generation
Image generation runs in two steps: create a task (202 + task id), then poll until the status is terminal and download the signed URL. Point your client at the media base URL https://api.atptoken.ai/omni/media/v1 and authenticate with a project atp- key. The gateway routes your unified model to an image provider (e.g. gpt-image-2); confirm names with GET /v1/models. The flow matches video generation.
- Output is written to object storage and returned as a signed edge URL (
https://media-<env>.atptoken.ai/v/...) with a 30-minute TTL — never inline base64. Fetch it promptly. - Requests are refused with
402 insufficient_quotawhen the project balance is ≤ 0.
Media availability depends on the environment and project. Call GET https://api.atptoken.ai/v1/models first; a model absent from that response is not available to that key.
Create a task#
curl https://api.atptoken.ai/omni/media/v1/images/generations/tasks \
-H "Authorization: Bearer atp-..." -H "Content-Type: application/json" \
-H "Idempotency-Key: img-job-001" \
-d '{ "model": "gpt-image-2", "prompt": "a watercolor cat", "size": "1024x1024", "quality": "high", "n": 1 }'
# → 202 { "id": "img_..." }Reuse the same Idempotency-Key when retrying a create after a network failure; use a fresh key for a genuinely new task.
Input images for edit-style models — reference_assets#
Edit-style models (nano-banana-pro-edit, qwen-image-edit-max, …) take their input images in a top-level reference_assets array of objects. It must be [{ "url": "…" }].
content[],imageandimage_urlare all rejected with422 Invalid input.reference_assets: required.- A plain array of URL strings is rejected as
Invalid input.reference_assets.
curl https://api.atptoken.ai/omni/media/v1/images/generations/tasks \
-H "Authorization: Bearer atp-..." -H "Content-Type: application/json" \
-H "Idempotency-Key: img-edit-001" \
-d '{
"model": "nano-banana-pro-edit",
"prompt": "Background: a sunlit marble kitchen counter. Preserve the product exactly — same shape, label text, colors and proportions as the reference.",
"n": 1,
"reference_assets": [
{ "url": "https://example.com/product.png" }
]
}'
# → 202 { "id": "img_..." }What url accepts (image endpoint):
The simplest way to turn an upload into a usable URL: POST /v1/files (the response key is id), then GET /v1/files/{id} without following the redirect and use the Location header — an object-store presigned URL that needs no auth and lives about 15 minutes. See /v1/files.
prompt is still required on an edit call. A short instruction is enough, but the field cannot be omitted.
Poll until terminal#
Poll every 3–8 seconds:
curl https://api.atptoken.ai/omni/media/v1/images/generations/tasks/img_... \
-H "Authorization: Bearer atp-..."
# → { "status": "succeeded", "data": [ { "url": "https://media-prod.atptoken.ai/v/image/...png?exp=...&sig=..." } ], "usage": { "prompt_tokens": 37, "completion_tokens": 7024, "total_tokens": 7061 } }statustransitionsqueued→running→succeeded/failed/cancelled/expired.- A succeeded task's
data[].urlis a signed URL with a 30-minute TTL; after expiry the task still reportssucceededwithexpired: trueand a nullurl— download promptly (re-create to regenerate). usagereports token usage for billing transparency; afailedtask carries a structurederrorobject.- The extension in
data[].urlis not a reliable format signal: a URL ending in.pngcan carry JPEG bytes. If the format matters — you are re-uploading the result, or converting it — sniff the bytes (magic number) rather than trusting the filename.
Python example#
A minimal create-and-poll flow:
import os, time, requests
BASE = "https://api.atptoken.ai/omni/media/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['ATP_API_KEY']}",
"Content-Type": "application/json",
}
resp = requests.post(
f"{BASE}/images/generations/tasks",
headers={**HEADERS, "Idempotency-Key": "img-job-001"},
json={
"model": "gpt-image-2",
"prompt": "a watercolor cat",
"size": "1024x1024",
"quality": "high",
"n": 1,
},
timeout=60,
)
resp.raise_for_status()
task_id = resp.json()["id"]
while True:
task = requests.get(
f"{BASE}/images/generations/tasks/{task_id}", headers=HEADERS, timeout=60
).json()
if task["status"] in ("succeeded", "failed", "cancelled", "expired"):
break
time.sleep(5)
if task["status"] == "succeeded":
for i, item in enumerate(task["data"]):
image = requests.get(item["url"], timeout=120).content # signed URL, 30-min TTL
with open(f"result_{i}.png", "wb") as f:
f.write(image)
else:
raise RuntimeError(task.get("error"))How per-image billing picks a size tier#
Models billed per image are charged by the tier of the image we actually deliver, measured from its longest side:
Two things worth knowing before you budget:
- A model's default output can be wider than you expect. For example a 1408×768 result (about 1.1 megapixels) has a longest side of 1408, so it is billed at the
2Ktier even though its pixel count is closer to a 1K image. - Some models ignore the requested
sizeand always return their native resolution. If the tier matters for your cost, read the dimensions of the returned image rather than assuming the request was honoured.
Only delivered images are billed — a failed generation, or one whose upload never completed, costs nothing. Where a model prices several tiers the same (Nano Banana, Nano Banana Pro), the tier makes no difference to what you pay.