# POST /v1/images/generations

Generate images from a text prompt. This is the OpenAI Images API surface: point
`client.images.generate` at `https://dev-api.routerplus.com/v1` and the call works unchanged. The path
`https://dev-api.routerplus.com/images/generations` is an alias for the same handler, for SDKs that omit
the `/v1` prefix.

To try it without writing code, pick an image model in the [playground](/docs/playground).

By default the route returns one JSON response. Existing clients do not need to enable
streaming on the development API. Optional
`stream: true` returns keep-alives followed by one final image event. Images always come
back as base64; streaming does not enable partial previews.
Models served by Krea also accept reference images and editing controls through this same
JSON endpoint; see [Krea image models](#krea-image-models). The GPT Image models are served by OpenAI direct; the others
(Nano Banana Pro, FLUX.2 [max] and [pro], Seedream 5.0 Lite, Grok Imagine Image, Wan 2.7
Image Pro) by Replicate, billed at Replicate's published price — see
[Models served by Replicate](#models-served-by-replicate).

## Authentication

| Header | Format |
|---|---|
| `Authorization` | `Bearer tm_vk_...` (the standard OpenAI-style header) |
| `x-api-key` | `tm_vk_...` — also accepted, same key |

A missing or invalid key returns 401 with `error_type: "auth"`. Optionally send
`HTTP-Referer` and `X-Title` to identify your app (content-free attribution).

## Request

JSON body, capped at 10 MB (413 `request_too_large` above that). Every rule below is
checked **before** anything is reserved or dispatched, so a refused request has no ledger
row, no `x-tm-attempts` header, and no charge.

| Parameter | Type | Behavior |
|---|---|---|
| `model` | string, required | An image model id — see [Model discovery](#model-discovery). A dated pin (`-20260421`, `-2026-04-21`, `-latest`) resolves to its catalog family for routing and pricing, and your exact string goes upstream. A **chat** model here is a 400 naming the chat routes; a decision model a 400 naming `/v1/decisions`. An id that is not an image model anywhere is a 404. Omitted or empty is a 400 naming `model`. |
| `prompt` | string, required | Non-empty, at most 32 000 characters. |
| `n` | integer | 1 to 4 on the GPT Image models. Defaults to 1. Ours is stricter than the SDK's 10: four buffered images already approach the response cap. |
| `size` | string | `auto`, `1024x1024`, `1536x1024` or `1024x1536` on the GPT Image models. |
| `quality` | string | `auto`, `low`, `medium`, `high` — plus `xhigh` and `max` on `gpt-image-2.5-flare` and `gpt-image-2.5-sunburst`. |
| `background` | string | `auto`, `transparent`, `opaque` on the GPT Image models. |
| `moderation` | string | `auto` or `low` on the GPT Image models. |
| `output_format` | string | `png`, `jpeg` or `webp` on the GPT Image models. |
| `output_compression` | integer | 0 to 100 on the GPT Image models. |
| `user` | string | Forwarded verbatim. Anything but a string is a 400. |
| `response_format` | string | `b64_json` only, and it is a no-op. `url` is a 400 — see [Always base64](#always-base64). |
| `stream` | boolean | `true` opts into final-image SSE and requires `n: 1`. Absent, `null`, or `false` returns the ordinary JSON response. |
| `partial_images` | integer | Absent, `null`, or `0` only. Partial previews are not supported; other values return 400 before dispatch. |
| `provider` | object | Routing controls read by the gateway and never forwarded — `require_parameters` and the rest, see [Routing policies](/docs/routing-policies). |

The accepted values for `size`, `quality`, `background`, `moderation`, `output_format` and
`output_compression` are **per model** and published in `/api/models.json` under
`supported_parameters`. A value outside a model's declared set is a 400 whose message
starts with the field name and lists what that model accepts. One of these parameters that
a model does not declare at all is dropped and recorded, like any unknown key.

An explicit `null` means "not provided" for every optional parameter above, exactly like
omitting the key: the OpenAI SDKs serialise an unset optional as `null`, so
`client.images.generate(model=…, prompt=…, n=None, size=None)` works. A `null` is never
forwarded upstream and never recorded as a drop. `model` and `prompt` are required, so
`null` there is still a 400.

### Everything else

Fields declared by the selected model in `supported_parameters` are validated and forwarded,
including native Krea reference and editing parameters. Undeclared keys are **dropped and recorded**, never forwarded and
never guessed at (D8 §2). The dropped names are comma-joined in the `x-tm-dropped-params`
response header, stored on the attempt's ledger row, and visible at
`GET /v1/generation?id=`. To refuse instead of dropping, send
`"provider": {"require_parameters": true}` — a would-be drop then becomes a 400 before any
money is reserved. `provider` itself is consumed by the gateway and never forwarded.

### Always base64

The gateway always returns the image bytes inline as `b64_json`. There is no hosted URL:
storing an image would make this service stateful and would put buyer content on our
disks, which the [data policy](/docs/data-policy) forbids. `response_format: "url"` is
therefore a typed 400, not a quietly different answer. Decode the base64 yourself — the
examples below do it in three languages.

## Response

Without `stream: true`: HTTP 200, `application/json`, with OpenAI's own `ImagesResponse` relayed untouched except
for one added field:

```json
{
  "created": 1790000000,
  "data": [{ "b64_json": "iVBORw0KGgoAAAANSUhEUg..." }],
  "background": "opaque",
  "output_format": "png",
  "quality": "medium",
  "size": "1024x1024",
  "usage": {
    "input_tokens": 12,
    "input_tokens_details": { "text_tokens": 12, "image_tokens": 0 },
    "output_tokens": 1056,
    "total_tokens": 1068,
    "cost": 0.0423
  }
}
```

| Field | Meaning |
|---|---|
| `data[].b64_json` | The image, base64-encoded. One entry per image the provider rendered — normally `n`; the gateway relays what it received and bills what was reported. |
| `data[].revised_prompt` | The provider's rewritten prompt, when it sends one. Relayed in the response. See [Data policy](/docs/data-policy). |
| `usage.input_tokens` | Prompt tokens, billed at the model's `prompt` rate. |
| `usage.output_tokens` | Image output tokens, billed at the model's `completion` rate. |
| `usage.input_tokens_details` | Informational only — the bill is computed from the two counts above. |
| `usage.total_tokens` | `input_tokens + output_tokens`. |
| `usage.cost` | USD, the **full request debit** including any attempt that failed over before this one. `0` on BYOK. Absent on static dev keys. |

The chat surface's `prompt_tokens_details` is never injected here; an images response keeps
its own shape.

### Long-running JSON requests

Image generation can take several minutes. An ordinary request keeps its final HTTP
status, complete JSON body, headers, and billing behavior. No request or SDK changes are
required; every image count supported by the selected model is preserved, and errors
retain their normal final HTTP status.

The edge relay keeps its origin connection active while the image is pending, then
returns the original result. This addresses
[Cloudflare's default 125-second proxy read timeout](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/)
without storing image content or initiating another generation.

The gateway's generation budget is 450 seconds. Allow at least that long in your client's
overall timeout. Do not automatically retry a lost response: the provider may already
have accepted the render.

### Final-image streaming

Send `"stream": true` with `"n": 1` for a render that may take several minutes. After
validation the response starts as HTTP 200 with `Content-Type: text/event-stream`.
A comment is sent immediately, followed by a keep-alive comment every 15 seconds while
the image is pending. Comments keep the connection active; they are not partial images
or progress estimates. The final event follows the
[OpenAI image completion event](https://developers.openai.com/api/reference/resources/images/generation-streaming-events):

```text
: rendering

event: image_generation.completed
data: {"type":"image_generation.completed","created_at":1790000000,"b64_json":"iVBORw0KGgoAAAANSUhEUg...","usage":{"cost":0.04},"request_id":"gen_example","response_headers":{"x-tm-provider":"krea","x-tm-attempts":"1"}}

```

The connection then closes. There is one completed event, no partial-image events, and
no additional `[DONE]` event. `b64_json` is the final image. `created_at` is its response
timestamp. Output metadata such as `size`, `quality`, `background`, and `output_format`
is included only when it is known from the result; requested values are not presented
as measured output. `usage` retains the same accounting information as the JSON response,
including the exact `usage.cost` where available.

`request_id` and `response_headers` are RouterPlus extensions. Since the HTTP headers
have already been sent, `response_headers` carries final routing metadata such as
`x-tm-provider`, `x-tm-attempts`, and `x-tm-upstream-status`. The ordinary `x-request-id`
HTTP header identifies the request from the start.

Errors found before the stream starts retain their normal HTTP status and JSON body.
After it starts, an unsuccessful request ends with an `error` event instead of a
completion:

```text
event: error
data: {"type":"error","error":{"code":"upstream_error","message":"Image generation failed.","type":"api_error","metadata":{"error_type":"upstream_error"}},"request_id":"gen_example","status":502,"response_headers":{"x-tm-error-code":"upstream_error"}}

```

The `status` field is the failure's HTTP-equivalent status; the open SSE response itself
remains HTTP 200. Error details and final routing headers are carried in `error` and
`response_headers`. The OpenAI SDK raises streaming errors. Do not automatically resubmit
a disconnected stream or a timed-out request: the provider may already have accepted
the render. Use the request ID to inspect [generation usage](/docs/api-usage).

Choose SSE when you want an explicit event stream. It is optional on the development API:
normal JSON uses the edge relay described above. Neither transport
extends the generation budget or guarantees that a provider completes in time.

> [!NOTE]
> If the provider returns a 200 without a usage object, or with `input_tokens` and
> `output_tokens` both 0, the gateway bills `n` × the model's
> per-image ceiling with provenance `estimated` — the same amount the reservation held —
> and writes those numbers into `usage` so your copy of the bill still adds up. The
> provenance is visible at [GET /v1/generation](/docs/api-usage).

## Billing

Same two SKUs as chat, same integer micro-USD math ([Pricing & billing](/docs/pricing)):
text in bills at `prompt`, image out bills at `completion`.

- **Reserve**, before any provider is contacted: prompt bytes ÷ 4 at the `prompt` rate,
  plus `n` × the model's per-image ceiling at the `completion` rate, rounded up. The chat
  path's `max_tokens` default plays no part here.
- **Settle**, on the provider-reported counts, rounded down. The hold is released in full.

Worked numbers on `gpt-image-1` (ceiling 6240 tokens, $5 / $40 per million):

| Case | Amount |
|---|---|
| Reserve, `n: 1` | 6240 × $40/M ≈ $0.2496, plus the prompt estimate |
| Reserve, `n: 4` | 4 × 6240 × $40/M ≈ $0.9984, plus the prompt estimate |
| Settle, one medium 1024×1024 image | 1056 × $40/M = $0.04224, plus the prompt |

**All-or-nothing.** A generation either arrives whole or costs nothing. A failed attempt,
a response over the 32 MiB cap, and a 200 that carries no decodable image all settle at
zero. Once a 200 has been received the gateway never re-dispatches, so one request can
never buy two renders.

**A cancelled render is billed at what the provider reported.** If you hang up mid-render
the upstream call is *not* aborted: the render finishes, the usage is read, and the attempt
settles the provider's own numbers — never the reservation, never a pretend $0. Nothing is
written to your dead socket.

> [!WARNING]
> Reservations are large next to a trial balance. `n: 4` at a high quality holds about a
> dollar, and your balance must cover every reservation your org has in flight. A
> `429 insufficient_quota` on `n: 4` is correct behavior, not a bug — lower `n`, or add
> credits.

### Krea image models

Krea models have provider-qualified IDs such as `krea/krea-3` and
`krea/google/nano-banana-2.1`. Discover the current list and each model's
`supported_parameters`, `required_parameters`, and `request_defaults` at
`/api/models.json`. Structured parameters carry their JSON schema under `schema`.
Each request renders one image (`n: 1`). Krea's asynchronous submit/poll/download flow
is handled by the gateway; your request returns the completed OpenAI-style base64 image.

```json
{
  "model": "krea/krea-3",
  "prompt": "Change the mug to blue, keeping everything else the same.",
  "image_urls": ["data:image/png;base64,..."],
  "resolution": "2k",
  "thinking_mode": "fast",
  "regional_edit": true
}
```

Krea 3 accepts up to eight `image_urls`, optional `negative_prompt` (up to 1,024 characters),
and `seed`. Style references (`style_urls`) are not yet available in Krea's API;
supplying that field returns 400 before dispatch. Regional edits are experimental. Use `fast` or `high` thinking;
`ultra` requires `resolution: "2k"` and cannot be combined with regional editing.
For a custom canvas, provide both `width` and `height` (positive integers up to 4096),
and omit `resolution` and `aspect_ratio`. Exact pixel dimensions are not guaranteed:
Krea rounds the request to a supported canvas. Otherwise the default is 2K and automatic
framing. The optional `aspect_ratio` values are listed with the model.

For Krea 3 references, supply a Krea asset URL or an inline PNG, JPEG, or WebP data URI.
The gateway uploads inline images to Krea's assets endpoint before submitting the job.
The playground also accepts local files. References remain subject to the 10 MB request
limit. Other Krea models accept the external reference URLs their schemas declare.

Krea 3 is free during early access. Other enabled Krea models charge the fixed USD price
for the selected settings and references, shown on their model pages. The gateway holds
the model's published maximum, then settles the selected price only after a successful
render; it does not interpret Krea's in-app compute units as API dollars. Models without
verified price rules are unavailable until those rules are established.

Qwen Image 3 (`krea/alibaba/qwen-image-3`) and Seedream 5 Pro
(`krea/bytedance/seedream-5-pro`) price the requested **total pixels**, `width × height`:

| Model | Lower tier, inclusive | Above that area | References |
|---|---|---|---|
| Qwen Image 3 | ≤1,048,576 pixels: $0.04 | $0.075 | Up to 3; included in the price |
| Seedream 5 Pro | ≤2,359,296 pixels: $0.045 | $0.09 | Up to 10; first included, then $0.003 each |

For example, Qwen's 2048×512 image costs $0.04, just like 1024×1024.
Seedream's 3072×768 image costs $0.045 before additional references. Crossing the
area threshold increases the base price even when only one dimension changes.
Qwen defaults to 1024×1024; Seedream defaults to 1536×1536. Send positive whole-pixel
`width` and `height` to choose another size; the model's individual dimension bounds
are published in its parameter schema. The reservation ceilings remain $0.075 and
$0.117 respectively.

### Models served by Replicate

These are billed at Replicate's published price for the render, pass-through, with no
margin ([Pricing & billing](/docs/pricing#models-billed-at-the-providers-reported-cost)): the
reservation is `n` × the model's per-image ceiling (**Per image ≤**), and the charge is the
price below. They take `aspect_ratio` (or `size`) and `resolution` rather than `quality`.

| Model id | Made by | Shape | Listed price | Per image ≤ |
|---|---|---|---|---|
| `nano-banana-pro` | Google | `aspect_ratio`; `resolution` 1K, 2K, 4K | $0.15 at 1K or 2K, $0.30 at 4K | $0.30 |
| `flux-2-max` | Black Forest Labs | `aspect_ratio`; `resolution` 0.5MP, 1MP, 2MP, 4MP; `seed` | $0.04 a render + $0.03 per output megapixel | $0.19 |
| `flux-2-pro` | Black Forest Labs | as `flux-2-max` | $0.015 a render + $0.015 per output megapixel | $0.09 |
| `seedream-5-lite` | ByteDance | `aspect_ratio`; `resolution` 2K, 3K | $0.035 | $0.035 |
| `grok-imagine-image` | xAI | `aspect_ratio` | $0.02 | $0.02 |
| `wan-2.7-image-pro` | Alibaba | `size` (10 exact sizes up to 2048x2048, or `auto`); `n` up to 4, at most two 2048x2048 images' pixels per answer; `seed` | $0.03 | $0.03 |

Replicate counts FLUX.2's output megapixels in whole megapixels, rounded up: a 0.5MP render
bills as 1. Prices were read from Replicate on 2026-10-12.

Wan 2.7 Image Pro answers in PNG only, so one answer holds at most 8,388,608 pixels: `n` up
to 4 at the 1K sizes, 3 at 2048x1152 and 1152x2048, and 2 at 2048x1536, 1536x2048, 2048x2048
and `auto`. A larger `n` is refused with a 400 before anything renders, so it costs nothing.

### Recraft models

Recraft models are served directly by Recraft and have IDs of the form `recraft/<model>`, such as
`recraft/recraftv4_1` or `recraft/recraftv2_vector`. They are billed at Recraft's published
price, pass-through. The charge is the cost Recraft reports for the request.

| Family | Model ids | Raster | Vector | Pro raster (2K) | Pro vector |
|---|---|---|---|---|---|
| V4.1 Flash | `recraft/recraftv4_1_flash` | $0.007 | — | — | — |
| V4.1 | `recraft/recraftv4_1`, `…_vector`, `…_pro`, `…_pro_vector` | $0.035 | $0.08 | $0.21 | $0.30 |
| V4.1 Utility | `recraft/recraftv4_1_utility`, `…_vector`, `…_pro`, `…_pro_vector` | $0.035 | $0.08 | $0.21 | $0.30 |
| V4 Styles | `recraft/recraftv4_styles`, `…_vector`, `…_pro`, `…_pro_vector` | $0.035 | $0.05 | $0.10 | $0.12 |
| V4 | `recraft/recraftv4`, `…_vector`, `…_pro`, `…_pro_vector` | $0.04 | $0.08 | $0.25 | $0.30 |
| V3 | `recraft/recraftv3`, `recraft/recraftv3_vector` | $0.04 | $0.08 | — | — |
| V2 | `recraft/recraftv2`, `recraft/recraftv2_vector` | $0.022 | $0.044 | — | — |

Prices are per image. A request with `style_reference_urls` adds $0.005 once, because Recraft
creates a style from the references. The per-image ceiling includes that fee. Prices were read
from Recraft on 2026-10-16.

- **Output:**
  - `_vector` models return SVG. `data[].b64_json` holds the SVG text, and `output_format` is `svg`.
  - Raster models return WebP by default. Send `image_format: "png"` for PNG.
- **Images per request:** `n` takes 1 to 6.
- **`size`:** accepts the 14 aspect ratios (`1:1`, `16:9`, `6:10`, …) on every model. Raster models also accept their exact `WxH` sizes; Pro models use the 2K ones. Omit `size` to let Recraft choose from the prompt.
- **Styles:**
  - Every model except V4.1 Flash takes `style_id` (a Recraft style) or `style_reference_urls`: one to ten PNG, JPEG or WebP URLs or data URIs. Never both.
  - The response's `style_id` can be reused.
  - V4 Styles models require one of the two.
  - `style_match` is `flexible` or `precise` on V4 and newer, and `regular` on V2 and V3.
- **V2 and V3 only:** `style` (a curated style name such as `Photorealism`) and `negative_prompt`. Prompts on V2 and V3 are limited to 1,000 characters; on newer models, 10,000.
- **V3 only:** `text_layout`. `controls` takes `colors` and `background_color` on every model, plus `artistic_level` and `no_text` on V3.
- **Seed:** `seed` is passed to Recraft as its `random_seed`.

```json
{
  "model": "recraft/recraftv4_styles_vector",
  "prompt": "a lighthouse on a cliff",
  "size": "16:9",
  "style_reference_urls": ["https://example.com/brand-reference.png"]
}
```

## Errors

| Case | Status | `error_type` | Billed |
|---|---|---|---|
| A field outside the model's accepted values, `n` outside the model's limits, `stream: true` with `n` other than 1, nonzero `partial_images`, `response_format: "url"`, a chat or decision model on this route | 400 | `invalid_request` | no row |
| The model is listed but has no price row | 400 | `model_not_priced` | no row |
| Not an image model in the catalog | 404 | `model_unavailable` | no row |
| Request body over 10 MB | 413 | `request_too_large` | no row |
| Key RPM or a shared limit, balance too low, or a monthly spend cap | 429 | `rate_limit` / `insufficient_quota` | no row |
| OpenAI moderation refusal (`moderation_blocked`) | the provider's, usually 400 | `content_policy` | $0; never rerouted, never a health strike |
| Any other provider 4xx | the provider's | `upstream_error` | $0; no failover |
| Provider 5xx, a failed connection, or the 450 s budget | 5xx relayed / 502 / 504 | `upstream_error` / `model_unavailable` | $0 per failed attempt; fails over like chat |
| Response over the 32 MiB cap | 502 | `gateway_error` | $0; no failover |
| A 200 that is unparseable or has no `b64_json` | 502 | `upstream_error` | $0; no failover |
| The budget ran out while reading the image body | 504 | `upstream_error` | $0; no failover |
| Every deployment serving the model is cooling down | 503 | `gateway_error` | nothing dispatched; `retry-after: 5` |
| The ledger journal, the shared limit store, or the admission lease is unavailable | 503 | `gateway_error` | nothing charged |

Bodies follow the OpenAI error envelope with the canonical class in
`error.metadata.error_type`, and the class is on every failure in `x-tm-error-code`. A
provider's own error body is never relayed; its status is in `x-tm-upstream-status`. The
full taxonomy and remediation table is in [Errors](/docs/errors).
For a failure after SSE begins, these details are in the terminal `error` event; the
HTTP status can no longer change. See [Final-image streaming](#final-image-streaming).

The reverse refusal also holds: an image model sent to `POST /v1/chat/completions`,
`POST /v1/messages` or `POST /v1/decisions` is a 400 `invalid_request` naming
`/v1/images/generations`.

## Limits

| Limit | Value |
|---|---|
| Images per request | JSON: model limit up to `n` = 4; SSE: `n` = 1 |
| Prompt length | 32 000 characters |
| Request body | 10 MB → 413 `request_too_large` |
| Upstream response | 32 MiB → 502 `gateway_error`, nothing billed. |
| Generation budget | 450 s, covering the body read as well as the render |
| Streaming | Opt-in final-image SSE; no partial previews |
| Admission | one slot held for the whole render (typically 10–60 s) |
| Providers | OpenAI direct for the GPT Image models — never Azure, Bedrock or an Anthropic deployment |

## Response headers

| Header | When | Meaning |
|---|---|---|
| `x-request-id` | every call | the id `GET /v1/generation?id=` audits |
| `x-tm-provider` | served requests | deployment that rendered the image |
| `x-tm-attempts` | after ≥ 1 dispatch | physical dispatches, failovers included |
| `x-tm-upstream-status` | when a provider answered | the provider's own HTTP status |
| `x-tm-dropped-params` | when non-empty | comma-joined names of parameters the gateway stripped |
| `x-tm-upstream-model` | when the route knows the model by another id | the id the gateway actually sent |
| `x-tm-served-by` | when a route reports one | the provider that ran the request |
| `x-tm-route-plan-id` | billed requests | the route plan that chose the deployment |
| `x-tm-admission-mode`, `x-tm-remaining-rpm`, `x-tm-remaining-tpm` | billed requests | how the request was admitted and the headroom left — see [Limits and capacity](/docs/admission) |
| `x-tm-error-code` | every failure | the canonical error class |
| `x-tm-error-origin`, `x-tm-limit-scope`, `x-tm-limit-kind` | limit, provider and infrastructure failures | where it came from, and the limit that refused it |

There is no image-specific header. Browsers can read all of these except `x-tm-error-code`
and `x-tm-route-plan-id`, which are not CORS-exposed.
For SSE, final routing headers are also returned in the completed event's
`response_headers` extension.

## Examples

curl — keep a slow Qwen render connected and print its final SSE event:

```bash
curl -N --max-time 480 https://dev-api.routerplus.com/v1/images/generations \
  -H "Authorization: Bearer $TM_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"krea/alibaba/qwen-image-3","prompt":"a red bicycle on a white background","width":1024,"height":1024,"n":1,"stream":true,"partial_images":0}'
```

Python — consume the final image through the official OpenAI SDK, with automatic retries
disabled so a lost response does not submit another render:

```python
import base64
import os
from pathlib import Path
from openai import OpenAI

client = OpenAI(
    base_url="https://dev-api.routerplus.com/v1",
    api_key=os.environ["TM_API_KEY"],
    timeout=480.0,
    max_retries=0,
)

stream = client.images.generate(
    model="krea/alibaba/qwen-image-3",
    prompt="a red bicycle on a white background",
    n=1,
    stream=True,
    partial_images=0,
    extra_body={"width": 1024, "height": 1024},
)
for event in stream:
    if event.type == "image_generation.completed":
        Path("out.image").write_bytes(base64.b64decode(event.b64_json))
        usage = event.usage.model_dump() if event.usage is not None else {}
        print(usage.get("cost"))
```

The file contains the provider's actual image bytes. Use `event.output_format` for a
matching file extension when that metadata is available; otherwise inspect the file type.

curl — decode the first image straight to a file:

```bash
curl -s https://dev-api.routerplus.com/v1/images/generations \
  -H "Authorization: Bearer $TM_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"gpt-image-1","prompt":"a red bicycle on a white background","size":"1024x1024","quality":"medium"}' \
  | jq -r '.data[0].b64_json' | base64 -d > out.png
```

Python — the official `openai` SDK with the base URL swapped:

```python
import base64
import os
from openai import OpenAI

client = OpenAI(base_url="https://dev-api.routerplus.com/v1", api_key=os.environ["TM_API_KEY"])

r = client.images.generate(
    model="gpt-image-1",
    prompt="a red bicycle on a white background",
    size="1024x1024",
    quality="medium",
)
with open("out.png", "wb") as f:
    f.write(base64.b64decode(r.data[0].b64_json))
print(r.usage.model_dump()["cost"])  # the exact ledger debit, in USD
```

TypeScript — the official `openai` package:

```typescript
import { writeFileSync } from "node:fs";
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://dev-api.routerplus.com/v1", apiKey: process.env.TM_API_KEY });

const r = await client.images.generate({
  model: "gpt-image-1",
  prompt: "a red bicycle on a white background",
  size: "1024x1024",
  quality: "medium",
});

const b64 = r.data?.[0]?.b64_json ?? "";
writeFileSync("out.png", Buffer.from(b64, "base64"));
console.log(r.usage); // input/output tokens and cost (USD)
```

## Request data

Prompts, image bytes and any `revised_prompt` follow the [Data policy](/docs/data-policy).
Process logs, telemetry and the billing ledger contain metadata only. Telemetry records
the response **byte size** so throughput stays measurable.

## Model discovery

```bash
curl -s "https://dev-api.routerplus.com/v1/models?output_modalities=image" \
  -H "Authorization: Bearer $TM_API_KEY"
```

Every row of `GET /v1/models` carries `architecture.output_modalities` — `["text"]`,
`["image"]` or `["decisions"]` — and the `?output_modalities=` filter takes a comma list of
those values.
The public feed `https://dev.routerplus.com/api/models.json` carries `output_modalities`, the full
`supported_parameters` descriptor map for image models, and `max_output_tokens` as the
per-image ceiling. See [GET /v1/models](/docs/api-models) and
[Models & catalog](/docs/models).

## BYOK

Image generation works through an `openai` provider connection. List the exact image ids
you intend to send on the connection — matching is literal, and an id the connection does
not carry is a `404 model_unavailable`. The model's metadata (ceiling, descriptors) still
comes from the house catalog. `usage.cost` is `0`, the marketplace charge is zero, and
OpenAI bills your own account for the render. See [Bring your own key](/docs/byok).

See also: [Models & catalog](/docs/models) · [Pricing & billing](/docs/pricing) ·
[Errors](/docs/errors) · [Wire compatibility](/docs/compat)
