> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eachlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload File

> Upload media files in two steps: request a presigned URL, then PUT the file directly to storage.

## Overview

Many models accept media (images, video, audio) as input. To use your own files, upload them with this endpoint and pass the returned `public_url` to [Create Prediction](/api/predictions/create-prediction).

Uploads happen in two steps:

<Steps>
  <Step title="Request a presigned URL">
    `POST /v1/upload/presign` with file metadata. The response contains a short-lived URL to upload to and a stable `public_url`.
  </Step>

  <Step title="Upload the file">
    `PUT` the raw file bytes to `presigned_url`, applying any `required_headers` exactly as returned.
  </Step>
</Steps>

## Step 1 — Request a presigned URL

### Endpoint

```
POST https://api.eachlabs.ai/v1/upload/presign
```

### Request Body

| Field                | Type    | Required | Description                                                                                                                                                                                                                                                                                                     |
| -------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content_type`       | string  | Yes      | MIME type of the file (e.g., `image/png`, `video/mp4`, `audio/mpeg`).                                                                                                                                                                                                                                           |
| `file_type`          | string  | No       | High-level category. One of `image`, `video`, `audio`, `other`.                                                                                                                                                                                                                                                 |
| `expires_in_seconds` | integer | No       | How long the stored file is retained before it's automatically deleted, in **seconds**. Sets the `expires_at` in the response. Defaults to **180 days** when omitted; clamped to a minimum of **60 seconds** and a maximum of **365 days** (`31536000`). Does **not** affect the 15-minute upload-URL lifetime. |

### Code Examples

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -X POST https://api.eachlabs.ai/v1/upload/presign \
    -H "Content-Type: application/json" \
    -H "X-API-Key: YOUR_API_KEY" \
    -d '{
      "content_type": "image/png",
      "file_type": "image"
    }'
  ```

  ```python Python theme={"dark"}
  import requests

  resp = requests.post(
      "https://api.eachlabs.ai/v1/upload/presign",
      headers={
          "Content-Type": "application/json",
          "X-API-Key": "YOUR_API_KEY",
      },
      json={
          "content_type": "image/png",
          "file_type": "image",
      },
  )
  data = resp.json()
  print(data["public_url"])
  ```

  ```javascript JavaScript theme={"dark"}
  const resp = await fetch("https://api.eachlabs.ai/v1/upload/presign", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": "YOUR_API_KEY",
    },
    body: JSON.stringify({
      content_type: "image/png",
      file_type: "image",
    }),
  });
  const { presigned_url, public_url, required_headers } = await resp.json();
  ```
</CodeGroup>

### Response

```json theme={"dark"}
{
  "id": "0dce0f44-b8a5-4f30-91d4-5f6b0c221bf0",
  "presigned_url": "https://eachlabs-storage.s3.amazonaws.com/uploads/...?X-Amz-Algorithm=...&X-Amz-Signature=...",
  "public_url": "https://cdn-us.eachlabs.ai/uploads/0dce0f44-b8a5-4f30-91d4-5f6b0c221bf0.png",
  "expires_at": "2026-12-08T18:15:00Z",
  "required_headers": {
    "x-amz-meta-file-id": "0dce0f44-b8a5-4f30-91d4-5f6b0c221bf0"
  }
}
```

### Response Fields

| Field              | Type   | Description                                                                                                                                                                                                                                                                                                          |
| ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`               | string | Internal upload identifier.                                                                                                                                                                                                                                                                                          |
| `presigned_url`    | string | Short-lived URL to `PUT` the file to.                                                                                                                                                                                                                                                                                |
| `public_url`       | string | Stable URL to pass as a model input once the upload succeeds.                                                                                                                                                                                                                                                        |
| `expires_at`       | string | ISO-8601 timestamp when the **stored file** is automatically deleted (its retention deadline). Defaults to 180 days out; set a custom window with `expires_in_seconds`. This is **not** the upload-URL deadline — `presigned_url` itself only accepts uploads for 15 minutes after issuance (see [Limits](#limits)). |
| `required_headers` | object | Headers that **must** be sent verbatim on the `PUT` request. The presigned URL is signed for these exact values — missing or modified headers will fail with a signature mismatch. May be empty.                                                                                                                     |

## Step 2 — Upload the file

`PUT` the raw file bytes to `presigned_url`. Include every header from `required_headers` exactly as returned, plus a `Content-Type` matching the value sent in step 1.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -X PUT "$PRESIGNED_URL" \
    -H "Content-Type: image/png" \
    -H "x-amz-meta-file-id: 0dce0f44-b8a5-4f30-91d4-5f6b0c221bf0" \
    --data-binary @./photo.png
  ```

  ```python Python theme={"dark"}
  with open("photo.png", "rb") as f:
      requests.put(
          presigned_url,
          data=f,
          headers={
              "Content-Type": "image/png",
              **(required_headers or {}),
          },
      )
  ```

  ```javascript JavaScript theme={"dark"}
  await fetch(presigned_url, {
    method: "PUT",
    headers: {
      "Content-Type": file.type,
      ...(required_headers ?? {}),
    },
    body: file,
  });
  ```
</CodeGroup>

A `200 OK` from the `PUT` means the file is live. Use `public_url` as the input value when you [create a prediction](/api/predictions/create-prediction).

```json theme={"dark"}
{
  "model": "flux-1-1-pro",
  "version": "1.0.0",
  "input": {
    "image": "https://cdn-us.eachlabs.ai/uploads/0dce0f44-b8a5-4f30-91d4-5f6b0c221bf0.png",
    "prompt": "Make it look cinematic"
  }
}
```

## Limits

| Limit                  | Value                  | Notes                                                                                                                                                                            |
| ---------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Max file size          | **100 MB** per upload  |                                                                                                                                                                                  |
| Presigned URL lifetime | **15 minutes**         | How long `presigned_url` accepts the `PUT`, measured from issuance. This is **not** returned in the response; request a new presign if it lapses.                                |
| Stored file retention  | **180 days** (default) | How long the uploaded file is kept before automatic deletion — the value reported by `expires_at`. Override per upload with `expires_in_seconds` (min 60 seconds, max 365 days). |

## Error Responses

| Status | Body                                            | Description             |
| ------ | ----------------------------------------------- | ----------------------- |
| `400`  | `{"error": "content_type is required"}`         | Missing required field. |
| `401`  | `{"error": "Invalid or missing API key"}`       | Authentication failure. |
| `500`  | `{"error": "Failed to generate presigned URL"}` | Server error.           |

<Note>
  The presigned URL expires **15 minutes** after it's issued — this is separate from `expires_at`, which reports when the **stored file** is deleted (default 180 days), not the upload deadline. Upload soon after requesting; if the URL lapses, just request a new one.
</Note>
