# Uploads

Source: https://staging-docs.aiand.com/api/uploads/

The Uploads API lets you push large files (multi-hundred-MB videos, multi-GB clips) by splitting them into ≤ 64 MB parts. 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`.

Fully compatible with the [OpenAI Uploads API](https://platform.openai.com/docs/api-reference/uploads) — the official `openai` SDK's `client.uploads.upload(...)` helper drives the entire sequence in one call.

```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 MB images or ≤ 100 MB videos). Reach for `/v1/uploads` only when you're above that — typically large video clips.
</Aside>

## Limits

| Constraint | Value |
|------------|-------|
| Total file size | 8 GB |
| Per-part size | ≤ 64 MB |
| 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.

## Quick start

The easiest path is the OpenAI SDK's high-level helper, which handles chunking for you:

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

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

    with open("clip.mp4", "rb") as f:
        upload = client.uploads.upload(
            file=f,
            purpose="video",
            filename="clip.mp4",
            mime_type="video/mp4",
        )

    print(upload.file.id)  # → file-vid789...
    ```
  </TabItem>
  <TabItem label="JavaScript">
    ```javascript
    import OpenAI from "openai";
    import fs from "node:fs";

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

    const upload = await client.uploads.upload({
      file: fs.createReadStream("clip.mp4"),
      purpose: "video",
      filename: "clip.mp4",
      mimeType: "video/mp4",
    });

    console.log(upload.file.id); // → file-vid789...
    ```
  </TabItem>
</Tabs>

The SDK splits your file into parts, opens a session, uploads each part, and calls `complete`. 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. Each part must be ≤ 64 MB. Parts are submitted in the order returned to the client; out-of-order completion is not supported.

```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 | Ordered list of `part.id`s from step 2 |
| `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.

### 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 MB, `part_ids` empty or contains an id from a different upload |
| `400` | `invalid_request_error` | Adding a part to an upload that's no longer `pending`, or whose session has expired |
| `401` | `invalid_api_key` | Missing or invalid `Authorization` header |
| `404` | `not_found` | Unknown `upload_id`, or id belongs to a different organization |
| `500` | `server_error` | Internal failure |

## 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.
- 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/).
