Console
API reference/POST /v1/images/generations

POST /v1/images/generations

OpenAI Images API: base64 in the body, all-or-nothing.

/llms.txt

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.

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. 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.

Authentication

HeaderFormat
AuthorizationBearer tm_vk_... (the standard OpenAI-style header)
x-api-keytm_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.

ParameterTypeBehavior
modelstring, requiredAn image model id — see 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.
promptstring, requiredNon-empty, at most 32 000 characters.
ninteger1 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.
sizestringauto, 1024x1024, 1536x1024 or 1024x1536 on the GPT Image models.
qualitystringauto, low, medium, high — plus xhigh and max on gpt-image-2.5-flare and gpt-image-2.5-sunburst.
backgroundstringauto, transparent, opaque on the GPT Image models.
moderationstringauto or low on the GPT Image models.
output_formatstringpng, jpeg or webp on the GPT Image models.
output_compressioninteger0 to 100 on the GPT Image models.
userstringForwarded verbatim. Anything but a string is a 400.
response_formatstringb64_json only, and it is a no-op. url is a 400 — see Always base64.
streambooleantrue opts into final-image SSE and requires n: 1. Absent, null, or false returns the ordinary JSON response.
partial_imagesintegerAbsent, null, or 0 only. Partial previews are not supported; other values return 400 before dispatch.
providerobjectRouting controls read by the gateway and never forwarded — require_parameters and the rest, see 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 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
  }
}
FieldMeaning
data[].b64_jsonThe 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_promptThe provider's rewritten prompt, when it sends one. Relayed in the response. See Data policy.
usage.input_tokensPrompt tokens, billed at the model's prompt rate.
usage.output_tokensImage output tokens, billed at the model's completion rate.
usage.input_tokens_detailsInformational only — the bill is computed from the two counts above.
usage.total_tokensinput_tokens + output_tokens.
usage.costUSD, 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 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:

: 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:

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.

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.

Billing

Same two SKUs as chat, same integer micro-USD math (Pricing & billing): 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):

CaseAmount
Reserve, n: 16240 × $40/M ≈ $0.2496, plus the prompt estimate
Reserve, n: 44 × 6240 × $40/M ≈ $0.9984, plus the prompt estimate
Settle, one medium 1024×1024 image1056 × $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:

ModelLower tier, inclusiveAbove that areaReferences
Qwen Image 3≤1,048,576 pixels: $0.04$0.075Up to 3; included in the price
Seedream 5 Pro≤2,359,296 pixels: $0.045$0.09Up 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): 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 idMade byShapeListed pricePer image ≤
nano-banana-proGoogleaspect_ratio; resolution 1K, 2K, 4K$0.15 at 1K or 2K, $0.30 at 4K$0.30
flux-2-maxBlack Forest Labsaspect_ratio; resolution 0.5MP, 1MP, 2MP, 4MP; seed$0.04 a render + $0.03 per output megapixel$0.19
flux-2-proBlack Forest Labsas flux-2-max$0.015 a render + $0.015 per output megapixel$0.09
seedream-5-liteByteDanceaspect_ratio; resolution 2K, 3K$0.035$0.035
grok-imagine-imagexAIaspect_ratio$0.02$0.02
wan-2.7-image-proAlibabasize (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.

FamilyModel idsRasterVectorPro raster (2K)Pro vector
V4.1 Flashrecraft/recraftv4_1_flash$0.007———
V4.1recraft/recraftv4_1, …_vector, …_pro, …_pro_vector$0.035$0.08$0.21$0.30
V4.1 Utilityrecraft/recraftv4_1_utility, …_vector, …_pro, …_pro_vector$0.035$0.08$0.21$0.30
V4 Stylesrecraft/recraftv4_styles, …_vector, …_pro, …_pro_vector$0.035$0.05$0.10$0.12
V4recraft/recraftv4, …_vector, …_pro, …_pro_vector$0.04$0.08$0.25$0.30
V3recraft/recraftv3, recraft/recraftv3_vector$0.04$0.08——
V2recraft/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

CaseStatuserror_typeBilled
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 route400invalid_requestno row
The model is listed but has no price row400model_not_pricedno row
Not an image model in the catalog404model_unavailableno row
Request body over 10 MB413request_too_largeno row
Key RPM or a shared limit, balance too low, or a monthly spend cap429rate_limit / insufficient_quotano row
OpenAI moderation refusal (moderation_blocked)the provider's, usually 400content_policy$0; never rerouted, never a health strike
Any other provider 4xxthe provider'supstream_error$0; no failover
Provider 5xx, a failed connection, or the 450 s budget5xx relayed / 502 / 504upstream_error / model_unavailable$0 per failed attempt; fails over like chat
Response over the 32 MiB cap502gateway_error$0; no failover
A 200 that is unparseable or has no b64_json502upstream_error$0; no failover
The budget ran out while reading the image body504upstream_error$0; no failover
Every deployment serving the model is cooling down503gateway_errornothing dispatched; retry-after: 5
The ledger journal, the shared limit store, or the admission lease is unavailable503gateway_errornothing 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. 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.

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

LimitValue
Images per requestJSON: model limit up to n = 4; SSE: n = 1
Prompt length32 000 characters
Request body10 MB → 413 request_too_large
Upstream response32 MiB → 502 gateway_error, nothing billed.
Generation budget450 s, covering the body read as well as the render
StreamingOpt-in final-image SSE; no partial previews
Admissionone slot held for the whole render (typically 10–60 s)
ProvidersOpenAI direct for the GPT Image models — never Azure, Bedrock or an Anthropic deployment

Response headers

HeaderWhenMeaning
x-request-idevery callthe id GET /v1/generation?id= audits
x-tm-providerserved requestsdeployment that rendered the image
x-tm-attemptsafter ≥ 1 dispatchphysical dispatches, failovers included
x-tm-upstream-statuswhen a provider answeredthe provider's own HTTP status
x-tm-dropped-paramswhen non-emptycomma-joined names of parameters the gateway stripped
x-tm-upstream-modelwhen the route knows the model by another idthe id the gateway actually sent
x-tm-served-bywhen a route reports onethe provider that ran the request
x-tm-route-plan-idbilled requeststhe route plan that chose the deployment
x-tm-admission-mode, x-tm-remaining-rpm, x-tm-remaining-tpmbilled requestshow the request was admitted and the headroom left — see Limits and capacity
x-tm-error-codeevery failurethe canonical error class
x-tm-error-origin, x-tm-limit-scope, x-tm-limit-kindlimit, provider and infrastructure failureswhere 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
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
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
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
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
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. 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 and Models & catalog.

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.

See also: Models & catalog · Pricing & billing · Errors · Wire compatibility

Markdown source for agents: /docs/api-images.md · index at /llms.txt