> For the complete documentation index, see [llms.txt](https://docs.unitlab.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.unitlab.ai/api-sdk-cli/apis/assets-and-folders-api.md).

# Assets and folders API

Upload media files through the HTTP API, organize Assets into folders, and create grouped annotation units with saved layouts.

Upload reusable media into Assets, organize folders, and create Data Groups before attaching them to a project. Use the [Project data and Batch Queues API](/api-sdk-cli/apis/project-data-and-batch-queues-api.md) for uploads directly into a project, or the [Annotations and workflow API](/api-sdk-cli/apis/annotations-and-workflow-api.md) to import existing labels.

Send `Authorization: Api-Key $UNITLAB_API_KEY` on every request. `UNITLAB_API_URL` is your API base URL without a trailing slash. IDs in the examples must be replaced with IDs returned by your workspace.

## Before you start

Configure your [workspace API key](https://docs.unitlab.ai/api-sdk-cli/get-started/authentication-and-configuration). Run the examples from a working directory containing these four real images. Later, save the downloaded grouping configuration alongside `road-views`, not inside it.

```
working-directory/
├── road-views/
│   ├── drive001_front.jpg
│   ├── drive001_left.jpg
│   ├── drive002_front.jpg
│   └── drive002_left.jpg
└── grouping-config.json
```

Follow the upload and grouping steps in order. `FOLDER_ID` is the source folder's returned `pk`; `ASSET_ID` is an uploaded file's `asset.pk`; `GROUPED_FOLDER_ID` is the later grouping result's `grouped_folder_id`. `PROJECT_ID` identifies the project that should receive the grouped data. Do not use folder names in place of these IDs.

## Endpoints

| Method | Endpoint                                                           | Purpose                                                                   |
| ------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| GET    | `/api/sdk/data-assets/folders/`                                    | List root folders, children with `parent_id`, or all folders with `all=1` |
| POST   | `/api/sdk/data-assets/folders/`                                    | Create a local or cloud-backed folder                                     |
| GET    | `/api/sdk/data-assets/folders/{folder_id}/`                        | Retrieve one folder                                                       |
| GET    | `/api/sdk/data-assets/folders/{folder_id}/items/`                  | List Assets directly inside a folder                                      |
| POST   | `/api/sdk/data-assets/upload/`                                     | Upload one multipart media file                                           |
| GET    | `/api/sdk/data-assets/assets/{asset_id}/status/`                   | Check media processing                                                    |
| PATCH  | `/api/sdk/data-assets/assets/{asset_id}/custom-metadata/`          | Replace or clear custom metadata                                          |
| POST   | `/api/sdk/data-assets/folders/{folder_id}/sync-cloud/`             | Synchronize a connected folder                                            |
| GET    | `/api/sdk/data-assets/folders/{folder_id}/auto-grouping/suggest/`  | Suggest filename grouping                                                 |
| POST   | `/api/sdk/data-assets/folders/{folder_id}/auto-grouping/estimate/` | Validate a configuration and preview groups                               |
| POST   | `/api/sdk/data-assets/folders/{folder_id}/auto-groups/`            | Save the configuration and create grouped Assets                          |
| POST   | `/api/sdk/projects/{project_id}/attach-data/preview/`              | Preview attaching a source folder to a project                            |
| POST   | `/api/sdk/projects/{project_id}/attach-data/commit/`               | Attach the selected source to a project                                   |

Folder and item lists use `limit` and `offset` pagination. Follow `next` until it is null; the default limit is 50 and maximum is 300.

## Upload files and folders

{% stepper %}
{% step %}

### Create the destination folder

Omit `parent_id` for a root folder. To build a hierarchy, create each child with its parent's returned `pk`.

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/data-assets/folders/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"name":"Road views"}'
```

This creates a root folder. The response contains `pk`, `name`, `parent_id`, and `asset_count`. Save `pk` as the destination `FOLDER_ID`. For a child folder instead, include `"parent_id":"PARENT_FOLDER_ID"` in the JSON body, using the existing parent's ID.
{% endstep %}

{% step %}

### Send one media file per request

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/data-assets/upload/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -F "folder_id=FOLDER_ID" \
  -F "tags=incoming" \
  -F 'custom_metadata={"collection":"road-views"}' \
  -F "file=@./road-views/drive001_front.jpg"
```

Repeat this request for `drive001_left.jpg`, `drive002_front.jpg`, and `drive002_left.jpg`, changing only the local `file` path. The HTTP endpoint accepts one multipart `file`, not a local directory or a JSON list of file paths. A client uploading a directory must walk it locally and send individual files to the appropriate folder IDs. For a ready-made directory uploader, use [CLI Assets uploads](/api-sdk-cli/clis/cli-assets-and-folders.md).

If you have not created a folder, the first upload can send `folder_name` instead. Preserve the returned `folder_id` and use it for the remaining uploads. With an existing `folder_id`, omit `folder_name` to upload into that folder.

The optional `path` field is for derived paths under project-root folders. It does not recreate local hierarchy inside ordinary Assets folders; use `parent_id` and child folder IDs for that.
{% endstep %}

{% step %}

### Check the uploaded Asset

The upload response contains `asset`, `folder_id`, and `folder_name`. Save `asset.pk` as the Asset ID. Some media require processing after the upload response; check their status before using them.

```bash
curl --fail-with-body --silent --show-error \
  "$UNITLAB_API_URL/api/sdk/data-assets/assets/ASSET_ID/status/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY"
```

Inspect `status` and any returned error. For ordinary Assets, terminal statuses are `completed` and `failed`. An accepted upload is not proof that processing succeeded.

Before grouping this example, list `GET /api/sdk/data-assets/folders/FOLDER_ID/items/` and check that all four filenames are present. Check each returned Asset's processing status.

The multipart route has media-specific size limits. Large-media direct-upload workflows and LiDAR scene uploads use their dedicated routes; the [Python SDK Assets guide](/api-sdk-cli/sdks/assets-and-custom-metadata.md) covers the supported upload strategies.
{% endstep %}
{% endstepper %}

Custom metadata is an object stored with the Asset. Updating `custom-metadata/` replaces the entire object; send `{"custom_metadata": null}` to clear it.

## Create Data Groups with a saved layout

A grouping configuration is a JSON description of how **existing Assets** match tiles. It is not a file manifest and does not upload or download media. For example, `drive001_front.jpg` and `drive001_left.jpg` become two views in the same `Drive drive001` group.

Download the complete [two-camera grouping configuration](https://1625055228-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1fWt922WFnUJmXYAzZfh%2Fuploads%2FXpLBZf9rz40ixfdUGZoT%2Fgrouping-config.json?alt=media), then save it locally as `grouping-config.json`:

```json
{
  "grouping_keys": ["session_id"],
  "group_name_template": "Drive {session_id}",
  "tiles": [
    {
      "tile_id": "front",
      "title": "Front camera",
      "match_rule": "^(?P<session_id>drive\\d+)_front\\.jpg$",
      "exclusions": []
    },
    {
      "tile_id": "left",
      "title": "Left camera",
      "match_rule": "^(?P<session_id>drive\\d+)_left\\.jpg$",
      "exclusions": []
    }
  ],
  "minimum_matched_tiles": 2,
  "required_tiles": ["front", "left"],
  "incomplete_group_handling": "complete_only",
  "recursive": true,
  "layout_type": "free",
  "layout": {
    "version": "1.0",
    "type": "free",
    "panels": [
      {"id": "front", "title": "Front camera", "x": 0, "y": 0, "width": 50, "height": 100},
      {"id": "left", "title": "Left camera", "x": 50, "y": 0, "width": 50, "height": 100}
    ]
  }
}
```

Each tile's regular expression must extract every grouping key as a named capture, such as `(?P<session_id>...)`. Rules match relative paths or filenames. `recursive: true` includes descendant folders. `required_tiles` and `minimum_matched_tiles` control completeness; `complete_only` requires every configured tile, while `allow_incomplete` permits missing optional tiles.

The layout's panel IDs must correspond to tile IDs. `x`, `y`, `width`, and `height` are percentages of the layout canvas. To use a generated layout, omit `layout` and choose `layout_type: "grid"` or `"list"`. An explicit `free` layout preserves custom panel positions.

{% stepper %}
{% step %}

### Optionally request a suggestion

```bash
curl --fail-with-body --silent --show-error \
  "$UNITLAB_API_URL/api/sdk/data-assets/folders/FOLDER_ID/auto-grouping/suggest/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY"
```

Check `suggested` before using the returned configuration fields. A suggestion is not required if you already have an explicit configuration.
{% endstep %}

{% step %}

### Estimate before creating

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/data-assets/folders/FOLDER_ID/auto-grouping/estimate/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @grouping-config.json
```

The estimate does not create groups. Inspect `valid`, `errors`, `asset_count`, `estimated_valid_groups`, `examples`, `conflicts`, and `skipped`. For this four-image example, expect `asset_count: 4`, `estimated_valid_groups: 2`, and empty `conflicts` and `skipped` lists. A file matching multiple tiles, multiple files matching one tile, missing required tiles, or duplicate group names need attention. `valid: true` means the configuration is valid, not that every source file became a group.

Split the source into smaller folders if `large` is true: more than 5,000 source files or more than 1,000 valid groups cannot be created in one public API call. The create endpoint returns HTTP 400 for this case; there is no background grouping job to poll.
{% endstep %}

{% step %}

### Create the groups

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/data-assets/folders/FOLDER_ID/auto-groups/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @grouping-config.json
```

Read the response body even when the HTTP request succeeds. Check `valid` and `created_count`, then preserve `created_group_ids`, `grouped_folder_id`, and `grouped_folder_name`. A completed creation returns `mode: "sync"` along with the estimate details.

Creating groups saves the configuration and layout automatically. Each run creates a fresh sibling grouped folder referencing the source Assets; the original folder remains available. Repeating the request creates another grouping run, so inspect the result before retrying after a timeout. This operation does not copy existing project annotation history into the groups.

For this example, expect `created_count: 2`. The following shows the resulting Unitlab groups and their tile assignments, not new directories or copied image files on your computer:

```
Grouped result in Unitlab
├── Drive drive001
│   ├── Front camera → drive001_front.jpg
│   └── Left camera  → drive001_left.jpg
└── Drive drive002
    ├── Front camera → drive002_front.jpg
    └── Left camera  → drive002_left.jpg
```

{% endstep %}
{% endstepper %}

![Auto-Groups custom layout editor](https://292810646-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FGjVLUz4wthGkGlRKM6rM%2Fuploads%2F8eyEMKLe8RbL8q4uIsNW%2Fauto-groups-custom-layout.png?alt=media\&token=810552ad-bdf3-44b8-b959-3f3afe77847d)

*The same tile rules and saved layout organize the grouped annotation workspace. See* [*Grouped files and layouts*](https://docs.unitlab.ai/documentation/data/data-groups-and-layouts) *for the visual setup.*

## Attach the grouped folder to a project

Use the returned `grouped_folder_id`, not the source folder ID. Preview the attachment first:

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/projects/PROJECT_ID/attach-data/preview/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"folder_ids":["GROUPED_FOLDER_ID"]}'
```

Review the resolved counts, `items_preview`, and `requires_fps`. Then send the same selection to `attach-data/commit/`. If `requires_fps` is true, include a suitable numeric `fps` value in the commit body.

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/projects/PROJECT_ID/attach-data/commit/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"folder_ids":["GROUPED_FOLDER_ID"]}'
```

Attaching creates or reuses a published dataset version for the folder and attaches it to the project. Its Data Groups and layouts become project annotation units. Creating the groups in Assets alone does not attach them to a project.

## Cloud folders

To reference an existing cloud integration, create a folder with `name`, `cloud_storage_id`, and an optional `prefix`, then synchronize it with `sync-cloud/`. See the [Cloud storage API](/api-sdk-cli/apis/cloud-storage-api.md) and [S3 setup and permissions](https://docs.unitlab.ai/documentation/data/cloud-providers) before connecting customer storage.

## Request operating contract

| Concern           | Required behavior                                                                          |
| ----------------- | ------------------------------------------------------------------------------------------ |
| Execution surface | TLS-verified client using the public `/api/sdk/...` routes.                                |
| Identity          | Workspace API key; keep secrets outside code and logs.                                     |
| Request boundary  | URL-encoded stable IDs, explicit deadlines, structured errors, and complete pagination.    |
| Success evidence  | Response identifiers, read-after-write verification, and separate media processing status. |

After a timeout, inspect remote state before repeating a mutation.

***

> **Continue with Unitlab:** [training-data curation workflows](https://unitlab.ai/en/data-curation)
