# Images

> Synchronous image generation: the connection is held until the images exist.

Section: Core API. Canonical page: https://routehook.ai/docs/images

POST /v1/images/generations takes a prompt and answers with URLs. It is OpenAI's Images API shape, so an existing images.generate call works against this base URL unchanged, and it holds the connection open until the image is ready, which suits a call of tens of seconds and nothing longer. Video and anything else measured in minutes goes through the queue instead.

## POST /v1/images/generations

Generate images and return their URLs. The connection is held until the images exist.

Status: Available. Authentication: any live API key on the account.

## The same route, OpenRouter's path

`POST /v1/images` reaches the identical handler under OpenRouter's spelling. One implementation, one bill; which path you post to is a matter of whichever client you already have.

## POST /v1/images

The same image generation under OpenRouter's path. One handler, one bill.

Status: Available. Authentication: any live API key on the account.

## Collecting the bytes

Every URL these routes return points at `GET /v1/files/{id}` on this domain, never at an upstream host. Present your API key on that route the way you would on any other. The URL is scoped to the account that generated it, so it is not a public link. Generated media is short-lived either way: read `expires_at` on the response rather than assuming a window, because how long a URL keeps working depends on how this deployment is configured. Past it the route answers `410 gone`, a permanent code chosen deliberately: labelling it with a retryable one would have well-behaved clients retrying for ever against something that cannot succeed. Download anything you mean to keep.

## GET /v1/files/{id}

Fetch generated media by reference. Proxied at request time, never redirected upstream.

Status: Available. Authentication: any live API key on the account.

## Generating an image

**cURL**

```bash
curl https://api.routehook.ai/v1/images/generations \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/nano-banana-2",
    "prompt": "isometric factory floor, technical illustration",
    "size": "1024x1024"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.routehook.ai/v1/images/generations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTEHOOK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "google/nano-banana-2",
    prompt: "isometric factory floor, technical illustration",
    size: "1024x1024",
  }),
});

const result = await res.json();
console.log(result.data[0].url);
```

**Python**

```python
import os, requests

res = requests.post(
    "https://api.routehook.ai/v1/images/generations",
    headers={"Authorization": f"Bearer {os.environ['ROUTEHOOK_API_KEY']}"},
    json={
        "model": "google/nano-banana-2",
        "prompt": "isometric factory floor, technical illustration",
        "size": "1024x1024",
    },
)

print(res.json()["data"][0]["url"])
```

## The response

**200 OK**

```json
{
  "created": 1786312455,
  "model": "google/nano-banana-2",
  "data": [
    { "url": "https://cdn.routehook.ai/img/9d21f4-0.png" },
    { "url": "https://cdn.routehook.ai/img/9d21f4-1.png" }
  ]
}
```

created is unix seconds. data is one entry per image, in the order they were generated, and an entry may also carry revised_prompt where the model rewrote what you asked for. The response header x-request-id holds the req_… id of the call, which is what GET /v1/generation takes if you want to know what it cost.

> **Note: This route names the id header x-request-id**
>
> Every other gateway route returns the request id as X-Routehook-Request-Id. This one returns the same req_… value under x-request-id. Code that reads the id for both should read either name rather than assuming one. HTTP header names are case-insensitive, but these are two different names, not two spellings of one.

## Parameters

| PARAMETER | TYPE | REQUIRED | DESCRIPTION |
| --- | --- | --- | --- |
| model | string | required | A slug from GET /v1/models. It must be an image model. Anything else is refused before money is reserved. |
| prompt | string | required | What to generate. Up to 32,000 characters. |
| n | integer | optional | How many images, 1 to 10. Defaults to 1. Each one bills at the model's rate. |
| size | string | optional | 1024x1024, a ratio like 16:9, or a tier like 1K. Advisory, and only the half it states. See below. |
| aspect_ratio | string | optional | The shape on its own: 16:9, 9:16, 1:1. Wins over anything derived from size. |
| resolution | string | optional | The magnitude on its own: 1K, 2K, 4K, or a pixel size. Wins over size, and is the field media rates are keyed on. |
| quality | string | optional | Effort tier (low, medium, high), honoured by models that have one and ignored by the rest. Priced alongside resolution where a model sells both. |
| style | string | optional | Style hint, honoured by models that have one and ignored by the rest. |
| reference_urls | string[] | optional | Up to 8 publicly reachable input images, for models that take references. Ignored by models that do not, rather than refused. |
| user | string | optional | Opaque end-user identifier, forwarded upstream. Up to 128 characters here. Chat and completions allow 256, so a value that fits there can still be refused on this route. |

## The model id names a product

model is a gateway slug (google/nano-banana-2) not one of OpenAI's four image model names. The slug names the product you are buying and we pick who serves it, which is the whole point: restricting the field to one vendor's names would defeat it. The model that comes back in the response is always the slug you asked for, never an upstream's own id, and no route reports which host ran it.

## size is advisory, and states one thing or two

Every upstream behind this expresses shape differently (an aspect ratio, an orientation, a resolution tier), and several reject values the others accept. So size is translated for whatever runs it, and a value a route refuses is snapped to the nearest it allows rather than 400-ing after you have already been quoted. What travels is the half the value actually states: 1024x1024 states a shape and a magnitude and sets both, 16:9 states only a shape, 1K states only a magnitude, and the half it does not state is left to the model's own default. Do not build a layout that assumes exact pixel dimensions. Read them off the image you were handed.

## Or name the two directly

aspect_ratio and resolution are the same two knobs under their own names, and either wins over what size would have implied. Reach for them when you want one without the other (aspect_ratio: "16:9" with no opinion on size) or when you want the rate to be unambiguous: resolution is what a media model's price is keyed on, so naming it names the rate you are billed at. A model that has no such knob ignores what you send, as it ignores anything else it does not declare.

> **Note: There is no b64_json**
>
> response_format is not accepted and images never come back inline. Everything behind this route writes to an object store and returns a URL, and base64-ing a multi-megabyte image back through the gateway would triple the memory cost of a request to change nothing about what you receive. Fetch the URL.

## n multiplies the bill

The reservation taken before the upstream call is exact here: n at the model's per-image rate, plus the model's flat per-request price where it carries one. An image is priced per image and you have already said how many you want, so there is no estimate in it and nobody is refused for headroom they did not need. You are then charged for what arrived, capped at what you asked for, three of four delivered is three paid for, and an upstream that returns more than n cannot bill past the ceiling you were gated on. Batching with n saves a round trip, not money.

> **Warning: This call takes 20–60 seconds**
>
> The connection stays open for the whole generation, so set your client timeout above a minute and do not make a browser wait on it directly. Hanging up aborts the upstream call rather than orphaning it, and the reservation is released. Anything longer than this (video, above all) is submitted rather than awaited: POST /v1/videos/generations answers 202 with a job id and the URLs that follow it, POST /v1/videos is OpenRouter's spelling of the same submit, and POST /v1/queue/{model} is the general form for every other model. All three put the work on the queue; none of them holds the connection.

## When it fails

A failed generation releases the reservation and charges nothing. Failover is not free on this route (where an upstream has probably already started work, its timeout is treated as non-retryable so the same prompt is not paid for twice), so a 503 upstream_unavailable can arrive without every target in the chain having been tried. Retry it; the hold is already gone. A model that is listed but has no per-image and no per-request rate is refused with 409 model_unavailable rather than served unbilled, which is an operator's job to fix and not something a retry will clear.

## Related

- [Queue](/docs/queue): Long work: submit, poll, fetch. Where every video job runs.
- [Pricing](/docs/pricing): What an image costs, and how the reservation becomes a charge.
- [Routing](/docs/routing): What happens when the first attempt at serving a model fails.
