# Upload a file

> Send an image once and reference it by ID afterwards.

Canonical page: https://www.rundiffusion.com/docs/api/uploads/create
Endpoint: POST /api/v2/uploads
Authorization: OAuth device flow or Personal API Access Token or Company API Access Token

---

Uploads are images you have sent to RunDiffusion and can refer back to by ID.
Send the bytes once, then use the ID wherever an image is needed instead of
resending them.

`POST /api/v2/uploads`

Authorization: OAuth device flow or Personal API Access Token or Company API Access Token

## Request

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | string | Yes | Bearer token. This endpoint accepts an OAuth device flow token, a Personal API Access Token, or a Company API Access Token. See [Authentication](/docs/api/authentication). |
| `Content-Type` | string | No | multipart/form-data with a boundary. Every HTTP client sets this for you when you hand it a file, so set it yourself only if you are building the body by hand. |

`multipart/form-data` with a single `file` part. There is no JSON body and no
query parameters.

**Body (multipart/form-data)**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | file | Yes | The image bytes, sent as a multipart file part. Example: -F "file=@portrait.png" with curl, or a Blob appended to FormData in the browser. |

cURL:

```bash
curl -X POST https://api2.rundiffusion.com/api/v2/uploads \
  -H "Authorization: Bearer $RUNDIFFUSION_TOKEN" \
  -F "file=@portrait.png"
```

JavaScript:

```javascript
const form = new FormData();
form.append('file', fileBlob, 'portrait.png');

const upload = await fetch('https://api2.rundiffusion.com/api/v2/uploads', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.RUNDIFFUSION_TOKEN}` },
  body: form,
}).then(r => r.json());
```

Python:

```python
with open("portrait.png", "rb") as f:
    upload = requests.post(
        "https://api2.rundiffusion.com/api/v2/uploads",
        headers={"Authorization": f"Bearer {os.environ['RUNDIFFUSION_TOKEN']}"},
        files={"file": f},
    ).json()
```

### Limits

| Name | Type | Description |
| --- | --- | --- |
| `Formats` | JPEG, PNG, WebP, HEIC, HEIF | Anything else is rejected with 415 UNSUPPORTED_MEDIA_TYPE. HEIC and HEIF are converted to JPEG server-side, which changes both the extension and the mime_type you get back. |
| `File size` | 28 MB | Larger files are rejected with 413 UPLOAD_TOO_LARGE. |
| `Dimensions` | 8192 px | Measured on either dimension. Larger images are rejected. |

## Response

`201 Created` for a new upload. Re-sending a file the caller has already
uploaded returns `200 OK` with the existing record unchanged, so retrying a
request that timed out mid-flight will not create a duplicate.

```json
{
  "id": "Yb3kQ9…",
  "name": "fa9c1e22-….png",
  "mime_type": "image/png",
  "size_bytes": 1863245,
  "width": 1024,
  "height": 1024,
  "url": "https://rundiffusion.com/...",
  "thumb_url": "https://rundiffusion.com/...",
  "created": "2026-07-23T09:31:02Z"
}
```

| Name | Type | Description |
| --- | --- | --- |
| `id` | string | The handle for this upload, a 20-character opaque string. This is what you reference later. Example: Yb3kQ9…. |
| `name` | string \| null | The stored filename, generated as {uuid}.{ext}. This is not the filename you sent, so keep your own mapping if the original matters. |
| `mime_type` | string \| null | MIME type as stored. HEIC and HEIF uploads report image/jpeg here, because they were converted. |
| `size_bytes` | integer \| null | Size of the stored file in bytes. |
| `width` | integer \| null | Pixel width of the stored image. |
| `height` | integer \| null | Pixel height of the stored image. |
| `url` | string \| null | Signed download URL, valid for roughly seven days. Treat it as opaque and re-list to refresh it rather than storing it. |
| `thumb_url` | string \| null | Signed URL to a 512x512 WebP thumbnail, same seven-day expiry. Falls back to the same value as url until the thumbnail has been generated. |
| `created` | string (ISO 8601) | When it was created, in UTC. |

> **Your filename is not preserved**
>
> Uploads are stored under a freshly generated `{uuid}.{ext}` name, so `name` in
> the response is not the filename you sent. Keep your own mapping if the
> original name matters to you.

## Errors

| Name | Type | Description |
| --- | --- | --- |
| `400` | INVALID_REQUEST | No file part, an empty file, or a malformed multipart body. |
| `401` | UNAUTHENTICATED | Missing, malformed, or expired token. |
| `413` | UPLOAD_TOO_LARGE | The file exceeds 28 MB. details carries the size you sent and the max, so you can report both. |
| `415` | UNSUPPORTED_MEDIA_TYPE | The declared type is not one of the allowed formats. details.allowed lists them. |
| `429` | RATE_LIMITED | Too many requests. Uploads are capped hourly rather than per minute. See [Rate limits](/docs/api/rate-limits#uploads). |
| `502` | UPSTREAM_UNAVAILABLE | Storage was unreachable. Safe to retry. |

See [Errors](/docs/api/errors) for the full envelope and the code list.

## Referencing an upload

The `id` from this response is the handle. Keep it alongside your own record of
the image, since the stored `name` will not match the filename you sent.

> **Upload once, reuse often**
>
> Uploading returns an ID you can use repeatedly, so a file you will reference
> more than once is worth uploading rather than resending. Use
> [List uploads](/docs/api/uploads/list) to find one you sent earlier.
