# Images (https://hypit.ai/api-reference/images/)

> Generate and edit images. Both endpoints are asynchronous and answer with a job.

Two routes, both asynchronous:

| Route                         | Purpose                            |
| ----------------------------- | ---------------------------------- |
| `POST /v1/images/generations` | text to image                      |
| `POST /v1/images/edits`       | image to image, with optional mask |

Both answer `202 Accepted` with a [job envelope](/api-reference/jobs). The finished pixels are
collected from `GET /v1/jobs/{job_id}/assets`.

<Callout type="warn" title="`client.images.generate()` will not work">
  The OpenAI SDK helper expects a synchronous `{"data": [{"url": …}]}` body. Ours returns a job. Call
  these two routes over plain HTTP.
</Callout>

## Generate [#generate]

```bash
curl https://hypit.ai/v1/images/generations \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "nano-banana-pro",
    "prompt": "a paper boat drifting down a rain gutter, shallow depth of field",
    "n": 1,
    "aspect_ratio": "16:9",
    "resolution": "1k"
  }'
```

```json
{
  "id": "job_5a0ce31f78b246d9c0117e42",
  "status": "queued",
  "kind": "image",
  "model": "nano-banana-pro",
  "progress": 0,
  "created_at": 1787824589,
  "dispatch_deadline_at": 1787825189
}
```

### Request body [#request-body]

| Field              | Type             | Notes                                                                                   |
| ------------------ | ---------------- | --------------------------------------------------------------------------------------- |
| `model`            | string           | **required**                                                                            |
| `prompt`           | string           | **required**                                                                            |
| `n`                | integer          | 1–10, default `1`. Each image becomes one asset ordinal                                 |
| `size`             | string           | e.g. `"1024x1024"`, or `"auto"`                                                         |
| `resolution`       | string           | A model-published output tier, e.g. `"1k"`, `"2k"`, `"4k"`; independent of aspect ratio |
| `quality`          | string           | e.g. `low`, `medium`, `high`, `hd`, `standard`, `auto`                                  |
| `style`            | string           | vendor-defined                                                                          |
| `aspect_ratio`     | string           | e.g. `"16:9"`                                                                           |
| `negative_prompt`  | string           |                                                                                         |
| `background`       | string           | e.g. `transparent`, `opaque`, `auto`                                                    |
| `output_format`    | string           | `png`, `jpeg`, `webp`                                                                   |
| `response_format`  | string           | `url` or `b64_json` — anything else is a `400`                                          |
| `seed`             | integer          |                                                                                         |
| `watermark`        | boolean          |                                                                                         |
| `reference_images` | array            | see below; `images` is accepted as a compatibility alias                                |
| `mask`             | string or object | see below                                                                               |

For models with output tiers, prefer `aspect_ratio` + `resolution`. A `4k` image is not a fixed `3840x2160` canvas: the model and aspect ratio determine the actual pixels. Legacy pixel `size` remains available on models that support it. When also supplying `aspect_ratio`, both must describe the same shape. `metadata.resolution` remains a compatibility alias; contradictory aliases return `400`.

Before creating a job or reserving credits, the gateway checks that an available route supports the requested tier and shape. Unsupported combinations return a parameter error instead of silently selecting a default. Pricing uses the same tier; see `pricing.resolution_ratio` or `pricing.media_rates` on the [model card](/api-reference/models).

<Callout type="info" title="`response_format` does not change how you collect the result">
  It is validated and forwarded to the upstream, but a job's artifacts are always delivered through
  the assets API as signed links. There is no path that returns base64 inline.
</Callout>

### Vendor passthrough [#vendor-passthrough]

Any key the schema above does not model is forwarded to the upstream verbatim. That is how features
that only exist on one vendor stay reachable. Two limits apply: the whole extra block is capped in
size and nesting depth, and any string that looks like a media URL must be a real `http(s)` or
`data:` URL — a URL smuggled through an extra field is rejected with `url_in_extra` rather than
silently fetched.

### Reference images in JSON [#reference-images-in-json]

`reference_images` (or the compatibility alias `images`) attaches source material to a JSON request. Each entry is either a bare URL
string or an object:

```json
{
  "model": "nano-banana-pro",
  "prompt": "put the boat in a snowstorm",
  "reference_images": [
    { "url": "https://example.com/boat.png" },
    { "b64": "iVBORw0KGgo…", "mime_type": "image/png", "filename": "boat.png" }
  ]
}
```

`mask` accepts the same object, or a plain URL / `data:` URL string.

Every URL you supply is fetched through a guarded fetcher: `http`, `https` and `data:` only, no
private or link-local addresses, and every redirect is re-checked. A URL that fails the check is a
`400` (`invalid_media_url`, `unsupported_media_url_scheme` or `invalid_input_url`) and nothing is
charged.

## Edit [#edit]

`POST /v1/images/edits` takes either `multipart/form-data` with real file parts, or the same JSON
body as above with `reference_images`. At least one source image is required.

```bash
curl https://hypit.ai/v1/images/edits \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -F model=nano-banana-pro \
  -F 'prompt=make the sky overcast' \
  -F image=@boat.png \
  -F mask=@sky-mask.png
```

Multipart field names:

| Part                                                                                                                                                                       | Kind  | Notes                                        |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -------------------------------------------- |
| `image`, `image[]`                                                                                                                                                         | file  | one or more source images, in the order sent |
| `mask`                                                                                                                                                                     | file  | at most one                                  |
| `model`, `prompt`, `n`, `size`, `resolution`, `quality`, `style`, `response_format`, `background`, `output_format`, `negative_prompt`, `aspect_ratio`, `seed`, `watermark` | value | same semantics as the JSON fields            |

Limits:

* **25 MiB** per uploaded file
* **100 MiB** for the whole multipart body
* **10 MiB** for a JSON body on `/v1/images/generations`
* 64 KiB per multipart scalar field

The upload's declared `Content-Type` is ignored — we sniff the bytes, and only **PNG, JPEG and WebP**
are accepted. Anything else is `400 unsupported_image_format`.

## End to end [#end-to-end]

```python
import os, time, httpx

BASE = os.environ.get("HYPIT_BASE_URL", "https://hypit.ai/v1")
AUTH = {"Authorization": f"Bearer {os.environ['HYPIT_API_KEY']}"}

job = httpx.post(
    f"{BASE}/images/generations",
    headers=AUTH,
    json={
        "model": "nano-banana-pro",
        "prompt": "a paper boat drifting down a rain gutter",
        "n": 1,
        "aspect_ratio": "16:9",
        "resolution": "1k",
    },
    timeout=60,
)
job.raise_for_status()
job_id = job.json()["id"]

while True:
    state = httpx.get(f"{BASE}/jobs/{job_id}", headers=AUTH, timeout=30).json()
    if state["status"] in {"succeeded", "failed", "queue_expired"}:
        break
    time.sleep(3)

if state["status"] != "succeeded":
    raise RuntimeError(f"{state['status']}: {state.get('error_code')} {state.get('error')}")

for a in httpx.get(f"{BASE}/jobs/{job_id}/assets", headers=AUTH, timeout=30).json()["items"]:
    data = httpx.get(a["url"], follow_redirects=True, timeout=None).content
    open(f"image-{a['ordinal']}.png", "wb").write(data)
```

```js
import { writeFile } from "node:fs/promises";

const BASE = process.env.HYPIT_BASE_URL ?? "https://hypit.ai/v1";
const AUTH = { Authorization: `Bearer ${process.env.HYPIT_API_KEY}` };
const TERMINAL = new Set(["succeeded", "failed", "queue_expired"]);

const created = await fetch(`${BASE}/images/generations`, {
  method: "POST",
  headers: { ...AUTH, "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "nano-banana-pro",
    prompt: "a paper boat drifting down a rain gutter",
    n: 1,
    aspect_ratio: "16:9",
    resolution: "1k",
  }),
});
if (!created.ok) throw new Error(`${created.status} ${await created.text()}`);
const { id } = await created.json();

let job;
do {
  await new Promise((r) => setTimeout(r, 3_000));
  job = await (await fetch(`${BASE}/jobs/${id}`, { headers: AUTH })).json();
} while (!TERMINAL.has(job.status));

if (job.status !== "succeeded") throw new Error(`${job.status}: ${job.error_code} ${job.error}`);

const { items } = await (await fetch(`${BASE}/jobs/${id}/assets`, { headers: AUTH })).json();
for (const a of items) {
  const bytes = Buffer.from(await (await fetch(a.url)).arrayBuffer());
  await writeFile(`image-${a.ordinal}.png`, bytes);
}
```

## Errors specific to these routes [#errors-specific-to-these-routes]

| Status | `code`                             | Meaning                                             |
| ------ | ---------------------------------- | --------------------------------------------------- |
| `400`  | `missing_model` / `missing_prompt` | both are required                                   |
| `400`  | `invalid_n`                        | `n` must be between 1 and 10                        |
| `400`  | `invalid_response_format`          | not `url` or `b64_json`                             |
| `400`  | `missing_image`                    | `/v1/images/edits` with no source image             |
| `400`  | `invalid_multipart`                | the multipart body could not be parsed              |
| `400`  | `unexpected_file`                  | a file part other than `image`, `image[]` or `mask` |
| `400`  | `invalid_mask` / `empty_mask`      | mask is neither a URL nor a `{b64\|url}` object     |
| `400`  | `unsupported_image_format`         | the bytes are not PNG, JPEG or WebP                 |
| `400`  | `empty_image` / `unreadable_image` | a file part carried nothing usable                  |
| `413`  | `image_too_large`                  | a single file exceeds 25 MiB                        |
| `413`  | `body_too_large`                   | JSON over 10 MiB, or a multipart body over 100 MiB  |
| `413`  | `form_field_too_large`             | a scalar multipart field exceeds 64 KiB             |
| `404`  | `model_not_found`                  | no enabled provider serves this model for images    |

The shared job, billing and rate-limit errors are listed in
[Errors and limits](/api-reference/errors).