---
title: Uploads and files
description: "Put your own images, video and audio into the Library and use them as references: a base64 body for small files, a signed PUT for large ones, or your own https URLs."
---

# Uploads and files

Upload a file once, keep its asset id, and reuse it as a reference. Uploaded media lives in the same Library as generated media and can be filed into a project.

![Uploads](../assets/art/uploads.jpg)

## Two ways to upload

| Route | Use it for | Limit |
| --- | --- | --- |
| Base64 JSON: `POST /assets` | Small PNG, JPEG or WebP images | About 10 MB decoded; the `data` field accepts at most 14,000,000 base64 characters |
| Signed PUT: `POST /assets/uploads`, PUT bytes, then complete | Images, video, audio and GLB models | Images: 100 MiB; video: 50 GiB; audio: 5 GiB; 3D: 200 MiB |

The base64 route returns a stored asset immediately. The signed route first creates an `uploading` asset; only completion verifies the bytes and makes it `ready`. The CLI's upload command selects the upload flow for the file.

## Small files

| Property | Value |
| --- | --- |
| Endpoint | `POST /assets` |
| Required fields | `content_type`, `data` |
| File types | `image/png`, `image/jpeg`, `image/webp` |
| Optional filing | `filename`, `project_id` |
| Success | `201` with an `Asset` |

These are examples, not live upload captures. Set `NOLGIA_TOKEN` as in the [Quick Start](./getting-started.html) and replace `reference.png` with a small local image. The curl command uses macOS `base64 -i`; on GNU systems use `base64 reference.png`. Rust also uses the `base64` crate for encoding.

```bash tab="curl" title="Small image upload example"
$ curl -sS https://api.nolgia.ai/v1/assets \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content_type":"image/png","filename":"reference.png","data":"<the file, base64-encoded>"}'
```
```bash tab="CLI" title="Local file upload example"
$ nolgia --json assets upload reference.png
```
```ts tab="TypeScript" title="Small image upload example"
import { readFile } from "node:fs/promises";
import { createNolgiaClient } from "@nolgia/sdk";

const nolgia = createNolgiaClient(process.env.NOLGIA_TOKEN!);
const { data: asset, error } = await nolgia.POST("/assets", {
  body: {
    content_type: "image/png",
    filename: "reference.png",
    data: (await readFile("reference.png")).toString("base64"),
  },
});
if (error) throw new Error(`${error.title}: ${error.detail ?? ""}`);
console.log(asset.id);
```
```python tab="Python" title="Small image upload example"
import base64
import os
from pathlib import Path
from nolgia import AuthenticatedClient
from nolgia.api.assets import upload_asset
from nolgia.models import Asset, UploadAssetRequest

client = AuthenticatedClient(base_url="https://api.nolgia.ai/v1", token=os.environ["NOLGIA_TOKEN"])
asset = upload_asset.sync(client=client, body=UploadAssetRequest.from_dict({
    "content_type": "image/png",
    "filename": "reference.png",
    "data": base64.b64encode(Path("reference.png").read_bytes()).decode("ascii"),
}))
if not isinstance(asset, Asset):
    raise SystemExit(f"refused: {asset}")
print(asset.id)
```
```rust tab="Rust" title="Small image upload example"
use base64::{engine::general_purpose::STANDARD, Engine};
use nolgia_client::ClientBuilder;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = ClientBuilder::new("https://api.nolgia.ai/v1")
        .bearer_token(std::env::var("NOLGIA_TOKEN")?)
        .build()?;
    let encoded = STANDARD.encode(std::fs::read("reference.png")?);
    let asset = client.upload_asset()
        .body_map(|b| b.content_type("image/png").data(encoded))
        .send().await?.into_inner();
    println!("{}", asset.id);
    Ok(())
}
```

The HTTP response is an `Asset`; this schema-built excerpt shows a stored image. The snippets print its reusable id. The omitted `model` field records upload provenance, not a generation model from the catalog.

```json title="201 Asset — schema-built excerpt"
{
  "id": "11111111-1111-4111-8111-111111111111",
  "user_id": "22222222-2222-4222-8222-222222222222",
  "modality": "image",
  "display_name": "reference",
  "mime_type": "image/png",
  "signed_url": "https://storage.googleapis.com/example/reference.png?…",
  "expires_at": "2026-09-21T05:00:00Z",
  "status": "ready",
  "created_at": "2026-09-21T03:30:00Z"
}
```

| Response field | Meaning |
| --- | --- |
| `id` | Store this id and pass it to a supported asset-reference field |
| `user_id`, `created_at` | Who uploaded the file and when |
| `modality` | The media kind |
| `display_name`, `mime_type` | Human-readable name and stored media type |
| `signed_url`, `expires_at` | Temporary download location and its real expiry |
| `status` | `ready` means the file is stored and usable |

## Large files, step by step

![Upload handshake: create an upload, PUT the bytes, and complete the asset](../assets/diagrams/upload-handshake.svg)

### 1. Create the upload

| Property | Value |
| --- | --- |
| Endpoint | `POST /assets/uploads` |
| Success | `201` with an `AssetUpload` |
| Required values | Original filename, exact MIME type and exact byte size |
| Lifetime | The signed PUT URL lasts 30 minutes |

<!-- gen:fields schema=CreateAssetUploadRequest -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `filename` | string | Yes | Original filename, kept as asset metadata. |
| `content_type` | one of `video/mp4`, `video/quicktime`, `video/webm`, `audio/mpeg`, `audio/wav`, `audio/ogg`, `audio/webm`, `audio/mp4`, `image/png`, `image/jpeg`, `image/webp`, `model/gltf-binary` | Yes | MIME type of the file. The signed PUT URL is bound to it, so the upload request must send exactly this Content-Type header. |
| `size_bytes` | integer | Yes | Exact size of the file in bytes. Verified against the uploaded object on completion. Limits: 50 GiB for video, 5 GiB for audio, 100 MiB for images, 200 MiB for 3D models. |
| `tags` | array of string | No | Tags to apply to the asset; trimmed, lowercased, and de-duplicated. |
| `display_name` | string, nullable | No | Optional human-readable display name stored on the pre-created asset (trimmed). Defaults to empty. |
| `project_id` | string, nullable | No | Files the uploaded asset into this caller-owned project directly (no tag matching involved); an unknown or foreign project returns 400. Without it, the asset lands in the default "Library" project. |
<!-- /gen -->

For a local `final-cut.mp4`, measure the size instead of estimating it. Keep the response in `upload` for the next two calls.

```bash title="Create signed upload example"
$ curl -sS https://api.nolgia.ai/v1/assets/uploads \
  -H "Authorization: Bearer $NOLGIA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"filename":"final-cut.mp4","content_type":"video/mp4","size_bytes":<the exact file size in bytes>}'
```

```json title="201 AssetUpload — schema-built example"
{
  "upload_id": "33333333-3333-4333-8333-333333333333",
  "asset_id": "44444444-4444-4444-8444-444444444444",
  "upload_url": "https://storage.googleapis.com/example/final-cut.mp4?…",
  "expires_at": "2026-09-21T04:00:00Z"
}
```

<!-- gen:fields schema=AssetUpload -->
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `upload_id` | string | Yes | Identifier for `POST /assets/uploads/{id}/complete`. |
| `asset_id` | string | Yes | The pre-created asset (status `uploading`) the bytes will belong to. |
| `upload_url` | string | Yes | Signed PUT URL. Upload with `PUT <upload_url>` and a `Content-Type` header exactly matching the declared `content_type`; no Authorization header is needed. |
| `expires_at` | string | Yes | Expiry of `upload_url` (30 minutes after creation). |
<!-- /gen -->

### 2. PUT the bytes

| Property | Value |
| --- | --- |
| Destination | The returned `upload_url`, used as-is |
| Method and body | `PUT` with the raw file bytes |
| Header | `Content-Type: video/mp4`, exactly matching the declaration |
| Authentication | The URL contains its authorization; send no bearer token |

```bash title="PUT the file example"
$ curl -sS -X PUT "<upload_url from the response>" -H "Content-Type: video/mp4" --upload-file final-cut.mp4
```

```http title="Storage PUT success — example"
HTTP/1.1 200 OK
Content-Length: 0
```

The successful storage PUT has no JSON body. It does not complete the asset; the API still needs the verification call below.

> [!WARNING]
> The PUT's `Content-Type` must match `content_type` exactly because the signature covers it. A different type, an expired URL or an incomplete file is not fixed by calling complete. Create a new upload if its 30-minute URL has expired.

### 3. Complete the asset

| Property | Value |
| --- | --- |
| Endpoint | `POST /assets/uploads/{id}/complete`; `{id}` is `upload_id` |
| Verification | The stored object's size must equal declared `size_bytes` |
| Success | `200` with the `ready` asset |
| Retry behavior | Completing an already completed upload returns the same asset |
| Incomplete upload | Missing object or mismatched size returns `409` |

```bash title="Complete signed upload example"
$ curl -sS -X POST "https://api.nolgia.ai/v1/assets/uploads/<upload_id from the response>/complete" -H "Authorization: Bearer $NOLGIA_TOKEN"
```

```json title="200 Asset — schema-built completion excerpt"
{
  "id": "44444444-4444-4444-8444-444444444444",
  "user_id": "22222222-2222-4222-8222-222222222222",
  "modality": "video",
  "mime_type": "video/mp4",
  "signed_url": "https://storage.googleapis.com/example/final-cut.mp4?…",
  "expires_at": "2026-09-21T05:00:00Z",
  "status": "ready",
  "created_at": "2026-09-21T03:30:00Z"
}
```

| Response field | Meaning |
| --- | --- |
| `id` | The same id as the create response's `asset_id` |
| `status` | Changes from `uploading` to `ready` after verification |
| `signed_url`, `expires_at` | Download URL, separate from the upload URL |
| `modality`, `mime_type` | The stored video's kind and media type |
| `user_id`, `created_at` | Owner and original creation time |

Default `GET /assets` listings hide unfinished uploads. Media duration, audio detection and thumbnails may arrive later through the media sweep; `ready` does not mean every optional metadata field is already populated.

## Using your own storage

Pass your own HTTPS URL to a request's supported `*_url` field, or a Library asset id to its `*_asset_id` / `*_asset_ids` field. The URL must remain reachable when processing starts. Reference support still depends on the model; see [Common model arguments](./model-arguments.html).

These two image request bodies illustrate the same choice. Use a reference-capable model and replace the URL or id with your own.

```json title="POST /generate/image with image_url — request example"
{
  "model": "gpt-image-2",
  "prompt": "Place this product on a plain white background",
  "image_url": "https://media.example.com/product.png"
}
```

```json title="POST /generate/image with reference_asset_ids — request example"
{
  "model": "gpt-image-2",
  "prompt": "Place this product on a plain white background",
  "reference_asset_ids": ["11111111-1111-4111-8111-111111111111"]
}
```

Both submit a job. This schema-built response applies to either request; follow the id with [submit and poll](./jobs.html).

```json title="202 Job — schema-built example"
{
  "id": "55555555-5555-4555-8555-555555555555",
  "user_id": "22222222-2222-4222-8222-222222222222",
  "modality": "image",
  "model": "gpt-image-2",
  "status": "queued",
  "created_at": "2026-09-21T03:30:00Z",
  "updated_at": "2026-09-21T03:30:00Z"
}
```

| Response field | Meaning |
| --- | --- |
| `id` | The durable job id to poll |
| `status` | `queued` means accepted, not finished |
| `user_id`, `modality`, `model` | Caller and selected generation route |
| `created_at`, `updated_at` | Acceptance and latest job-update times |

> [!TIP]
> Prefer asset ids for files already in your Library. The server re-signs those references at execution, so a queued job does not start with an expired saved download URL. An external URL remains your responsibility to keep accessible.

## Upload details

| Property | Value |
| --- | --- |
| Base64 limit | About 10 MB decoded; PNG, JPEG and WebP only |
| Signed-upload image limit | 100 MiB |
| Signed-upload video limit | 50 GiB |
| Signed-upload audio limit | 5 GiB |
| Signed-upload 3D limit | 200 MiB |
| PUT content type | Must exactly match the declared `content_type` |
| Signed-upload tags | Up to 10; trimmed, lowercased and deduplicated; completion applies tag-based project associations |
| Direct project filing | Set `project_id`; unknown or foreign project returns `400`; omitted means the default Library project |
| Display name | Supply `filename` for a recognizable name; signed uploads also accept `display_name` |

Accepted MIME types for signed uploads are generated from the request schema:

<!-- gen:mime schema=CreateAssetUploadRequest field=content_type -->
- `video/mp4`
- `video/quicktime`
- `video/webm`
- `audio/mpeg`
- `audio/wav`
- `audio/ogg`
- `audio/webm`
- `audio/mp4`
- `image/png`
- `image/jpeg`
- `image/webp`
- `model/gltf-binary`
<!-- /gen -->

## Google Drive

Import a media file into the Library or export an asset to Google Drive. Both calls use a short-lived Google OAuth token with the `drive.file` scope for that request only. Import accepts the signed-upload media types and size limits; Google Docs, Sheets, and Slides are not media imports. These calls are available on Studio plans, Team, and Enterprise.

<!-- gen:endpoints paths=/assets/imports/google-drive,/assets/{id}/exports/google-drive -->
| Method | Path | What it does |
| --- | --- | --- |
| [POST](../api/#tag/assets/post/assets/imports/google-drive) | `/assets/imports/google-drive` | Import a Google Drive file into the library. |
| [POST](../api/#tag/assets/post/assets/{id}/exports/google-drive) | `/assets/{id}/exports/google-drive` | Save an asset to Google Drive. |
<!-- /gen -->

## Expiry and access

Upload URLs and download URLs have different lifetimes. Read [Storage and data retention](./storage.html) for signed URL renewal and deletion, and [File access controls](./file-access.html) for Library scope and share links that remain useful after a download URL expires.

:::cards
- [Storage and data retention](./storage.html): Refresh download URLs and manage trash or permanent deletion.
- [File access controls](./file-access.html): Share a file with an expiry and a revoke.
- [Common model arguments](./model-arguments.html): Choose a supported reference field for your model.
:::
