# Image Generation

## Endpoint: `POST /v0/images/generations`

Generate one or more images from a raw prompt or a preset.

### Modes

| Mode | When | Behaviour |
|---|---|---|
| **Sync (default)** | Models that finish ≤ 180s | Blocks until result ready or 504 timeout |
| **Async** (`?async=true`) | Slow models (e.g., GPT Image 2) | Returns `{ id, status: 'queued' }` immediately; poll with `GET /v0/images/generations/:jobId` |

### Body (JSON)

```json
{
  "prompt": "a serene mountain lake at sunrise",
  "negativePrompt": "watermark, text",
  "presetId": "<optional>",
  "variables": {},
  "aspectRatio": "16:9",
  "resolution": "standard",
  "sampleCount": 1,
  "modelVersion": "gemini-3-pro-image-preview",
  "isPublic": false
}
```

| Field | Type | Required | Notes |
|---|---|---|---|
| `prompt` | string | yes (or via preset) | Free-form prompt. Leave empty when using a `presetId` that has a complete template. |
| `presetId` | string | no | If set, the preset's prompt is rendered with `variables` and used as the base. |
| `variables` | object | no | `{ key: value }` map matching the preset's `requirements` |
| `aspectRatio` | string | no | One of: `1:1, 1:2, 1:4, 1:8, 2:1, 2:3, 3:2, 3:4, 4:1, 4:3, 4:5, 5:4, 8:1, 9:16, 9:19.5, 16:9, 19.5:9, 21:9` |
| `resolution` | `'low' \| 'standard' \| 'high' \| '4k'` | no (default `standard`) | Affects credit cost |
| `sampleCount` | number | no (default `1`) | How many images to generate |
| `modelVersion` | string | no | Override the preset/team default |
| `isPublic` | boolean | no | Publish the result to the public gallery |
| `refImageBase64` | array | no | `[{ mimeType, data }]` — reference images as base64 (max 5) |
| `refImageUrls` | string[] | no | URLs the server fetches itself (max 5) |

### Body (multipart) — when uploading file requirements

```
Content-Type: multipart/form-data

prompt=<string>
presetId=<id>
variables=<JSON-stringified object>
<requirementKey>=@./file.jpg            # for type=file requirements
<requirementKey>_0=@./a.jpg             # for multiple=true file requirements
<requirementKey>_1=@./b.jpg
```

### Query parameters

| Query | Values | Effect |
|---|---|---|
| `response_format` | `url` (default), `b64_json`, `both` | URL only, base64 only, or both |
| `include_base64` | `true`/`1` | Add base64 to the response (alias for `response_format=both`) |
| `async` | `true`/`1` | Don't block — return `jobId` immediately |

### Response — sync, success (`200`)

```json
{
  "data": {
    "id": "<jobId>",
    "created": 1714161234,
    "model": "gemini-3-pro-image-preview",
    "data": [
      {
        "id": "<imageId>",
        "url": "https://r2.../image.webp",
        "storageKey": "generated/team-xxx/abc.webp"
      }
    ],
    "usage": { "credits": 1 }
  }
}
```

### Response — async or queued (`200`)

```json
{ "data": { "id": "<jobId>", "status": "queued", "created": ..., "model": "..." } }
```

### Response — sync timeout (`504`)

After 180s of waiting, returns:

```json
{ "error": "Generation timed out after 180s", "jobId": "...", "hint": "GET /v0/images/generations/<jobId> to check status" }
```

The job is **still running** — poll with the `jobId`.

## Endpoint: `GET /v0/images/generations/:jobId`

Poll a job. Possible statuses: `queued`, `active`, `completed`, `failed`.

```json
{
  "data": {
    "id": "<jobId>",
    "status": "completed",
    "created": 1714161234,
    "model": "gemini-3-pro-image-preview",
    "data": [{ "id": "...", "url": "...", "width": 1024, "height": 1024 }],
    "usage": { "credits": 1 }
  }
}
```

For `failed`, you get `{ "id": "...", "status": "failed", "error": "..." }`.

## Endpoint: `GET /v0/images/library`

List generated images for the team, paginated.

### Query

| Param | Default | Description |
|---|---|---|
| `page` | `1` | Page number |
| `limit` | `20` | Items per page (1–100) |
| `presetId` | — | Filter to images generated from this preset |
| `search` | — | Substring search across `userPrompt` and preset name |

### Response

```json
{
  "data": {
    "images": [
      {
        "id": "img_abc",
        "url": "https://cdn.../image.webp",
        "thumbnailUrl": "https://cdn.../thumb.webp",
        "width": 1024,
        "height": 1024,
        "presetId": "preset_xyz",
        "modelVersion": "gemini-3-pro-image-preview",
        "userPrompt": "...",
        "isPublic": false,
        "createdAt": "2026-04-26T..."
      }
    ],
    "total": 124,
    "page": 1,
    "limit": 20
  }
}
```

## Endpoint: `GET /v0/images/library/:imageId`

Read a single image's metadata + public URL. Returns `404` if the image isn't owned by the API key's team.

```json
{
  "data": {
    "id": "img_abc",
    "presetId": "preset_xyz",
    "prompt": "rendered prompt with variables filled in",
    "userPrompt": "ภาพถ่ายรองเท้าวิ่ง",
    "modelVersion": "gemini-3-pro-image-preview",
    "variables": { "product_name": "Nike Air Zoom" },
    "sourceType": null,
    "isPublic": false,
    "url": "https://cdn.../image.webp",
    "thumbnailUrl": "https://cdn.../thumb.webp",
    "width": 1024,
    "height": 1024,
    "createdAt": "2026-04-26T..."
  }
}
```

## Endpoint: `GET /v0/images/library/:imageId/download`

Download the raw bytes. Response is a binary stream with:

```
Content-Type: image/webp           (or whatever the upload mimeType is)
Content-Disposition: attachment; filename="<imageId>.webp"
Content-Length: <bytes>
```

> Tip: most clients don't need this — the `url` from `/library/:id` is already a public CDN URL. Use the download endpoint when you need the bytes server-side (e.g. zipping, re-uploading).

## Credits

Each model + resolution has its own credit cost (configurable per `AiModelConfig`):

| Resolution | Default cost |
|---|---|
| `low` | 1 |
| `standard` | 1 |
| `high` | 2 |
| `4k` | 4 |

Total cost = `cost(resolution) × sampleCount`. The team is charged before generation; failed jobs are refunded.

Check remaining balance:

```bash
curl "$BASE/v0/usage" -H "Authorization: Bearer $KEY"
```
