# Image Generation API

Create image generation tasks with POST /api/v1/images, understand the response, credits, and errors.

## Create an image task

`POST /api/v1/images` validates the request, charges credits, and submits the job before it responds. Most models then render asynchronously: poll [`GET /api/v1/tasks/{taskId}`](https://viewmax.studio/docs/api/tasks) every 5–10 seconds until `success`, `failed`, or `canceled`, and read the images from `data.taskUrls`.

```http
POST https://viewmax.studio/api/v1/images
Authorization: Bearer sk-your-api-key
Content-Type: application/json
Idempotency-Key: kite-poster-001
```

## Request body

| Field        | Required | Type     | Description                                                                                                                                      |
| ------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `model`      | Yes      | string   | A model `id` from `GET /api/v1/models?type=image`                                                                                                |
| `prompt`     | Yes      | string   | Non-empty; trimmed                                                                                                                               |
| `scene`      | No       | string   | `text-to-image` or `image-to-image`; must be a key of the model's `modes`                                                                        |
| `image_urls` | No       | string[] | `http://` or `https://` image URLs the provider can fetch without authentication. At least one for `image-to-image`; ViewMax sets no count limit |
| `options`    | No       | object   | Generation controls from the model's catalog entry                                                                                               |

If `scene` is omitted, it is `image-to-image` when `image_urls` is non-empty, otherwise `text-to-image`.

### `options`

| Field          | Type   | Description                                                                                                                                                                                                      |
| -------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aspect_ratio` | string | One of `modes.<scene>.aspect_ratios`, such as `1:1`, `16:9`, or `9:16`                                                                                                                                           |
| `quality`      | string | One of `modes.<scene>.qualities`, case-insensitive: `1K`/`2K`/`4K` (Nano Banana 2, Wan 2.7), `Basic`/`High` (Seedream 5.0), `Medium`/`High` (GPT Image 1.5), `Low`/`Medium`/`High`/`XHigh`/`Max` (GPT Image 2.5) |
| `resolution`   | string | GPT Image 2.5: `1K` (default), `2K`, or `4K`. Other models read it as `quality` when `quality` is omitted                                                                                                        |
| `size`         | string | GPT Image 2.5 only: exact pixels such as `1536x1024` (multiples of 16, longest side at most 3840, ratio at most 3:1, 655,360–8,294,400 pixels). Overrides `aspect_ratio` and `resolution`                        |

Unrecognized values are handled differently by model:

- GPT Image 2.5 (`gpt-image-2.5`, `gpt-image-2.5-sunburst`) returns 400: `Invalid GPT Image 2.5 quality`, `Invalid GPT Image 2.5 resolution`, `Invalid GPT Image 2.5 aspect ratio`, or `Invalid GPT Image 2.5 pixel dimensions`. Its defaults are `Medium`, `1K`, and `1:1`.
- Other models replace an unrecognized `quality` with the first listed value and an unrecognized `aspect_ratio` with the default, and bill at that value. Models whose `aspect_ratios` list is empty pass the value through unchecked. Check the catalog before sending.

## Example

```bash
curl -X POST https://viewmax.studio/api/v1/images \
  -H "Authorization: Bearer $VIEWMAX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: kite-poster-001" \
  -d '{
    "model": "nano-banana-2",
    "scene": "text-to-image",
    "prompt": "A paper kite crossing a bright summer sky",
    "options": { "aspect_ratio": "16:9", "quality": "2K" }
  }'
```

This costs 8 credits, or `0` within the plan allowance described below.

## Successful response

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "id": "8b2d4f6a-1c3e-4a5b-9d7f-0e2c4a6b8d10",
    "userId": "usr_123",
    "mediaType": "image",
    "model": "nano-banana-2",
    "prompt": "A paper kite crossing a bright summer sky",
    "options": "{\"aspect_ratio\":\"16:9\",\"quality\":\"2K\"}",
    "status": "pending",
    "createdAt": "2026-09-29T08:00:00.000Z",
    "updatedAt": "2026-09-29T08:00:01.000Z",
    "deletedAt": null,
    "taskId": "job_def",
    "taskInfo": null,
    "taskResult": null,
    "taskUrls": [],
    "costCredits": 8,
    "scene": "text-to-image",
    "visibility": "user",
    "creditId": "cr_456",
    "waivedCredits": 0,
    "fairUseKey": null
  }
}
```

The fields are described under **Task fields** in the [Video Generation API](https://viewmax.studio/docs/api/video-generation). `taskUrls` is always an array and stays empty until `success`.

## Idempotency

Send `Idempotency-Key` (1–200 visible ASCII characters, no spaces) with every create call. Repeating a key returns the original task and its `costCredits` without charging again; the body is not compared, and keys are shared by all create endpoints of the account. Retry a timed-out request with the same key; use a new key for a deliberate new attempt. Without the header, every POST creates and charges a new task.

## Credits and refunds

- Credits are deducted when the task is created. If the balance is lower than the cost, the call returns 402 and creates no task.
- A task that ends `failed` or `canceled` is refunded automatically, and any plan allowance it used is returned.
- On Pro and Ultra, `gpt-image-2`, `gpt-image-2.5`, `gpt-image-2.5-sunburst`, `nano-banana-2`, and `grok-imagine` cost `0` within a daily allowance shared with the ViewMax app (currently 25 images per UTC day on Pro and 100 on Ultra). While it lasts, `costCredits` is `0`, `waivedCredits` holds the normal price, and `fairUseKey` is set. Other models, and these models after the allowance, are charged.

## Estimate the cost

There is no cost-preview endpoint. Compute the cost from `modes.<scene>` in `GET /api/v1/models?type=image`:

| Catalog field                                                               | Cost                                      |
| --------------------------------------------------------------------------- | ----------------------------------------- |
| `credits[quality]`                                                          | Price for the sent (or defaulted) quality |
| `credits_per_generation`                                                    | Price per image when `credits` is absent  |
| `credits_by_resolution_and_aspect_ratio[resolution][aspect_ratio][quality]` | GPT Image 2.5                             |

## Errors

All errors use the [error envelope](https://viewmax.studio/docs/api/errors).

| HTTP | `error.type`           | Example `message`                                                        | What to do                                                                                                                                             |
| ---- | ---------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400  | `invalid_request`      | `unsupported image model: <id>. Supported models: ...`                   | Use an `id` from `GET /api/v1/models?type=image`                                                                                                       |
| 400  | `invalid_request`      | `scene must be one of text-to-image\|image-to-image, got <value>`        | Send a valid `scene`                                                                                                                                   |
| 400  | `invalid_request`      | `image-to-image requires image_urls`                                     | Add at least one image URL                                                                                                                             |
| 400  | `invalid_request`      | `image_urls must contain valid http(s) URLs`                             | Send `http://` or `https://` URLs                                                                                                                      |
| 400  | `invalid_request`      | `Invalid GPT Image 2.5 quality`                                          | Send a listed value                                                                                                                                    |
| 400  | `invalid_request`      | `the generation request uses unsupported settings`, with `error.task_id` | The provider rejected the settings; the task failed and was refunded. Read its `taskInfo.errorMessage`, adjust, and retry with a new `Idempotency-Key` |
| 400  | `content_rejected`     | `input content was rejected by safety filters`                           | Prohibited-use wording (deepfake, face swap, impersonation) or provider moderation. Change the content; do not retry unchanged                         |
| 402  | `insufficient_credits` | `insufficient credits`                                                   | No task was created. Add credits; do not retry until then                                                                                              |
| 500  | `internal_error`       | `the generation service is temporarily unavailable`                      | With `error.task_id` the task failed and was refunded: retry once. If the hint says the outcome is unknown, poll that task instead of retrying         |
| 503  | `internal_error`       | —                                                                        | The content-safety check is temporarily unavailable; retry after a short delay                                                                         |
