# Image generation

> Generate and edit images with POST /v1/images/generations and /v1/images/edits — parameters, sizes, response shape, and per-image billing.

Source: https://infro.io/docs/modalities/images

> **Note:** INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See [Early access](https://infro.io/early-access).

Image generation is synchronous. You post a prompt, the request stays open while a provider renders, and the response carries the finished images. Expect a few seconds on fast models and up to a minute on the highest-quality ones.

Two endpoints share one request shape: `/v1/images/generations` creates images from text, `/v1/images/edits` modifies an image you upload. Both accept the universal `fallbacks`, `logging`, and `metadata` extensions, both name the `provider` and `connection` that served the request, and both report estimated provider cost in `usage.cost`. Image requests are served by connections to OpenAI, Azure OpenAI, Google AI (Gemini), Google Vertex AI, Together AI, OpenRouter, Runway, KIE, LaoZhang API, fal.ai, Replicate and OpenAI-compatible endpoints; INFRO translates the request to each provider's own API. Video works differently — it is asynchronous, see [Video generation](https://infro.io/docs/modalities/video).

```text
POST https://api.infro.io/v1/images/generations
POST https://api.infro.io/v1/images/edits
```

## Request body

- `model` (string, required) — Image model ID in `vendor/model-name` form, e.g. `bfl/flux-2-pro`, `ideogram/ideogram-3`, or `google/nano-banana-pro`, or the slug of one of your routes. Browse IDs and vendor list rates in the [catalog](https://infro.io/docs/models). The model must be served by one of your connected providers.
- `prompt` (string, required) — What to render, up to 32,000 characters. Prompt conventions differ by model — `recraft/recraft-v3` responds to style keywords, `ideogram/ideogram-3` is the strongest at rendering text inside the image. The provider may enforce a shorter limit.
- `n` (integer) — How many images to request, `1`–`10`. Defaults to `1`. Sent to the provider as-is; a provider may return fewer, and `usage.images` counts what came back.
- `size` (string) — Exact pixels, `WIDTHxHEIGHT`, e.g. `1024x1024`. Passed to the provider unchanged; omit it and the provider's default applies. Send `size` or `resolution` and `aspect_ratio`, not both. See the note on sizes below.
- `resolution` (string) — A resolution tier: `512`, `768`, `1K`, `1.5K`, `2K`, `3K` or `4K`. Each model takes its own tiers on each provider (Nano Banana 2 Lite renders `1K` only; Nano Banana Pro takes `1K`, `2K` and `4K`), and INFRO sends the tier the way that provider spells it, as pixels where the provider takes pixels. A tier the model cannot render on a target is refused before that target is called; see below.
- `aspect_ratio` (string) — `W:H`, e.g. `16:9` or `9:16`. Checked against what the model takes on each target, like `resolution`.
- `quality` (string) — `low`, `medium`, `high` or `auto`; GPT Image 2.5 also takes `xhigh` and `max`. The legacy `standard` reads as `medium`. Sent only where the model has a quality setting (GPT Image, Grok Imagine, Ideogram's rendering speed) and dropped elsewhere; a value outside the model's own list is refused (Grok Imagine takes `low` or `medium`).
- `response_format` (string) — `url` (default) returns a signed link to a copy INFRO stores for 24 hours; `b64_json` returns the image inline as base64. Base64 payloads are several megabytes each — prefer `url` unless your runtime cannot make a second fetch.
- `seed` (integer) — Best-effort deterministic sampling, forwarded on `/generations` only, and only to providers whose model takes a seed; elsewhere it is dropped.

## Settings each model takes

The same model takes different settings on different providers, so INFRO keeps a record of what each model accepts on each provider it serves, read from that provider's own documentation. Before calling a target, the gateway checks `resolution`, `aspect_ratio` and `quality` against that record. A setting the target cannot take is never sent: the next target in the route is tried, and when no target can take it the request is a `400 invalid_request_error` naming what the model accepts, for example `Nano Banana 2 Lite on LaoZhang API renders at 1K, not 2K.` No provider is called and nothing is charged. A model with no record is passed through unchanged. The console's Playground offers only these settings.

## Reference images

`references` is a list of images that guide the render, each `{ "source": "https://… or data:image/…", "role": "character" | "style" | "object" | "subject" }` (role defaults to `subject`). INFRO fetches each image once (URLs pass the same outbound checks as every re-host; SVG and HTML are refused), then checks the count and roles against what the model takes on each target — Nano Banana Pro takes 14 (6 objects, 5 characters, 3 styles), GPT Image 16, FLUX.2 [pro] on fal 9 — and sends them in that provider's own field. A target that cannot take them is skipped; when none can, the request is a `400` naming what the model takes. `/v1/images/edits` also takes several input images (`image[]`, up to 16, the first is the one edited); where edit images and references share one list on the provider, their total counts against it.

## Upscale an image

`POST /v1/images/upscale` takes one image (`multipart/form-data`: `image`, `model`, and `scale` where the model takes a factor) and returns it larger, in the same response shape as generations. Each model takes its own factors on each provider (Topaz on fal: 2, 3 or 4; Magnific on Runway: 2, 4, 8 or 16; Recraft Crisp: none, it picks); a factor a target cannot take is refused before any call, naming what it takes. Priced per image.

## INFRO extensions

The same top-level extensions available on [chat completions](https://infro.io/docs/api/chat-completions) apply here, with identical semantics.

- `routing` (object) — Legacy routing fields do not rank or region-constrain customer-owned connections. Configure a named route for provider selection; see [Routing](https://infro.io/docs/features/routing).
- `fallbacks` (array) — Up to five model IDs to try, in order, if every connection for `model` fails, e.g. `["bfl/flux-2-dev", "qwen/qwen-image-3"]`. A fallback must be an image model too. An ID that is not a model this organization can call is refused with `400 unknown_fallback_model`. See [Failover & fallbacks](https://infro.io/docs/features/fallbacks).
- `logging` (boolean) — Set `false` to keep this request's prompt out of payload capture when your organization has it turned on. Rendered images are never captured. Request metadata (model, provider, status, estimated cost) is still recorded. See [Privacy & data](https://infro.io/docs/privacy).

## Example request

**curl**

```bash
curl https://api.infro.io/v1/images/generations \
  -H "Authorization: Bearer $INFRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bfl/flux-2-pro",
    "prompt": "An isometric cutaway of a small data center, soft studio lighting",
    "size": "1024x1024",
    "n": 1,
    "fallbacks": ["ideogram/ideogram-3"]
  }'
```

**Python**

```python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.infro.io/v1",
    api_key=os.environ["INFRO_API_KEY"],
)

image = client.images.generate(
    model="bfl/flux-2-pro",
    prompt="An isometric cutaway of a small data center, soft studio lighting",
    size="1024x1024",
    n=1,
    extra_body={"fallbacks": ["ideogram/ideogram-3"]},
)

print(image.data[0].url)
```

**TypeScript**

```typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.infro.io/v1",
  apiKey: process.env.INFRO_API_KEY,
});

const image = await client.images.generate({
  model: "bfl/flux-2-pro",
  prompt: "An isometric cutaway of a small data center, soft studio lighting",
  size: "1024x1024",
  n: 1,
  // @ts-expect-error INFRO extension
  fallbacks: ["ideogram/ideogram-3"],
});

console.log(image.data[0].url);
```

**Dart**

```dart
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;

Future<void> main() async {
  final apiKey = Platform.environment['INFRO_API_KEY']!;

  final res = await http.post(
    Uri.parse('https://api.infro.io/v1/images/generations'),
    headers: {
      'Authorization': 'Bearer $apiKey',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'model': 'bfl/flux-2-pro',
      'prompt': 'An isometric cutaway of a small data center, soft studio lighting',
      'size': '1024x1024',
      'n': 1,
      'fallbacks': ['ideogram/ideogram-3'],
    }),
  );

  final body = jsonDecode(res.body) as Map<String, dynamic>;
  final images = body['data'] as List<dynamic>;
  print(images.first['url']);
}
```

## Response

```json
{
  "id": "req_5c3f9a21",
  "created": 1755950000,
  "model": "bfl/flux-2-pro",
  "route": "primary",
  "route_name": null,
  "provider": "together",
  "connection": "conn_8b21c4e0",
  "data": [
    {
      "url": "https://api.infro.io/media/img/2026-09-24/org_.../req_5c3f9a21-0.png?exp=1756036400&sig=...",
      "b64_json": null
    }
  ],
  "usage": {
    "images": 1,
    "cost": 0.03,
    "price_source": "list"
  }
}
```

- `data[].url` (string) — A signed link to a copy of the image INFRO stores, or `null` when `response_format` is `b64_json`. The link stops working 24 hours after the request.
- `data[].b64_json` (string) — Base64-encoded image bytes, or `null` when `response_format` is `url`. Exactly one of the two fields is populated per entry.
- `route` (string) — INFRO extension: which position in the resolved chain rendered the images — `primary`, or `standby_a` and so on after a failover. `route_name` is the slug of the route used, or `null` for a direct model request.
- `provider / connection` (string) — INFRO extension: the provider kind and the ID of your connection that served the request.
- `usage.images` (integer) — Number of images the provider returned, which is what the estimate is priced on. Usually equals `n`.
- `usage.cost` (number | null) — INFRO extension: an estimate of what your provider will bill you for this request, with `usage.price_source` saying which rate it used. `null` when the model is unpriced.

> **Warning:** Image URLs expire 24 hours after the request. Download the bytes or copy them to your own storage if you need them longer — the same 24-hour link applies to [async job outputs](https://infro.io/docs/api/jobs).

## Editing an image

`/v1/images/edits` takes `multipart/form-data` rather than JSON, because it carries file uploads. `model`, `prompt`, `n`, `size`, `resolution`, `aspect_ratio`, `quality` and `response_format` behave as above and are sent as form fields; `seed` is accepted but not forwarded on edits. `fallbacks` and `metadata` may be sent as JSON-encoded form fields. The whole request body is limited to 51 MB.

- `image` (file, required) — The image to edit. PNG, JPEG, or WebP, up to 25 MB; a larger file is refused with `400 invalid_request_error`. Which dimensions and aspect ratios are accepted is up to the provider.
- `mask` (file) — Optional PNG, JPEG, or WebP up to 25 MB, forwarded to the provider with the image. On OpenAI's API it is a PNG with an alpha channel the same size as `image`, where transparent pixels mark the region to repaint. Without a mask the model edits the whole image guided by the prompt.
- `prompt` (string, required) — What the edited region should contain. Describe the desired result, not the change — `a walnut reading chair` works better than `replace the sofa`.

**curl**

```bash
curl https://api.infro.io/v1/images/edits \
  -H "Authorization: Bearer $INFRO_API_KEY" \
  -F model="bfl/flux-2-dev" \
  -F image=@room.png \
  -F mask=@room-mask.png \
  -F prompt="A walnut reading chair with a brass floor lamp beside it" \
  -F size="1024x1024"
```

**Python**

```python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.infro.io/v1",
    api_key=os.environ["INFRO_API_KEY"],
)

with open("room.png", "rb") as image, open("room-mask.png", "rb") as mask:
    edited = client.images.edit(
        model="bfl/flux-2-dev",
        image=image,
        mask=mask,
        prompt="A walnut reading chair with a brass floor lamp beside it",
        size="1024x1024",
    )

print(edited.data[0].url)
```

## Sizes and aspect ratios

Every model publishes the sizes it can render. These five are common choices; check that your model supports the one you pick:

| Size | Ratio | Typical use |
| --- | --- | --- |
| `1024x1024` | 1:1 | Avatars, product shots, grid thumbnails |
| `1536x1024` | 3:2 | Blog headers and landscape hero images |
| `1024x1536` | 2:3 | Posters, print, portrait cards |
| `1792x1024` | 16:9 | Slides, video thumbnails, wide banners |
| `1024x1792` | 9:16 | Stories and vertical mobile feeds |

An exact `size` is checked only for its `WIDTHxHEIGHT` shape and goes to your provider unchanged; a provider that cannot render it picks its own nearest size or refuses the request, according to its own rules. To let INFRO fit the request to each provider instead, send `resolution` and `aspect_ratio`: they are checked against what the model takes on each target and sent in that provider's own form (a tier, a ratio, or pixels derived within its limits).

## Cost

Providers bill images per image, never per token. The rate depends on the model, the resolution, and `quality`, and `usage.cost` on every response estimates what yours will charge. `n` multiplies it linearly. Vendor list rates live in the [catalog](https://infro.io/providers); if you have negotiated your own, enter them under **Prices** and every estimate uses yours instead.

If a connection errors before producing an image, INFRO tries the next target on the route — and then each model in `fallbacks` — and the cost estimate counts only the images that came back. A request that returns no images is recorded with no cost. Failover needs no change in your code; the `provider`, `connection`, and `route` fields in the response show where it ran.

## Errors

- `400 invalid_request_error` — a malformed body or `size`, `n` outside `1`–`10`, an upload over 25 MB or not PNG, JPEG, or WebP, or a model of another modality. Refusals from your provider (for example an unsupported size) are reported too.
- `402 budget_exceeded` — the estimated cost of the request has no room under a budget you set. Nothing is rendered and your provider is never called.
- `413 request_too_large` — the request body is over 1 MB on `/generations` or 51 MB on `/edits`.
- `429 rate_limit_exceeded` — retry with backoff; see [Rate & spend limits](https://infro.io/docs/api/limits).
- `503 no_available_provider` — nothing can serve `model`. With code `no_provider_connected`, you have no connection for that vendor — connect one, a retry will not help. Without it, every target on the route is unavailable; retry or widen `fallbacks`.

Every status code and the full error body are documented in [Errors](https://infro.io/docs/api/errors).
