# Uploading assets

An asset is content submitted to CalmLens for analysis. You can create one from a file's bytes, from inline content, from a remote URL, or in sequential parts for larger uploads. This guide shows each upload mode, the limits that apply, and how to attach metadata and folders. For every field and error code, see the [Assets API reference](/docs/api/assets).

Requests authenticate with a project API key sent from your server. See [Authentication](/docs/guides/authentication).

## Upload with an SDK

For files, pass a file, buffer or stream to your SDK. It selects the upload mode and handles authentication.

**python**

```python
import os
from calmlens import CalmLensClient

client = CalmLensClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id=os.environ["CALMLENS_PROJECT_ID"],
)

with open("cat.jpg", "rb") as content:
    asset = client.assets.create({
        "name": "cat.jpg",
        "visibility": "private",
        "processingOptions": {"keepContentAfterProcessing": True},
        "content": {"data": content, "type": "image/jpeg"},
    })
print(asset["id"])
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    calmlens "github.com/Censkh/CalmLens/packages/go-sdk"
)

func main() {
    client, err := calmlens.NewClient(calmlens.Options{APIKey: os.Getenv("CALMLENS_API_KEY"), ProjectID: os.Getenv("CALMLENS_PROJECT_ID")})
    if err != nil { log.Fatal(err) }
    ctx := context.Background()
    content, err := os.Open("cat.jpg")
    if err != nil { log.Fatal(err) }
    defer content.Close()
    asset, err := client.Assets.Create(ctx, calmlens.SubmitAssetOptions{Name: "cat.jpg", Visibility: "private", ProcessingOptions: calmlens.JSON{"keepContentAfterProcessing": true}, Content: &calmlens.Content{Data: content, Type: "image/jpeg"}})
    if err != nil { log.Fatal(err) }
    fmt.Println(asset.ID)
}

```

**JavaScript**

```javascript
import { readFile } from "node:fs/promises";
import { CalmLensClient } from "@calmlens/sdk";

const client = new CalmLensClient({
  apiKey: process.env.CALMLENS_API_KEY!,
  projectId: process.env.CALMLENS_PROJECT_ID!,
});

const asset = await client.assets.create({
  name: "cat.jpg",
  visibility: "private",
  processingOptions: { keepContentAfterProcessing: true },
  content: {
    buffer: await readFile("./cat.jpg"),
    type: "image/jpeg",
  },
});
```

**ruby**

```ruby
require 'calmlens'
client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
asset = client.assets.create(name: 'cat.jpg', content: {data: File.binread('cat.jpg'), type: 'image/jpeg'})
puts asset['id']
client.close
```

**rust**

```rust
use calmlens::{models::*, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(std::env::var("CALMLENS_API_KEY")?, std::env::var("CALMLENS_PROJECT_ID")?)?;
    let bytes = std::fs::read("cat.jpg")?;
    let asset = client.assets().create(SubmitAsset::new("cat.jpg", Content::bytes(bytes, "image/jpeg"))).await?;
    println!("{}", asset.id);
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensClient;
import com.calmlens.Models.*;

public class Example {
  public static void main(String[] args) throws Exception {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    byte[] bytes = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("cat.jpg"));
    Asset asset = client.assets.create(new SubmitAsset("cat.jpg", Content.bytes(bytes, "image/jpeg")).visibility(Visibility.PRIVATE).processingOptions(new ProcessingOptions().keepContentAfterProcessing(true)));
    System.out.println(asset.id());
  }
}
```

**curl**

```sh
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/assets/upload?name=cat.jpg&contentType=image%2Fjpeg&queue=true&visibility=private&processingOptions=%7B%22keepContentAfterProcessing%22%3Atrue%7D" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "X-CalmLens-Upload: content" \
  -H "Content-Type: image/jpeg" \
  --data-binary @cat.jpg
```

## Waiting for the decision

Choose either flow independently of the upload mode:

| Flow | SDK | Direct HTTP | Result |
| --- | --- | --- | --- |
| Moderate in place | `client.assets.moderate()` | `queue=false` (the default) | Wait for classification and read the decision from the response. |
| Queue moderation | `client.assets.create()` | `queue=true` | Receive an asset ID while classification runs, then get the decision through an [`asset.classified`](/docs/api/events#event-asset-classified) webhook. |

Text and images support both flows. Audio and video always run asynchronously and return `pending`, even with `queue=false`. A multipart upload uses the queue mode chosen when it starts: CalmLens saves it with the upload, so whichever client sends the final part gets the same behaviour. See [Queue mode](/docs/guides/multipart-uploads#queue-mode).

For queued uploads, save the asset ID and [configure a webhook](/docs/guides/webhooks) for [`asset.classified`](/docs/api/events#event-asset-classified) and [`asset.failed`](/docs/api/events#event-asset-failed). Keep content unpublished until `approved`; a successful upload alone is not a moderation decision.

Completed assets use the nested report described in [Report format](/docs/guides/moderation-reports). Check each category's `status` before reading `score`.

See [Asset lifecycle](/docs/guides/asset-lifecycle) for each status and how to wait for a queued asset, and [Delivering assets](/docs/guides/delivering-assets) to serve asset content, create signed URLs for private assets, and apply lenses.

### Fallback when webhooks are unavailable

If your integration cannot receive webhooks, retrieve the asset periodically with `client.assets.retrieve({ assetId })`. This requires a retry loop in your application; the retrieval call does not wait for classification.

Start with a short delay, increase it between checks using exponential backoff, and cap the interval. Set an overall timeout so checks cannot continue indefinitely. Stop when the status is `approved`, `rejected` or `error`; keep content unpublished while it is `pending`. If the timeout expires, treat the result as unresolved and keep the content unpublished.

`client.assets.retrieve({ assetId })`

## Upload modes

### Choosing a mode

| You have | Use | Endpoint |
| --- | --- | --- |
| A file, buffer or stream | [Direct binary upload](/docs/guides/uploading-assets#direct-binary-upload) | [`POST /projects/:projectId/assets/upload`](/docs/api/assets#upload-asset-file) |
| Base64 content or bytes inside a JSON request | [JSON upload](/docs/guides/uploading-assets#json-upload-with-base64-content) | [`POST /projects/:projectId/assets`](/docs/api/assets#create-asset) |
| Text you have already extracted | [Plain text upload](/docs/guides/uploading-assets#plain-text-upload) | [`POST /projects/:projectId/assets`](/docs/api/assets#create-asset) |
| A public web page or file URL | [Remote URL upload](/docs/guides/uploading-assets#remote-url-upload) | [`POST /projects/:projectId/assets`](/docs/api/assets#create-asset) |
| A large file, or one you produce in chunks | [Multipart upload](/docs/guides/uploading-assets#multipart-upload) | [`POST /projects/:projectId/assets/multipart-upload`](/docs/api/assets#start-multipart-file-upload) or [`POST /projects/:projectId/assets`](/docs/api/assets#create-asset), then [`POST /assets/:assetId/part`](/docs/api/assets#upload-asset-part) |

The SDKs choose the mode for you: they stream files with direct binary or multipart uploads and send other inputs as JSON.

### Direct binary upload

For direct HTTP integrations, send the unencoded file to [`POST /projects/:projectId/assets/upload`](/docs/api/assets#upload-asset-file). Metadata goes in query parameters. The [Assets reference](/docs/api/assets) documents the complete request.

### JSON upload

[`POST /projects/:projectId/assets`](/docs/api/assets#create-asset) remains available for base64, plain-text, and remote URL submissions.

- Direct image uploads must be smaller than <code>10MB</code>.
- All other file types support up to <code>5GB</code>.
- The request body can contain `content.base64`, `content.plainText`, or `url`.

#### JSON upload with base64 content

```bash
curl -X POST "https://api.calmlens.com/v1/projects/your-project-id/assets?queue=true" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "cat.png",
  "description": "Homepage hero image",
  "visibility": "private",
  "processingOptions": {
    "keepContentAfterProcessing": true
  },
  "content": {
    "base64": "your-base64-file",
    "type": "image/png"
  }
}'
```

#### Plain text upload

```bash
curl -X POST "https://api.calmlens.com/v1/projects/your-project-id/assets?queue=true" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "notes.txt",
  "content": {
    "plainText": "This is the extracted text to moderate.",
    "type": "text/plain"
  }
}'
```

#### Remote URL upload

Remote URLs must use HTTP or HTTPS. Private IP addresses, localhost names and URL credentials are rejected, and each redirect is checked. Downloads time out after 30 seconds and are limited to 10 MB for images and 20 MB for other files. Use multipart upload for larger files.

```bash
curl -X POST "https://api.calmlens.com/v1/projects/your-project-id/assets?queue=true" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "landing-page",
  "url": "https://example.com"
}'
```

### Multipart upload

The SDK chooses multipart uploads when needed. For manual chunks, ordering, finalisation and recovery, see [Multipart uploads](/docs/guides/multipart-uploads).

## Retrying uploads

Each successful upload creates an asset that counts toward your usage, and a request that timed out may still have succeeded. Send an `Idempotency-Key` header with a unique value, such as a UUID, and reuse it when you retry: if the first request succeeded, CalmLens returns its response instead of creating a second asset. An `externalId` is also unique within a project, so a retry that reuses one is refused with [`409 asset/duplicate-external-id`](/docs/api/assets#post-asset-error-asset-duplicate-external-id) rather than creating a duplicate. See [Retrying requests safely](/docs/guides/error-handling#retrying-requests-safely).

## Asset types and limits

CalmLens detects the asset type from the content's MIME type.

<table>
  <thead>
    <tr>
      <th>Asset type</th>
      <th>How it is detected</th>
      <th>Typical input</th>
      <th>Upload limit</th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td><code>image</code></td>
      <td><code>image/\*</code> MIME types</td>
      <td>PNG, JPEG, WebP, GIF, AVIF</td>

      <td>
        <code>
          20MB
        </code>
      </td>
    </tr>

    <tr>
      <td><code>video</code> (beta)</td>
      <td><code>video/\*</code> MIME types</td>
      <td>MP4, MOV, WebM</td>

      <td>
        <code>
          5GB
        </code>
      </td>
    </tr>

    <tr>
      <td><code>audio</code> (beta)</td>
      <td><code>audio/\*</code> MIME types</td>
      <td>MP3, WAV, M4A</td>

      <td>
        <code>
          5GB
        </code>
      </td>
    </tr>

    <tr>
      <td><code>plaintext-document</code></td>
      <td>Plain text MIME types</td>
      <td>TXT, JSON, CSV, YAML</td>

      <td>
        <code>
          5GB
        </code>
      </td>
    </tr>

    <tr>
      <td><code>multimedia-document</code></td>
      <td>Supported document MIME types</td>
      <td>PDF, DOCX, DOC, RTF, ODT</td>

      <td>
        <code>
          5GB
        </code>
      </td>
    </tr>

    <tr>
      <td><code>website</code></td>
      <td><code>text/html</code> MIME type or URL upload</td>
      <td>HTML or remote website URL</td>

      <td>
        <code>
          5GB
        </code>
      </td>
    </tr>

    <tr>
      <td><code>file</code></td>
      <td>Fallback for other file types</td>
      <td>Other binary files</td>

      <td>
        <code>
          5GB
        </code>
      </td>
    </tr>
  </tbody>
</table>

Image uploads are also constrained by a maximum image dimension of `12,000 x 12,000` pixels.

## Asset metadata

Attach custom fields to your uploads. See [Asset metadata](/docs/guides/organising-assets#asset-metadata) for field definitions and examples.

## Folders

Pass a `folderId` to organise an upload. See [Folders](/docs/guides/organising-assets#folders) for creating, moving and managing a folder tree.
