# Uploads

Source: https://aiand-docs-staging.pages.dev/api/uploads/

The Uploads API lets you push large files (multi-hundred-MB videos, multi-GB clips) by splitting them into parts of 5 MiB to 64 MiB. Each part is uploaded against an upload session; calling `complete` assembles them into a regular [File object](/api/files/) you can reference in inference requests via `file_id`.

Wire-compatible with the [OpenAI Uploads API](https://platform.openai.com/docs/api-reference/uploads): the official Python `openai` SDK's `client.uploads.upload_file_chunked(...)` helper drives the entire sequence in one call. One difference: parts are assembled in the order they arrive, so upload them one after another and complete in that same order (see [Part sizes and order](#part-sizes-and-order)).

```http
POST /v1/uploads
POST /v1/uploads/{upload_id}/parts
POST /v1/uploads/{upload_id}/complete
POST /v1/uploads/{upload_id}/cancel
```

<Aside type="note">
  Use [`POST /v1/files`](/api/files/#upload-a-file) for files within the single-shot limits (≤ 100
  MiB images or ≤ 100 MiB videos). Reach for `/v1/uploads` only when you're above that — typically
  large video clips.
</Aside>

## Limits

| Constraint       | Value                                                                 |
| ---------------- | --------------------------------------------------------------------- |
| Total file size  | 8 GiB                                                                 |
| Per-part size    | 5 MiB to 64 MiB. Only the last part may be smaller than 5 MiB.        |
| Session lifetime | 1 hour from creation                                                  |
| Allowed purposes | `video` (recommended). `vision` works but rarely needed at this size. |

Sessions that aren't completed within 1 hour are automatically cancelled. The completed file inherits the standard 30-day expiry from the Files API.

### Part sizes and order

Cut the file into parts of at least 5 MiB (the SDK helper uses 64 MiB); only the last part may be smaller.

Parts are numbered in the order they reach the API and assembled in that order. Upload them one after another, and pass `part_ids` to `complete` in the same order. A part under 5 MiB that arrives before any other part is refused with a `400`, unless it is the whole file. If parts arrive out of file order, `complete` is refused with a `400` rather than assembling a file in the wrong order. The sizes of the listed parts must add up exactly to the `bytes` you declared when creating the session.

## Quick start

The easiest path is the Python SDK's high-level helper, which handles chunking for you. The Node SDK has no equivalent helper; the JavaScript tab drives the raw endpoints, which is the same sequence in a few more lines.

<Tabs>
  <TabItem label="Python">
    ```python
    from pathlib import Path
    from openai import OpenAI

    client = OpenAI(
        api_key="sk-your-api-key",
        base_url="https://api.aiand.com/v1",
    )

    upload = client.uploads.upload_file_chunked(
        file=Path("clip.mp4"),
        purpose="video",
        mime_type="video/mp4",
    )

    print(upload.file.id)  # → file-vid789...
    ```

  </TabItem>
  <TabItem label="JavaScript">
    ```javascript
    import OpenAI, { toFile } from "openai";
    import fs from "node:fs/promises";

    const client = new OpenAI({
      apiKey: "sk-your-api-key",
      baseURL: "https://api.aiand.com/v1",
    });

    const PART = 64 * 1024 * 1024;
    const data = await fs.readFile("clip.mp4");

    const session = await client.uploads.create({
      filename: "clip.mp4",
      purpose: "video",
      bytes: data.byteLength,
      mime_type: "video/mp4",
    });

    const partIds = [];
    for (let offset = 0; offset < data.byteLength; offset += PART) {
      const chunk = data.subarray(offset, Math.min(offset + PART, data.byteLength));
      const part = await client.uploads.parts.create(session.id, {
        data: await toFile(chunk, "part"),
      });
      partIds.push(part.id);
    }

    const upload = await client.uploads.complete(session.id, { part_ids: partIds });
    console.log(upload.file.id); // → file-vid789...
    ```

  </TabItem>
</Tabs>

Both paths split the file into uniform parts, open a session, upload the parts one after another, and call `complete` with the part ids in upload order. The returned `upload.file.id` is a regular `file-...` id you can use in chat completions exactly like any other file.

## Manual flow

If you're not using the SDK, here's the same flow against the raw HTTP endpoints.

### 1. Create a session

```http
POST /v1/uploads
```

| Field       | Type    | Required | Description                                        |
| ----------- | ------- | -------- | -------------------------------------------------- |
| `filename`  | string  | Yes      | Original filename                                  |
| `purpose`   | string  | Yes      | One of `vision`, `video`, `audio`, or `document`   |
| `bytes`     | integer | Yes      | Total declared size of the file                    |
| `mime_type` | string  | Yes      | Mime type; checked against the purpose's allowlist |

```bash
curl https://api.aiand.com/v1/uploads \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "clip.mp4",
    "purpose": "video",
    "bytes": 524288000,
    "mime_type": "video/mp4"
  }'
```

#### Response

```json
{
  "id": "upload-abc123",
  "object": "upload",
  "bytes": 524288000,
  "purpose": "video",
  "filename": "clip.mp4",
  "status": "pending",
  "created_at": 1719450000,
  "expires_at": 1719453600,
  "file": null
}
```

### 2. Upload parts

```http
POST /v1/uploads/{upload_id}/parts
```

Multipart form upload with a single `data` field. Every part except the last must be at least 5 MiB; no part may exceed 64 MiB. Upload parts one after another: they are numbered as they arrive, and `complete` must list them in that order. A part larger than the declared `bytes`, or a first part under 5 MiB that is not the whole file, is refused with a `400`.

```bash
curl https://api.aiand.com/v1/uploads/upload-abc123/parts \
  -H "Authorization: Bearer sk-your-api-key" \
  -F "data=@clip.part1.bin"
```

#### Response

```json
{
  "id": "part-xyz789",
  "object": "upload.part",
  "upload_id": "upload-abc123",
  "created_at": 1719450010
}
```

Repeat until every chunk of the source file has been submitted, capturing each `part.id` in order.

### 3. Complete the upload

```http
POST /v1/uploads/{upload_id}/complete
```

| Field      | Type     | Required | Description                                                                                   |
| ---------- | -------- | -------- | --------------------------------------------------------------------------------------------- |
| `part_ids` | string[] | Yes      | The `part.id`s from step 2, in upload order. Their sizes must add up to the declared `bytes`. |
| `md5`      | string   | No       | Optional MD5 of the assembled file                                                            |

```bash
curl https://api.aiand.com/v1/uploads/upload-abc123/complete \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "part_ids": ["part-xyz789", "part-uvw456", "part-rst123"] }'
```

#### Response

```json
{
  "id": "upload-abc123",
  "object": "upload",
  "bytes": 524288000,
  "purpose": "video",
  "filename": "clip.mp4",
  "status": "completed",
  "created_at": 1719450000,
  "expires_at": 1719453600,
  "file": {
    "id": "file-vid789",
    "object": "file",
    "bytes": 524288000,
    "purpose": "video",
    "filename": "clip.mp4",
    "created_at": 1719450090,
    "expires_at": 1722042090
  }
}
```

The `file.id` is now a regular file you can reference in chat completions. For documents, `file` also carries `page_count`, exactly as `GET /v1/files/{file_id}` does.

Completing is idempotent: if the response to a successful `complete` was lost, calling it again on the same session returns the same `file`.

### 4. Cancel (optional)

```http
POST /v1/uploads/{upload_id}/cancel
```

Aborts a pending session. Idempotent — already-completed or already-cancelled sessions return `404`.

```bash
curl -X POST https://api.aiand.com/v1/uploads/upload-abc123/cancel \
  -H "Authorization: Bearer sk-your-api-key"
```

## Reference the file in chat completions

Once `complete` returns a `file.id`, use it like any other file:

```json
{
  "model": "<video-capable-model>",
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "Describe what happens in this clip." },
        { "type": "file", "file": { "file_id": "file-vid789" } }
      ]
    }
  ]
}
```

See [Files → Reference a file in chat completions](/api/files/#reference-a-file-in-chat-completions) for full examples.

## Errors

| Status | `error.code`            | When                                                                                                                                                                                  |
| ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `invalid_request_error` | Missing field, unknown `purpose`, mime not in allowlist, declared `bytes` exceeds the per-purpose cap, part body > 64 MiB, `part_ids` empty or contains an id from a different upload |
| `400`  | `invalid_request_error` | A part that does not fit the session: the first part under 5 MiB when it is not the whole file, or a part larger than the declared `bytes`                                            |
| `400`  | `invalid_request_error` | `part_ids` out of upload order, listing a part twice, listing a part under 5 MiB anywhere but last, or with sizes that do not add up to the declared `bytes`                          |
| `400`  | `invalid_request_error` | Adding a part to, or completing, an upload that's no longer `pending` or whose session has expired                                                                                    |
| `401`  | `invalid_api_key`       | Missing or invalid `Authorization` header                                                                                                                                             |
| `403`  | `zero_data_retention`   | Your organization is on [zero data retention](/organizations/zero-data-retention/), which refuses uploads                                                                             |
| `404`  | `not_found`             | Unknown `upload_id`, or id belongs to a different organization                                                                                                                        |
| `500`  | `server_error`          | Internal failure. If the message says to start a new upload session, the session is now `failed` and cannot be retried.                                                               |

## Notes

- Upload sessions and resulting files are scoped to the **organization** that created them.
- Each part is durably stored as soon as it's accepted — if your client crashes mid-upload, you can resume by listing your existing `part_ids` (kept until session expiry) and continuing.
- A part re-sent after a lost response (the OpenAI SDKs do this automatically on connection errors and 5xx) becomes a second, unused part. Pass only the ids you want assembled; the duplicate is discarded with the session.
- The model must have a matching capability for the file's purpose. `purpose: "video"` requires a `video`-capable model — see [`GET /v1/models`](/api/models/).
