# Images

Every image input is an object with exactly one of three members.

| Member | Value |
|---|---|
| `id` | A handle returned by `POST /v1/images`, in the form `img_<sha256>`. |
| `base64` | Standard base64 of a JPEG, PNG or WebP file. A `data:` prefix is tolerated. |
| `url` | Reserved. An `https://` URL is not supported yet and answers `400 unsupported`. |

```json
{ "image": { "base64": "/9j/4AAQSkZJRgABAQ..." } }
```

```json
{ "image": { "id": "img_312c4e3a0ef23fa991baec515316f6b5897d4d534ad7058c57d8563cfcbdc6e2" } }
```

## Upload and reuse

`POST /v1/images` stores an image and returns a handle. It is free of charge. Handles avoid sending the same bytes repeatedly, for example when several prompts, refinements or models work on one picture.

The file is sent either as `multipart/form-data` with the field `file`, or as the raw request body with `Content-Type: image/jpeg`, `image/png` or `image/webp`.

```bash
curl https://api.stack.foldbun.com/v1/images \
  -H "Authorization: Bearer $FOLDBUN_API_KEY" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg
```

```json
{
  "id": "img_312c4e3a0ef23fa991baec515316f6b5897d4d534ad7058c57d8563cfcbdc6e2",
  "width": 1343,
  "height": 1008,
  "bytes": 388375,
  "contentType": "image/jpeg"
}
```

The handle is content-addressed: uploading the same picture again returns the same `id`, with status 200 instead of 201. `width` and `height` are the size of the oriented original (EXIF orientation applied), which is the coordinate space of every later call.

A handle is valid only inside the account that uploaded it. Handles of other accounts do not resolve.

### Retention

The image itself is kept on a best-effort basis and deleted after at most 7 days, counted from the first time the account stored the image. Stack also evicts the least recently used images earlier when storage capacity is needed, so there is no minimum retention. When a call answers `404 image_not_found`, upload the image again and repeat the call.

When image storage is temporarily unavailable, a call answers a retryable `503 unavailable` with `Retry-After`.

## Accepted input

| Property | Limit |
|---|---|
| Formats | JPEG, PNG, static WebP |
| File size | At most 20 MiB per upload |
| Pixels | At most 40 megapixels, and 16384 px per side |
| JSON request body | At most 28 MiB, so that a base64 image fits |

A file or request body above the size limits is refused with `413 payload_too_large`. An unsupported or malformed file, or an image above the pixel limits, is refused with `400 invalid_request`. A file that passes these checks but cannot be decoded is refused with `400 invalid_image` the first time a model needs it, and the same handle keeps answering `400 invalid_image`. Such a call is never charged. When image processing is busy, the answer is a retryable `503 unavailable`.

An image given inline as `base64` is handled like an upload.

## Storage and privacy

Uploads are sanitized when they arrive: metadata (EXIF, XMP, comments, embedded thumbnails) and any data appended after the image are removed. The pixels are not changed or re-encoded, so a JPEG stays a JPEG, a PNG a PNG and a WebP a WebP.

The image itself is kept for at most 7 days (see [Retention](#retention)). It is never forwarded to a GPU node. Nodes only receive resized copies that Stack generates for each model.

`img_<sha256>` is the SHA-256 of the sanitized bytes, not of the uploaded file.

## Coordinates

All coordinates in requests and responses are pixels of the oriented original: the uploaded image at full size with its EXIF orientation applied. This covers boxes (`[x1, y1, x2, y2]`), points, polygons and quadrilaterals. The size is returned as `image.width` and `image.height` in every response that has an image, and by `POST /v1/images`. There is no resizing and no short-side limit: the limits are those in [Accepted input](#accepted-input), up to 40 megapixels and 16384 px per side.

A client that downscales an image before upload gets coordinates in the downscaled pixels. Smaller uploads save upload time and bandwidth; models work on resized copies in any case.

## Masks

`bbox` and `polygon` are always in image pixels. A mask, returned only on request (`geometry: ["mask"]`), can be smaller than the image. Its `mask.size` (`[height, width]`) is the actual size of the mask grid, and a client scales by it: x by `image.width / size[1]`, y by `image.height / size[0]`. See [Post-processing](https://stack.foldbun.com/docs/post-processing/).

`options.maskSize` on `POST /v1/segment` and `POST /v1/refine` sets the long side of the returned mask grid, from 64 to 4096 px, 2048 by default. The grid is never larger than the image. It affects `mask` output only; polygons are traced at image resolution. A value out of range is refused with `400 invalid_request`.
