# Files

Upload input images with POST /files, or pass an image URL directly to a generation.

Effects take images as input. There are two ways to provide one.

## Upload with POST /files

```sh
curl https://api.moan.hot/v1/files \
  -H "Authorization: Bearer $MOANHOT_API_KEY" \
  -F "file=@photo.jpg"
```

```json
{
  "id": "file_3kq9c0m2x7z1b8v4n6p5r2t0",
  "object": "file",
  "bytes": 482113,
  "mime_type": "image/jpeg",
  "width": 1080,
  "height": 1440,
  "sha256": "9b1c...",
  "created_at": "2026-10-20T12:00:00Z",
  "expires_at": "2026-10-21T12:00:00Z"
}
```

Use the `id` as the value of an input slot in `POST /generations`. A file expires **24 hours** after upload and can be used by any number of generations until then. `GET /files/{id}` returns its metadata.

Upload when the image lives on your user's device or behind authentication. The file is checked at upload, so a bad image fails fast, before you try to create anything.

## Pass a URL

Instead of a `file_` id, put a public `https://` URL in `inputs`:

```json
{ "inputs": { "image": "https://cdn.example.com/u/123.jpg" } }
```

We fetch it once, at create. The URL must answer directly with the image bytes, without a login.

## Limits

| | |
|---|---|
| Formats | JPEG, PNG, WebP |
| Size | 20 MB at most |
| Dimensions | 8192 px at most on the long side. Each effect sets a minimum short side in `inputs[].min_short_side_px` |
| Lifetime | 24 hours for uploads |

## Errors

| Code | Status | When |
|---|---|---|
| `invalid_request` | 400 | The body is not `multipart/form-data` or has no `file` part |
| `image_too_large` | 413 | Over 20 MB or over 8192 px on the long side |
| `unsupported_media_type` | 415 | HEIC, GIF or another format. Convert to JPEG, PNG or WebP |
| `image_unreadable` | 422 | The bytes do not decode, the image is too small, or a URL fetch failed |
| `file_not_found` | 404 | Unknown id, another account's or mode's file, or past its 24 hours |

For URL inputs, the `image_*` codes come from `POST /generations` instead.
