# Organising assets

Use folders to organise content and scope access, metadata to attach structured information, and external IDs to connect assets to your own records.

## External IDs

Use a stable `externalId` for your application record. A duplicate ID within a project returns [`409 asset/duplicate-external-id`](/docs/api/assets#post-asset-error-asset-duplicate-external-id). Folders and moves do not change asset IDs or CDN URLs.

## Asset metadata

Attach custom metadata as an array of `{ id, value }` items. To use a value for portal filtering or grouping, first define a field with the same ID in [Metadata fields](/portal/metadata). The field title and type control how the item appears in the portal; uploaded values remain JSON values and are not checked against the field type.

**JavaScript**

```javascript
const asset = await client.assets.create({
  name: "campaign-image.jpg",
  content: { buffer: imageBytes, type: "image/jpeg" },
  metadata: [{ id: "campaign.id", value: "launch-2026" }],
});
```

**python**

```python
import os
import json
from calmlens import CalmLensClient

client = CalmLensClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id="your-project-id",
)

options = json.loads("{\"name\":\"campaign-image.jpg\",\"content\":{\"base64\":\"<base64-encoded-image>\",\"type\":\"image/jpeg\"},\"metadata\":[{\"id\":\"campaign.id\",\"value\":\"launch-2026\"}]}")
response = client.assets.create(options)
print(response)
```

**go**

```go
package main

import (
    "context"
    "encoding/json"
    "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: "your-project-id",
    })
    if err != nil { log.Fatal(err) }

    var options calmlens.SubmitAssetOptions
    if err := json.Unmarshal([]byte("{\"name\":\"campaign-image.jpg\",\"metadata\":[{\"id\":\"campaign.id\",\"value\":\"launch-2026\"}]}"), &options); err != nil { log.Fatal(err) }
    var content calmlens.Content
    if err := json.Unmarshal([]byte("{\"base64\":\"<base64-encoded-image>\",\"type\":\"image/jpeg\"}"), &content); err != nil { log.Fatal(err) }
    options.Content = &content
    response, err := client.Assets.Create(context.Background(), options)
    if err != nil { log.Fatal(err) }
    fmt.Println(response)
}
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.assets.create(name: "campaign-image.jpg", content: {"base64" => "<base64-encoded-image>", "type" => "image/jpeg"}, metadata: [{"id" => "campaign.id", "value" => "launch-2026"}])
puts result
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 result = client.assets().create(SubmitAsset { name: "campaign-image.jpg".into(), content: Some(Content { mime_type: "image/jpeg".into(), payload: Payload::Base64("<base64-encoded-image>".into()), ..Default::default() }), metadata: Some(vec![MetadataItem { id: "campaign.id".into(), value: serde_json::Value::from("launch-2026"), ..Default::default() }]), ..Default::default() }).await?;
    println!("{result:?}");
    Ok(())
}
```

**java**

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

public class Example {
  public static void main(String[] args) {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    var result = client.assets.create(new SubmitAsset().name("campaign-image.jpg").content(new Content().base64("<base64-encoded-image>").mimeType("image/jpeg")).metadata(java.util.Arrays.asList(new MetadataItem().id("campaign.id").value("launch-2026"))));
    System.out.println(result);
  }
}
```

**curl**

```sh
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/assets?queue=true" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "campaign-image.jpg",
  "content": {
    "base64": "<base64-encoded-image>",
    "type": "image/jpeg"
  },
  "metadata": [
    {
      "id": "campaign.id",
      "value": "launch-2026"
    }
  ]
}'
```

For [`POST /projects/:projectId/assets`](/docs/api/assets#create-asset), include the same `metadata` array in the JSON body. For the direct binary and streamed multipart endpoints, pass the JSON array as the URL-encoded `metadata` query parameter; the JavaScript SDK handles this encoding. Manage field definitions in [Metadata fields](/portal/metadata).

## Folders

Folders organize assets hierarchically within a project without changing asset IDs or CDN URLs. Like files in a filesystem, folders can contain assets and nested subfolders. Pass a `folderId` when you upload to put the new asset straight into a folder. See the [Folders API reference](/docs/api/folders) for complete folder endpoint references and schemas.

- **Stable IDs**: Folders and assets are identified by immutable UUIDs. Folder names and paths are display values and cannot be used as resource identifiers in API calls.
- **Nesting**: Create folders with [`POST /projects/:projectId/folders`](/docs/api/folders#create-folder) at the project root (`parentId: null` or omitted) or nested under an existing folder by passing its `parentId`.
- **Renaming**: Rename a folder with [`PATCH /folders/:folderId`](/docs/api/folders#update-folder).
- **Moving**: Move a folder with [`POST /folders/:folderId/move`](/docs/api/folders#move-folder) and `{ "parentId": string | null }`, and an asset with [`POST /assets/:assetId/move`](/docs/api/assets#move-asset) and `{ "folderId": string | null }`. Pass `null` to move a folder or asset to the project root.
- **Name uniqueness**: Folder names must be unique among siblings sharing the same parent folder (or at the root). The same folder name can be used in different branches of the folder tree.
- **Hierarchy cycles**: Moving a folder into itself or one of its descendants is rejected with [`409 folder/parent-cycle`](/docs/api/folders#move-folder-error-folder-parent-cycle).
- **Safe deletion**: Only empty folders can be deleted. Attempting to delete a folder that still contains assets or child folders is rejected with [`409 folder/not-empty`](/docs/api/folders#delete-folder-error-folder-not-empty).

**python**

```python
import os
from calmlens import CalmLensClient

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

parent = client.folders.create({"name": "Campaigns"})
child = client.folders.create({"name": "Launch", "parentId": parent["id"]})
client.assets.move({"assetId": "your-asset-id", "folderId": child["id"]})
client.folders.move({"folderId": child["id"], "parentId": None})
```

**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()
    parent, err := client.Folders.Create(ctx, calmlens.CreateFolderOptions{Name: "Campaigns"})
    if err != nil { log.Fatal(err) }
    child, err := client.Folders.Create(ctx, calmlens.CreateFolderOptions{Name: "Launch", ParentID: &parent.ID})
    if err != nil { log.Fatal(err) }
    _, err = client.Assets.Move(ctx, calmlens.MoveAssetOptions{AssetID: "your-asset-id", FolderID: &child.ID})
    if err != nil { log.Fatal(err) }
    folder, err := client.Folders.Move(ctx, calmlens.MoveFolderOptions{FolderID: child.ID, ParentID: nil})
    if err != nil { log.Fatal(err) }
    fmt.Println(folder.ID)
}

```

**JavaScript**

```javascript
const parent = await client.folders.create({ name: "Campaigns" });
const child = await client.folders.create({ name: "Launch", parentId: parent.id });

await client.assets.move({ assetId: asset.id, folderId: child.id });
await client.folders.move({ folderId: child.id, parentId: null });
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.folders.create(name: "Campaigns")
puts result
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 result = client.folders().create(CreateFolder { name: "Campaigns".into(), ..Default::default() }).await?;
    println!("{result:?}");
    Ok(())
}
```

**java**

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

public class Example {
  public static void main(String[] args) {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    var result = client.folders.create(new CreateFolder().name("Campaigns"));
    System.out.println(result);
  }
}
```

**curl**

```sh
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/folders" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "Campaigns"
}'

# Replace parentId below with the returned folder id.
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/folders" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "Launch",
  "parentId": "your-parent-folder-id"
}'

# Replace folderId below and set FOLDER_ID to the returned child id.
curl -X POST "https://api.calmlens.com/v1/assets/$ASSET_ID/move" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "folderId": "your-child-folder-id"
}'

curl -X POST "https://api.calmlens.com/v1/folders/$FOLDER_ID/move" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "parentId": null
}'
```

To manage the folder tree itself:

**python**

```python
import os
from calmlens import CalmLensClient

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

marketing = client.folders.create({"name": "Marketing"})
campaigns = client.folders.create({"name": "Campaigns", "parentId": marketing["id"]})
folders = client.folders.list()
folder = client.folders.retrieve({"folderId": campaigns["id"]})
updated = client.folders.update({"folderId": campaigns["id"], "name": "Summer Campaigns"})
client.assets.move({"assetId": "your-asset-id", "folderId": campaigns["id"]})
client.assets.move({"assetId": "your-asset-id", "folderId": None})
client.folders.move({"folderId": campaigns["id"], "parentId": None})
client.folders.delete({"folderId": campaigns["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()
    marketing, err := client.Folders.Create(ctx, calmlens.CreateFolderOptions{Name: "Marketing"})
    if err != nil { log.Fatal(err) }
    campaigns, err := client.Folders.Create(ctx, calmlens.CreateFolderOptions{Name: "Campaigns", ParentID: &marketing.ID})
    if err != nil { log.Fatal(err) }
    folders, err := client.Folders.List(ctx)
    if err != nil { log.Fatal(err) }
    fmt.Println(folders)
    _, err = client.Folders.Retrieve(ctx, calmlens.FolderIDOptions{FolderID: campaigns.ID})
    if err != nil { log.Fatal(err) }
    name := "Summer Campaigns"
    _, err = client.Folders.Update(ctx, calmlens.UpdateFolderOptions{FolderID: campaigns.ID, Name: &name})
    if err != nil { log.Fatal(err) }
    _, err = client.Assets.Move(ctx, calmlens.MoveAssetOptions{AssetID: "your-asset-id", FolderID: &campaigns.ID})
    if err != nil { log.Fatal(err) }
    _, err = client.Assets.Move(ctx, calmlens.MoveAssetOptions{AssetID: "your-asset-id", FolderID: nil})
    if err != nil { log.Fatal(err) }
    _, err = client.Folders.Move(ctx, calmlens.MoveFolderOptions{FolderID: campaigns.ID, ParentID: nil})
    if err != nil { log.Fatal(err) }
    if err := client.Folders.Delete(ctx, calmlens.FolderIDOptions{FolderID: campaigns.ID}); err != nil { log.Fatal(err) }
}

```

**JavaScript**

```javascript
import { CalmLensClient } from "@calmlens/sdk";

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

// 1. Create a root folder
const marketing = await client.folders.create({
  name: "Marketing",
});

// 2. Create a nested child folder
const campaigns = await client.folders.create({
  name: "Campaigns",
  parentId: marketing.id,
});

// 3. List all folders in the project
const folders = await client.folders.list();

// 4. Retrieve a folder by ID
const folder = await client.folders.retrieve({
  folderId: campaigns.id,
});

// 5. Rename a folder
const updated = await client.folders.update({
  folderId: campaigns.id,
  name: "Summer Campaigns",
});

// 6. Move an asset into a folder
await client.assets.move({
  assetId: "987fcdeb-51a2-43d7-b890-123456789abc",
  folderId: campaigns.id,
});

// 7. Move an asset back to the project root
await client.assets.move({
  assetId: "987fcdeb-51a2-43d7-b890-123456789abc",
  folderId: null,
});

// 8. Move a folder under another parent or to root
await client.folders.move({
  folderId: campaigns.id,
  parentId: null, // Move to root
});

// 9. Delete an empty folder
await client.folders.delete({
  folderId: campaigns.id,
});
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.folders.create(name: "Marketing")
puts result
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 result = client.folders().create(CreateFolder { name: "Marketing".into(), ..Default::default() }).await?;
    println!("{result:?}");
    Ok(())
}
```

**java**

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

public class Example {
  public static void main(String[] args) {
    var client = new CalmLensClient(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID"));
    var result = client.folders.create(new CreateFolder().name("Marketing"));
    System.out.println(result);
  }
}
```

**curl**

```sh
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/folders" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "Marketing"
}'

# Use the returned id as parentId to create a child folder.
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/folders" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "Campaigns",
  "parentId": "your-parent-folder-id"
}'

curl "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/folders" \
  -H "X-Api-Key: $CALMLENS_API_KEY"

# Set FOLDER_ID to the child folder id.
curl "https://api.calmlens.com/v1/folders/$FOLDER_ID" \
  -H "X-Api-Key: $CALMLENS_API_KEY"

curl -X PATCH "https://api.calmlens.com/v1/folders/$FOLDER_ID" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "Summer Campaigns"
}'

curl -X POST "https://api.calmlens.com/v1/assets/$ASSET_ID/move" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "folderId": "your-child-folder-id"
}'

curl -X POST "https://api.calmlens.com/v1/assets/$ASSET_ID/move" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "folderId": null
}'

curl -X POST "https://api.calmlens.com/v1/folders/$FOLDER_ID/move" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "parentId": null
}'

curl -X DELETE "https://api.calmlens.com/v1/folders/$FOLDER_ID" \
  -H "X-Api-Key: $CALMLENS_API_KEY"
```

## Find assets

See [Pagination and filtering](/docs/guides/pagination-and-filtering) to query assets and [Members and permissions](/docs/guides/project-collaboration#folder-scoped-access) to restrict access by folder.
