---
title: Images
description: 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](https://platform.openai.com/docs/api-reference/images) applies, with the [limits](#limits) below.

## Models {#models}

| Model | Price | Sizes |
| --- | --- | --- |
| `gpt-image-2` | $0.04 per image, any size or quality | Up to 3840×2160 |
| `gpt-image-2.5-flare` | $0.04 per image, any size | Up to 3840×2160 |
| `gpt-image-2.5-sunburst` | $0.04 per image, any size | Up to 3840×2160 |
| `z-image-turbo-nsfw-v2` | $0.03 per image, any size | Up to 1536×1536 ([hosted](/docs/models/hosted#images), generation only) |
| `moss-image-sft` | $0.12 per image | Up to 1024×1024 ([hosted](/docs/models/hosted#images), generation only) |
| `moss-image-nsfw` | $0.105 per image | Up to 1024×1024 ([hosted](/docs/models/hosted#images), generation only) |

The GPT Image models generate and [edit](#edit-an-image), with [reference images](#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](/docs/models/channel-groups) and `z-image-turbo-nsfw-v2` in `creative-hosted`, so a key bound to another group can't use them.

## Generate an image {#generate-an-image}

```bash title="cURL"
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
```

```python title="Python"
import base64
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://lovelyrouter.com/v1",
    api_key=os.environ["LOVELY_API_KEY"],
)
result = client.images.generate(
    model="gpt-image-2",
    prompt="A watercolor painting of a red fox in a snowy birch forest at dawn",
    size="1024x1024",
    quality="medium",
)
with open("fox.png", "wb") as f:
    f.write(base64.b64decode(result.data[0].b64_json))
print(result.usage)
```

```js title="Node.js"
import fs from 'node:fs';
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'https://lovelyrouter.com/v1',
  apiKey: process.env.LOVELY_API_KEY,
});
const result = await client.images.generate({
  model: 'gpt-image-2',
  prompt: 'A watercolor painting of a red fox in a snowy birch forest at dawn',
  size: '1024x1024',
  quality: 'medium',
});
fs.writeFileSync('fox.png', Buffer.from(result.data[0].b64_json, 'base64'));
console.log(result.usage);
```

A trimmed response:

```json title="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](#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](#jobs).

## Size and quality {#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 {#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`.

```python title="Python"
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))
```

```js title="Node.js"
const result = await client.images.generate({
  model: 'gpt-image-2',
  prompt: 'A watercolor painting of a red fox in a snowy birch forest at dawn',
  n: 4,
});
result.data.forEach((image, i) => fs.writeFileSync(`fox-${i + 1}.png`, Buffer.from(image.b64_json, 'base64')));
```

```bash title="cURL"
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", "n": 4}' \
  | jq '.data | length'
```

- 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](/docs/models/hosted#images) 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](#jobs) then has `status: "succeeded"` and `error.type: "incomplete"`, with a message that says how many are missing.
- Every image counts toward the [20 images](#queue) an account can have queued or generating at once.

## Edit an image {#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](#reference-images).

```bash title="cURL"
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
```

```python title="Python"
result = client.images.edit(
    model="gpt-image-2",
    image=open("fox.png", "rb"),
    prompt="Make it night with a full moon",
    quality="medium",
)
with open("fox-night.png", "wb") as f:
    f.write(base64.b64decode(result.data[0].b64_json))
```

```js title="Node.js"
import { toFile } from 'openai';

const result = await client.images.edit({
  model: 'gpt-image-2',
  image: await toFile(fs.createReadStream('fox.png'), 'fox.png', { type: 'image/png' }),
  prompt: 'Make it night with a full moon',
  quality: 'medium',
});
fs.writeFileSync('fox-night.png', Buffer.from(result.data[0].b64_json, 'base64'));
```

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:

```bash title="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 {#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.

```bash title="cURL"
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
```

```python title="Python"
result = client.images.edit(
    model="gpt-image-2.5-flare",
    image=[open("mug.png", "rb"), open("table.jpg", "rb")],
    prompt="The mug from the first image on the wooden table from the second, morning light",
)
```

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

```bash title="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 {#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](/app/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`](#several-images), 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 {#queue}

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}`](#jobs).

- 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](/app/logs).

## Image jobs {#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.

```bash title="cURL"
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"}'
```

```json title="Response (202)"
{
  "id": "img_8c1f0e2a9b7d4f3e6a5c2b1d",
  "object": "image_job",
  "status": "queued",
  "operation": "generation",
  "model": "gpt-image-2",
  "prompt": "A watercolor painting of a red fox in a snowy birch forest at dawn",
  "size": "1536x1024",
  "quality": null,
  "n": 1,
  "queue_position": 2,
  "charge_usd": null,
  "reserved_usd": 0.04,
  "error": null,
  "created_at": "2026-10-15T12:40:03.112Z",
  "started_at": null,
  "finished_at": null,
  "result": null
}
```

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](#reference-images). 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:

```bash title="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`](#several-images) 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:

```bash title="cURL"
curl -o fox.png https://lovelyrouter.com/v1/images/jobs/img_8c1f0e2a9b7d4f3e6a5c2b1d/content \
  -H "Authorization: Bearer $LOVELY_API_KEY"
```

| Status | Meaning |
| --- | --- |
| `queued` | Waiting for capacity. `queue_position` is its place in line. |
| `running` | Generating. |
| `succeeded` | Done. The image is in `result` for 7 days. |
| `failed` | No image. `error.message` says why; nothing was charged. |
| `cancelled` | Cancelled before it started; nothing was charged. |

| Endpoint | Does |
| --- | --- |
| `POST /v1/images/jobs` | Submit 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}/content` | The 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/jobs` | Your most recent jobs, newest first. `?limit=` up to 100; page with `?before=` set to `next_before`. |
| `POST /v1/images/jobs/{id}/cancel` | Cancel a job that is still `queued`. A running job can't be cancelled. |

The [playground](/app/playground) uses image jobs: switch it to **Images** to try prompts and edits without code.

## Limits {#limits}

- **Up to 10 images per request** with [`n`](#several-images). 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](/docs/models/rate-limits) applies to submitting images. Up to 20 images per account can be queued or generating at once.

| Status | When |
| --- | --- |
| `400` | Invalid 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. |
| `402` | Your balance can't cover the price of the images, or the key reached its spend limit. |
| `404` | Unknown model, or a model that doesn't generate images. |
| `429` | The 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. |
| `502` | Image generation failed on every channel. Nothing was charged. Retry. |
| `504` | The image isn't ready after 9 minutes. It keeps going; fetch it with the job id from `x-lovely-image-job`. |
