DocsModalities
Image generation
Generate and edit images with POST /v1/images/generations and /v1/images/edits — parameters, sizes, response shape, and per-image billing.
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.
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.
POST https://api.infro.io/v1/images/generations
POST https://api.infro.io/v1/images/editsRequest body
modelstringrequired- Image model ID in
vendor/model-nameform, e.g.bfl/flux-2-pro,ideogram/ideogram-3, orgoogle/nano-banana-pro, or the slug of one of your routes. Browse IDs and vendor list rates in the catalog. The model must be served by one of your connected providers. promptstringrequired- What to render, up to 32,000 characters. Prompt conventions differ by model —
recraft/recraft-v3responds to style keywords,ideogram/ideogram-3is the strongest at rendering text inside the image. The provider may enforce a shorter limit. ninteger- How many images to request,
1–10. Defaults to1. Sent to the provider as-is; a provider may return fewer, andusage.imagescounts what came back. sizestring- Exact pixels,
WIDTHxHEIGHT, e.g.1024x1024. Passed to the provider unchanged; omit it and the provider's default applies. Sendsizeorresolutionandaspect_ratio, not both. See the note on sizes below. resolutionstring- A resolution tier:
512,768,1K,1.5K,2K,3Kor4K. Each model takes its own tiers on each provider (Nano Banana 2 Lite renders1Konly; Nano Banana Pro takes1K,2Kand4K), 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_ratiostringW:H, e.g.16:9or9:16. Checked against what the model takes on each target, likeresolution.qualitystringlow,medium,highorauto; GPT Image 2.5 also takesxhighandmax. The legacystandardreads asmedium. 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 takeslowormedium).response_formatstringurl(default) returns a signed link to a copy INFRO stores for 24 hours;b64_jsonreturns the image inline as base64. Base64 payloads are several megabytes each — preferurlunless your runtime cannot make a second fetch.seedinteger- Best-effort deterministic sampling, forwarded on
/generationsonly, 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 apply here, with identical semantics.
routingobject- Legacy routing fields do not rank or region-constrain customer-owned connections. Configure a named route for provider selection; see Routing.
fallbacksarray- Up to five model IDs to try, in order, if every connection for
modelfails, 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 with400 unknown_fallback_model. See Failover & fallbacks. loggingboolean- Set
falseto 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.
Example request
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"]
}'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)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);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
{
"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[].urlstring- A signed link to a copy of the image INFRO stores, or
nullwhenresponse_formatisb64_json. The link stops working 24 hours after the request. data[].b64_jsonstring- Base64-encoded image bytes, or
nullwhenresponse_formatisurl. Exactly one of the two fields is populated per entry. routestring- INFRO extension: which position in the resolved chain rendered the images —
primary, orstandby_aand so on after a failover.route_nameis the slug of the route used, ornullfor a direct model request. provider / connectionstring- INFRO extension: the provider kind and the ID of your connection that served the request.
usage.imagesinteger- Number of images the provider returned, which is what the estimate is priced on. Usually equals
n. usage.costnumber | null- INFRO extension: an estimate of what your provider will bill you for this request, with
usage.price_sourcesaying which rate it used.nullwhen the model is unpriced.
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.
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.
imagefilerequired- 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. maskfile- 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. promptstringrequired- What the edited region should contain. Describe the desired result, not the change —
a walnut reading chairworks better thanreplace the sofa.
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"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; 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 orsize,noutside1–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/generationsor 51 MB on/edits.429 rate_limit_exceeded— retry with backoff; see Rate & spend limits.503 no_available_provider— nothing can servemodel. With codeno_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 widenfallbacks.
Every status code and the full error body are documented in Errors.