# Image generation

> Create images from a text prompt and optional reference images with POST /images, returned as base64 in one response or as a stream.

Source: https://0.0.0.0:3000/docs/multimodal/image-generation
Documentation index: https://0.0.0.0:3000/docs/llms.txt

`POST /images` generates one or more images from a prompt, optionally guided by reference images. Images always come back as base64 data with their media type; there are no hosted URLs. Image generation is available on models whose endpoints offer it: find them with `GET /models?output_modalities=image`.

```endpoint
POST /api/v1/images
```

Image prompts preserve JSON-escaped control characters without stripping. Prompt and request size limits still apply.

## Generate an image

```request
{
  "method": "POST",
  "path": "/images",
  "body": {
    "model": "author/image-model",
    "prompt": "A lighthouse on a basalt cliff at dusk, watercolor, soft light",
    "size": "1024x1024",
    "output_format": "png"
  }
}
```

```json title="Response"
{
  "created": 1790000000,
  "data": [
    {
      "b64_json": "iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAIAAADwf7zU...",
      "media_type": "image/png"
    }
  ],
  "usage": {
    "input_images": 0,
    "output_images": 1,
    "input_audio_seconds": 0,
    "output_audio_seconds": 0,
    "input_characters": 0,
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0,
    "cost": 0.04
  }
}
```

The response carries an `X-Generation-Id` header; use it with [`GET /generation`](/docs/api-reference/chat/get-generation) to look up the request later.

:::note The OpenAI SDK image methods don't apply
The OpenAI SDKs send image requests to `/images/generations`, which doesn't exist here. Call `POST /images` with any HTTP client.
:::

## Request fields

:::fields Request body
- `model` `string` required — Canonical ID of an image-generation model, such as `author/image-model`.
- `prompt` `string` required — What to draw. Must not be blank; at most 1 MiB.
- `n` `integer` default `1` — Number of images, from 1 to 10.
- `size` `string` — `auto`, or `WIDTHxHEIGHT` in pixels with each side from 64 to 8192, such as `1024x1536`. Generated images may not exceed 16,777,216 pixels (4096 × 4096).
- `resolution` `string` — Resolution label, at most 16 bytes. Accepted values depend on the model.
- `aspect_ratio` `string` — Aspect ratio label such as `16:9`, at most 16 bytes. Accepted values depend on the model.
- `quality` `string` — `auto`, `low`, `medium` or `high`.
- `output_format` `string` — `png`, `jpeg` or `webp`.
- `background` `string` — `auto`, `opaque` or `transparent`.
- `output_compression` `integer` — Compression level from 0 to 100, typically for `jpeg` and `webp`.
- `seed` `integer` — 64-bit seed for models that support reproducible sampling.
- `stream` `boolean` default `false` — Stream Server-Sent Events instead of one JSON response.
- `input_references` `array` — Up to 16 reference images. See [Reference images](#reference-images).
- `provider` `object` — Routing preferences, as for chat. See [Provider routing](/docs/routing/provider-routing).
:::

Unknown fields, including `response_format` and `user`, are rejected with 400. The request body is limited to 10 MiB.

## Controls and routing

Image models differ in the controls they support. Every control you set, including `n` and `size`, is forwarded, and an endpoint that can't apply one rejects the request rather than ignoring it. To route only to endpoints that declare every control you set, add `provider.require_parameters: true`:

```request
{
  "method": "POST",
  "path": "/images",
  "body": {
    "model": "author/image-model",
    "prompt": "A flat vector icon of a paper airplane",
    "size": "1024x1024",
    "background": "transparent",
    "output_format": "png",
    "provider": { "require_parameters": true }
  }
}
```

If no endpoint declares them, the request fails with 503 `provider_unavailable`. Each endpoint lists its controls in `supported_parameters` of [`GET /models/{author}/{slug}/endpoints`](/docs/api-reference/models/list-model-endpoints).

## Reference images

Each item in `input_references` is a URL string or an object `{"image_url": "..."}`. Unlike chat, the object holds the URL directly, not in a nested `url` field:

```json title="Request body with references"
{
  "model": "author/image-model",
  "prompt": "Place the product from the first image on the table in the second image",
  "input_references": [
    "https://example.com/images/product.png",
    { "image_url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..." }
  ]
}
```

References follow the same rules as [image inputs](/docs/multimodal/images): HTTPS or base64 `data:` URLs, PNG, JPEG, GIF or WebP, at most 8 MiB each and 24 MiB in total. A request with references needs an endpoint that accepts `image` input, and each reference counts as one `input_images` unit.

## Stream progress events

With `"stream": true`, the response is an SSE stream. Each frame is one `data:` line holding a JSON event, and the stream ends with `data: [DONE]`:

```sse title="Stream"
data: {"type":"image_generation.partial","index":0,"b64_json":"iVBORw0KGgoAAAANSUhEUgAA...","media_type":"image/png"}

data: {"type":"image_generation.completed","index":0,"b64_json":"iVBORw0KGgoAAAANSUhEUgAB...","media_type":"image/png"}

data: {"type":"image_generation.usage","usage":{"input_images":0,"output_images":1,"input_audio_seconds":0,"output_audio_seconds":0,"input_characters":0,"prompt_tokens":0,"completion_tokens":0,"total_tokens":0,"cost":0.04}}

data: [DONE]
```

| Event `type` | Fields | Meaning |
| --- | --- | --- |
| `image_generation.partial` | `index`, `b64_json`, `media_type` | A preview of image `index`, if the model produces previews |
| `image_generation.completed` | `index`, `b64_json`, `media_type` | A final image |
| `image_generation.usage` | `usage` | Final usage, sent once after all images |
| `image_generation.error` | `error` | The request failed after the stream started |

`index` identifies which of the `n` images an event belongs to, from 0. A failure after the stream starts keeps HTTP status 200 and sends one error event, then `[DONE]`, with no usage event:

```sse title="In-band error"
data: {"type":"image_generation.error","error":{"code":"provider_unavailable","message":"provider became unavailable","status":503}}

data: [DONE]
```

`error.code` is an error type such as `provider_unavailable`, `rate_limit_exceeded`, `timeout` or `cancelled`, and `error.status` the matching HTTP status. Look up what was delivered and charged with `GET /generation`. Failures before the first event return an ordinary JSON error.

Frames that carry full images can be several megabytes long; read them with a line reader that has no small line-length limit.

## Save the images

Decode `b64_json` and write the bytes with the extension that matches `media_type`:

:::code-group
```python title="Python"
import base64
import os

import requests

EXTENSIONS = {"image/png": "png", "image/jpeg": "jpg", "image/webp": "webp"}

response = requests.post(
    "https://api.numes.ai/api/v1/images",
    headers={"Authorization": f"Bearer {os.environ['NUMES_API_KEY']}"},
    json={"model": "author/image-model", "prompt": "A lighthouse at dusk, watercolor", "n": 2},
    timeout=630,
)
response.raise_for_status()
result = response.json()

for index, image in enumerate(result["data"]):
    path = f"lighthouse-{index}.{EXTENSIONS[image['media_type']]}"
    with open(path, "wb") as file:
        file.write(base64.b64decode(image["b64_json"]))

print("cost", result["usage"]["cost"], "generation", response.headers["X-Generation-Id"])
```

```typescript title="TypeScript"
import { writeFile } from "node:fs/promises";

const EXTENSIONS: Record<string, string> = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };

const response = await fetch("https://api.numes.ai/api/v1/images", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NUMES_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ model: "author/image-model", prompt: "A lighthouse at dusk, watercolor", n: 2 }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);

const result = (await response.json()) as {
  data: { b64_json: string; media_type: string }[];
  usage: { cost: number };
};
for (const [index, image] of result.data.entries()) {
  await writeFile(`lighthouse-${index}.${EXTENSIONS[image.media_type]}`, Buffer.from(image.b64_json, "base64"));
}
console.log("cost", result.usage.cost, "generation", response.headers.get("X-Generation-Id"));
```
:::

When streaming, save each `image_generation.completed` event as it arrives:

```python title="stream_images.py"
import base64
import json
import os

import requests

EXTENSIONS = {"image/png": "png", "image/jpeg": "jpg", "image/webp": "webp"}

with requests.post(
    "https://api.numes.ai/api/v1/images",
    headers={"Authorization": f"Bearer {os.environ['NUMES_API_KEY']}"},
    json={"model": "author/image-model", "prompt": "A red fox in fresh snow", "stream": True},
    stream=True,
    timeout=630,
) as response:
    response.raise_for_status()
    for line in response.iter_lines(decode_unicode=True):
        if not line or not line.startswith("data: "):
            continue
        payload = line[len("data: "):]
        if payload == "[DONE]":
            break
        event = json.loads(payload)
        if event["type"] == "image_generation.completed":
            with open(f"fox-{event['index']}.{EXTENSIONS[event['media_type']]}", "wb") as file:
                file.write(base64.b64decode(event["b64_json"]))
        elif event["type"] == "image_generation.usage":
            print("cost", event["usage"]["cost"])
        elif event["type"] == "image_generation.error":
            raise RuntimeError(f"{event['error']['code']}: {event['error']['message']}")
```

## Billing

| Unit | Charged for |
| --- | --- |
| `output_images` | Each final image delivered to you, at the endpoint's output image rate |
| `input_images` | Each reference image, at the endpoint's input image rate |
| Request | The endpoint's per-request price, if it has one |

Before the request runs, funds for all `n` images are held, so your balance must cover `n` images even if fewer are delivered. `GET /generation` is the authoritative record of what was charged. See [Usage accounting](/docs/account/usage-accounting).

## Errors

| Status | `error_type` | Cause |
| --- | --- | --- |
| 400 | `invalid_request` | Blank or oversized prompt, `n` outside 1–10, malformed `size`, unknown enum value, more than 16 references, invalid reference image, unknown field |
| 402 | `insufficient_credits` | Balance or key limit doesn't cover `n` images |
| 408 | `timeout` | The request didn't complete in time |
| 413 | `payload_too_large` | Body larger than 10 MiB |
| 429 | `rate_limited` | Key or account rate limit; wait for `Retry-After` |
| 502 | `provider_unavailable` | The provider returned an invalid image |
| 503 | `provider_unavailable` | No eligible endpoint for the model, or none declares a required control |
| 503 | `provider_overloaded` | Every eligible endpoint is at capacity |

See [Errors](/docs/api-reference/errors) for the envelope and retry guidance.

## Next steps

:::cards
- {image} [Image inputs](/docs/multimodal/images) Send images to vision models for analysis.
- {network} [Provider routing](/docs/routing/provider-routing) Require parameters, order endpoints and cap prices.
- {receipt} [Usage accounting](/docs/account/usage-accounting) Read usage and cost.
- {code} [API reference](/docs/api-reference/media/create-image) Full schema for `POST /images`.
:::
