---
title: "Edit image"
description: "Edit images (OpenAI Images protocol)."
canonical: "https://pirouter.ai/docs/api/image-edits"
last-updated: "2026-10-16"
lang: "en"
---

# Edit image

Edit images (OpenAI Images protocol).

`POST https://api.pirouter.ai/v1/images/edits`

OpenAI-compatible image edits: one or more input images (and an optional mask) plus a prompt, sent as `multipart/form-data`. The platform reads the `model` and `stream` text parts; every other part (`image[]`, `mask`, `size`, `quality`, `n`, ...) is forwarded to the upstream provider as-is, except the routing fields `provider`, `models`, `route` and `fallbacks`, which are not supported here and are removed. Image models list this endpoint only where their upstream implements edits; calling another model is `validation_failed` (422). With `stream: true` the response is a Server-Sent Events stream of `image_edit.*` events. The request body is limited to 32 MiB; a larger one is refused with 413.

## Authorization

Send your API key in one of these headers.

| Header | Value | Description |
| --- | --- | --- |
| `Authorization` | `Bearer sk-pi-…` | Platform-issued API key with the `sk-pi-` prefix, sent as a Bearer token. |
| `X-Api-Key` | `sk-pi-…` | The same platform-issued `sk-pi-` key, sent in `X-Api-Key`. Read only when `Authorization` is absent or empty; when both are sent, `Authorization` wins. A bare key without the `Bearer ` prefix is accepted in either header. |

## Body

`multipart/form-data`

OpenAI Images edit request, sent as multipart/form-data. Only the parts the platform reads or the protocol requires are listed; the rest are forwarded upstream unchanged, except the routing parts `provider`, `models`, `route` and `fallbacks`, which this endpoint does not support and removes before forwarding.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | Model slug in "vendor/model" form. Must be an image model registered for this endpoint. |
| `prompt` | string | Yes | Text description of the edit. Forwarded unchanged. |
| `image` | file[] | Yes | The input image(s), as `image[]` file parts (or a single `image` part). Forwarded unchanged. |
| `stream` | boolean | No | When true, respond as a Server-Sent Events stream of image_edit events. Default: `false`. |

## Response

`200` `application/json`, `text/event-stream`

The edited images, in the shape the OpenAI Images protocol defines; the body is the upstream's, passed through.

## Errors

| Status | Code | Description |
| --- | --- | --- |
| `400` | `invalid_request` | The edit request is incomplete: no `model` part (`invalid_request`, `model is required`), or a part the upstream needs is missing or malformed - no image file part, no `prompt` - which is refused before any image is generated with a message naming the part (for example `failed to parse request: at least one image is required`). Nothing is charged. |
| `401` | `invalid_api_key`, `api_key_not_found`, `api_key_revoked`, `api_key_expired` | API key is missing or malformed (`invalid_api_key`), unknown (`api_key_not_found`), revoked (`api_key_revoked`), or expired (`api_key_expired`). |
| `402` | `insufficient_balance` | Wallet balance cannot cover the requested call (`insufficient_balance`). |
| `404` | `model_not_found` | The requested model slug is unknown or not enabled (`model_not_found`), or no provider currently serves it. |
| `413` |  | The request body is larger than this endpoint accepts (32 MiB for image edits). Refused before admission: nothing is forwarded or charged. The error has type `invalid_request_error` and no code. |
| `422` | `validation_failed` | The model is not served on the image edit endpoint (`validation_failed`): the message lists the endpoints it is served on. Nothing is forwarded or charged. |
| `429` | `rate_limited` | Request rejected by rate limiting (`rate_limited`). |
| `500` | `internal_error` | Other failures: `provider_unavailable` (503) when no healthy upstream can serve the request, `internal_error` (500) for unexpected platform-side failures. |
| `503` | `provider_unavailable` | Other failures: `provider_unavailable` (503) when no healthy upstream can serve the request, `internal_error` (500) for unexpected platform-side failures. |

Error body

```json
{
  "error": {
    "message": "No such model. Model names are vendor-qualified (vendor/model), so a name copied from a provider's own docs needs its vendor prefix added. Call GET /v1/models for the slugs this gateway serves.",
    "type": "not_found_error",
    "code": "model_not_found"
  }
}
```

## Response headers

Each header is listed with the response statuses it is sent with.

| Header | Sent with | Description |
| --- | --- | --- |
| `X-Pi-Trace-Id` | `200`, `400`, `401`, `402`, `404`, `413`, `422`, `429`, `default` | Platform request id, minted by the gateway. Present on every response of the inference endpoints; quote it when reporting a problem. |
| `Access-Control-Allow-Origin` | `200`, `400`, `401`, `402`, `404`, `413`, `422`, `429`, `default` | Always `*`. Credentials are the API key header, never a cookie. |
| `Access-Control-Expose-Headers` | `200`, `400`, `401`, `402`, `404`, `413`, `422`, `429`, `default` | Lists every `X-Pi-*` response header plus `Retry-After` and `Content-Encoding`, so browser code can read them. |
| `X-Pi-Provider` | `200`, `default` | Provider that served this response, in the same vocabulary as `provider.order` and `GET /v1/models/{vendor}/{model}/endpoints`. Present only when the upstream provider answered (a completion or an upstream error); after a fallback it names the provider that took over. |
| `X-Pi-Model` | `200`, `default` | Model slug that served this response - the same value as the body's `model`. When a fallback model took over, it is that model. Present only when the upstream provider answered. |
| `X-Pi-Upstream-Model` | `200`, `default` | The full model id the upstream provider itself reported in its response, passed on verbatim (on a stream, from the first data event). Absent when the upstream response named no model; never filled from the platform's own records. |
| `X-Pi-Upstream-Request-Id` | `200`, `default` | The upstream provider's own request id, passed on verbatim, for looking the call up on the provider's side. Absent when the provider returned none. |

## Request samples

cURL

```bash
curl https://api.pirouter.ai/v1/images/edits \
  -H "Authorization: Bearer $PIROUTER_API_KEY" \
  -F model=openai/gpt-image-2 \
  -F "prompt=Make the kite blue" \
  -F "image[]=@kite.png"
```

Python

```python
import os
import requests

response = requests.post(
    "https://api.pirouter.ai/v1/images/edits",
    headers={"Authorization": f"Bearer {os.environ['PIROUTER_API_KEY']}"},
    data={
        "model": "openai/gpt-image-2",
        "prompt": "Make the kite blue",
    },
    files=[("image[]", open("kite.png", "rb"))],
)
print(response.json())
```

TypeScript

```ts
import { openAsBlob } from 'node:fs'

const form = new FormData()
form.append('model', 'openai/gpt-image-2')
form.append('prompt', 'Make the kite blue')
form.append('image[]', await openAsBlob('kite.png'), 'kite.png')

const response = await fetch('https://api.pirouter.ai/v1/images/edits', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.PIROUTER_API_KEY}` },
  body: form,
})
console.log(await response.json())
```

## Response sample

`200` `application/json`

```json
{
  "created": 1791795980,
  "data": [
    {
      "b64_json": "iVBORw0KGgoAAAANSUhEUg..."
    }
  ]
}
```

`200` `text/event-stream`

```text
event: image_edit.partial_image
data: {"type":"image_edit.partial_image","partial_image_index":0,"b64_json":"iVBORw0KGgoAAAANSUhEUg...","size":"1024x1024","quality":"low","output_format":"png"}

event: image_edit.completed
data: {"type":"image_edit.completed","b64_json":"iVBORw0KGgoAAAANSUhEUg...","size":"1024x1024","quality":"low","output_format":"png","usage":{"input_tokens":282,"input_tokens_details":{"image_tokens":256,"text_tokens":26},"output_tokens":273,"output_tokens_details":{"image_tokens":273},"total_tokens":555}}

data: [DONE]
```
