Lovely RouterDocs
Get a key
Models API

Images

Generate and edit images with GPT Image 2 and GPT Image 2.5 through the OpenAI Images API and the official OpenAI SDKs, at a flat price per image. Covers sizes, edits, reference images, pricing and limits.

Lovely Router serves the OpenAI Images API: POST /v1/images/generations and POST /v1/images/edits. Point the OpenAI SDK at https://lovelyrouter.com/v1 and use your Lovely Router key. Requests pass through as OpenAI defines them, so OpenAI's Images API reference applies, with the limits below.

Models#

ModelPriceSizes
gpt-image-2$0.04 per image, any size or qualityUp to 3840×2160
gpt-image-2.5-flare$0.04 per image, any sizeUp to 3840×2160
gpt-image-2.5-sunburst$0.04 per image, any sizeUp to 3840×2160
z-image-turbo-nsfw-v2$0.03 per image, any sizeUp to 1536×1536 (hosted, generation only)
moss-image-sft$0.12 per imageUp to 1024×1024 (hosted, generation only)
moss-image-nsfw$0.105 per imageUp to 1024×1024 (hosted, generation only)

The GPT Image models generate and edit, with reference images too; z-image-turbo-nsfw-v2 only generates. quality has no effect on the GPT Image 2.5 models or on z-image-turbo-nsfw-v2.

Image models only answer the Images API. Sent to Chat Completions, they return 404 with a pointer to /v1/images/generations. The GPT Image models are served in the gpt-value channel group and z-image-turbo-nsfw-v2 in creative-hosted, so a key bound to another group can't use them.

Generate an image#

curl https://lovelyrouter.com/v1/images/generations \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-image-2",
    "prompt": "A watercolor painting of a red fox in a snowy birch forest at dawn",
    "size": "1024x1024",
    "quality": "medium"
  }' | jq -r '.data[0].b64_json' | base64 --decode > fox.png

A trimmed response:

Response
{
  "created": 1792064388,
  "data": [{ "b64_json": "iVBORw0KGgoAAAANSUhEUgAA…" }],
  "background": "auto",
  "model": "gpt-image-2",
  "output_format": "png",
  "quality": "medium",
  "size": "1024x1024",
  "usage": {
    "input_tokens": 19,
    "input_tokens_details": { "text_tokens": 19, "image_tokens": 0 },
    "output_tokens": 1756,
    "output_tokens_details": { "image_tokens": 1756, "text_tokens": 0 },
    "total_tokens": 1775
  }
}

Images always come back as base64 in b64_json. response_format: "url" is ignored. An image usually takes 40 seconds to 2 minutes, plus any time it waits in the queue, so give your HTTP client a timeout of at least ten minutes (the gateway answers within nine). A 3840×2160 PNG is about 14 MB, or 18 MB as base64. To submit an image and collect it later instead of holding a connection open, use image jobs.

Size and quality#

  • size: 1024x1024, 1536x1024, 1024x1536, 2048x2048, 3840x2160 or any WIDTHxHEIGHT where both sides are multiples of 16, the long side is at most 3840 pixels and at most three times the short side, and the image has between 655,360 and 8,294,400 pixels. auto lets the model choose.
  • quality: low, medium, high or auto (the default). The price is the same for every quality.
  • output_format: png (default), jpeg or webp. output_compression (0 to 100) applies to jpeg and webp.
  • background: transparent works with png and webp.

Several images#

Set n, from 1 to 10, to get several images of the same prompt from one request. This works for generations and edits. The images come back together in data.

result = client.images.generate(
    model="gpt-image-2",
    prompt="A watercolor painting of a red fox in a snowy birch forest at dawn",
    n=4,
)
for i, image in enumerate(result.data):
    with open(f"fox-{i + 1}.png", "wb") as f:
        f.write(base64.b64decode(image.b64_json))
  • Each image costs the price of one image, so four images cost four times as much. The price of all n is reserved when you submit, and you pay only for the images you get.
  • The images are made in as few calls to the model as each channel allows. With the GPT Image models, n: 4 usually takes about as long as one image. Hosted models make them one after another on the same GPU.
  • If some of the images can't be made, you get the ones that were and pay for those only. An image job then has status: "succeeded" and error.type: "incomplete", with a message that says how many are missing.
  • Every image counts toward the 20 images an account can have queued or generating at once.

Edit an image#

Every model edits, GPT Image 2.5 included. Send one or more images, and optionally a mask, as multipart/form-data to POST /v1/images/edits. The model, prompt and the other options work as for generations. To give the model several images to work from, see reference images.

curl https://lovelyrouter.com/v1/images/edits \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F model=gpt-image-2 \
  -F image=@fox.png \
  -F prompt="Make it night with a full moon" \
  -F quality=medium | jq -r '.data[0].b64_json' | base64 --decode > fox-night.png

With GPT Image 2.5, set model to gpt-image-2.5-flare or gpt-image-2.5-sunburst and leave out quality, which these models ignore:

cURL
curl https://lovelyrouter.com/v1/images/edits \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F model=gpt-image-2.5-flare \
  -F image=@fox.png \
  -F prompt="Make it night with a full moon" | jq -r '.data[0].b64_json' | base64 --decode > fox-night.png

An edit costs the same as a generation.

Reference images#

Give the model up to 16 images to work from, such as a product, a logo, a person or a style, and say in the prompt what to take from each. Send each one as an image[] file. The OpenAI SDKs do this when you pass a list.

curl https://lovelyrouter.com/v1/images/edits \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -F model=gpt-image-2.5-flare \
  -F "image[]=@mug.png" \
  -F "image[]=@table.jpg" \
  -F prompt="The mug from the first image on the wooden table from the second, morning light" \
  | jq -r '.data[0].b64_json' | base64 --decode > mug-on-table.png

You can also send JSON and give the images as URLs or base64 data: URLs:

cURL
curl https://lovelyrouter.com/v1/images/edits \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "model": "gpt-image-2",
    "prompt": "The same character, now waving, flat illustration",
    "images": [
      {"image_url": "https://example.com/character.png"},
      {"image_url": "data:image/png;base64,iVBORw0KGgo..."}
    ]
  }' | jq -r '.data[0].b64_json' | base64 --decode > waving.png
  • image also takes one URL or a list of URLs, and mask takes one. Images given by file_id are not supported.
  • URLs are downloaded once, when the request arrives. They must be public and answer within 20 seconds.
  • Input images are PNG, JPEG or WebP, up to 50 MB in total.
  • A request to POST /v1/images/generations that carries input images is handled as an edit.

Pricing#

The GPT Image models cost $0.04 per image and z-image-turbo-nsfw-v2 $0.03, the same for every size and quality, for generations and edits alike. The usage object in the response is informational only and doesn't change the price. The channel group's multiplier applies (×1.00 for gpt-value and creative-hosted).

GET /v1/models shows the current price as pricing.per_image, in US dollars. Every call appears in Logs with its charge.

When you submit an image, its price is reserved from your balance and released when the image is done. With n, the price of all n images is reserved, and you pay only for the images you get. You are not charged when the request is refused, the job is cancelled or no image is produced. Once an image has been generated it is charged, even if your client disconnected before it arrived.

How requests are queued#

Every image request joins a queue and starts as soon as image capacity is free, so a burst of requests waits its turn instead of failing. POST /v1/images/generations and POST /v1/images/edits hold the connection until the image is ready, for up to 9 minutes. If it isn't ready by then, they answer 504 with the job id in the x-lovely-image-job header and the message; the image keeps generating, is charged when it's done, and you can fetch it from GET /v1/images/jobs/{id}.

  • Each account can have up to 20 images queued or generating at once, counting every image of a request with n. A request that would go past 20 returns 429 until some finish.
  • An image that can't start within 30 minutes fails with 429 and nothing is charged.
  • Every response has an x-lovely-request-id header (the job id) that matches the call in Logs.

Image jobs#

Image jobs are the same requests without holding a connection open: submit, then poll or long-poll. Use them for batches, or when your client can't wait several minutes.

curl https://lovelyrouter.com/v1/images/jobs \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model": "gpt-image-2", "prompt": "A watercolor painting of a red fox in a snowy birch forest at dawn", "size": "1536x1024"}'

The body takes the same fields as POST /v1/images/generations. Input images make it an edit, as with POST /v1/images/edits: multipart/form-data with image files, or images in JSON. Add wait (0 to 300 seconds, in the body or as ?wait=) to wait for the job before answering: you get 200 if it finished in time, otherwise 202.

Then wait for it. wait holds the request until the job finishes or the time runs out:

cURL
curl "https://lovelyrouter.com/v1/images/jobs/img_8c1f0e2a9b7d4f3e6a5c2b1d?wait=60" \
  -H "Authorization: Bearer $LOVELY_API_KEY"

A finished job has status: "succeeded", its charge_usd, and a result with the image's url, content_type, bytes, size and usage. result.images lists every image of the job with its own url, content_type and bytes: with n greater than 1, the first image is at /content and the others at /content/1, /content/2 and so on. Download the image file from that URL with the same key:

cURL
curl -o fox.png https://lovelyrouter.com/v1/images/jobs/img_8c1f0e2a9b7d4f3e6a5c2b1d/content \
  -H "Authorization: Bearer $LOVELY_API_KEY"
StatusMeaning
queuedWaiting for capacity. queue_position is its place in line.
runningGenerating.
succeededDone. The image is in result for 7 days.
failedNo image. error.message says why; nothing was charged.
cancelledCancelled before it started; nothing was charged.
EndpointDoes
POST /v1/images/jobsSubmit an image (generation, or an edit with an image file).
GET /v1/images/jobs/{id}The job. ?wait= up to 300 seconds.
GET /v1/images/jobs/{id}/contentThe image file, for 7 days after it finished.
GET /v1/images/jobs/{id}/content/{i}Image i, counting from 0, of a job with n greater than 1.
GET /v1/images/jobsYour most recent jobs, newest first. ?limit= up to 100; page with ?before= set to next_before.
POST /v1/images/jobs/{id}/cancelCancel a job that is still queued. A running job can't be cancelled.

The playground uses image jobs: switch it to Images to try prompts and edits without code.

Limits#

  • Up to 10 images per request with n. For more, send several requests or jobs.
  • No streaming. stream: true returns 400. Partial images are not available yet.
  • No variations. POST /v1/images/variations (DALL·E 2 only) returns 404.
  • Your key's requests-per-minute limit applies to submitting images. Up to 20 images per account can be queued or generating at once.
StatusWhen
400Invalid parameters, stream, an n outside 1 to 10, an input image that can't be downloaded or read, or a prompt refused by the safety system. The message says why.
402Your balance can't cover the price of the images, or the key reached its spend limit.
404Unknown model, or a model that doesn't generate images.
429The images would take your account past 20 queued or generating, your key's rate limit was reached, or the image couldn't start within 30 minutes.
502Image generation failed on every channel. Nothing was charged. Retry.
504The image isn't ready after 9 minutes. It keeps going; fetch it with the job id from x-lovely-image-job.