> 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/project-data-and-batch-queues-api.md).

# Project data and Batch Queues API

Upload local files and folders through the HTTP API, monitor Batch Queues, and attach workspace data to projects.

Upload each media file with an authenticated HTTP request, collect the returned item IDs, and wait for processing before importing labels or starting annotation. A local folder is a client-side loop of file uploads using one shared Batch Queue ID.

For a ready-made uploader, use [CLI media uploads](/api-sdk-cli/clis/cli-projects-and-data-units.md) or [Python project uploads](/api-sdk-cli/sdks/project-uploads.md). For existing labels, use the separate [annotation import API](/api-sdk-cli/apis/annotations-and-workflow-api.md).

## Before you start

Use an existing project and a workspace API key. Complete [authentication](https://docs.unitlab.ai/api-sdk-cli/get-started/authentication-and-configuration) and load `UNITLAB_API_KEY` into your environment. The examples require Bash, curl, and Python 3.

Set the API origin and replace `YOUR_PROJECT_UUID` with your project's ID:

```bash
export UNITLAB_API_URL="https://api.unitlab.ai"
export PROJECT_ID="YOUR_PROJECT_UUID"
```

Prepare two valid images and run the commands from `road-scenes/`:

```
road-scenes/
└── images/
    ├── road.jpg
    └── crossing.jpg
```

Choose either the single-file example or the folder example below. Running both uploads `road.jpg` twice. Send `Authorization: Api-Key ...` on Unitlab API requests.

Read the project's upload configuration before sending files:

```bash
curl --fail-with-body --silent --show-error \
  "$UNITLAB_API_URL/api/sdk/projects/$PROJECT_ID/upload-info/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY"
```

| Response field          | Meaning                                                                                        |
| ----------------------- | ---------------------------------------------------------------------------------------------- |
| `accepted_formats`      | Extensions accepted by the ordinary media uploader.                                            |
| `max_file_sizes`        | Per-family limits for multipart form uploads, in bytes.                                        |
| `direct_max_file_sizes` | Per-family limits for direct multipart uploads where supported.                                |
| `max_file_size`         | Largest ordinary limit; do not apply this value to every family.                               |
| `pointcloud`            | Separate LiDAR capability and limits, including `enabled`, accepted formats, and maximum size. |

Use the limit for the file's family and upload transport. Format availability and limits depend on the deployment and workspace plan.

## Upload one media file

Send `multipart/form-data` to `POST /api/sdk/projects/{project_id}/upload-data/`. Let your HTTP client set the multipart boundary.

| Form field       | Required                        | Value                                                                                                                          |
| ---------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `file`           | Yes                             | One nonempty file.                                                                                                             |
| `session_id`     | Recommended; required for DICOM | A UUID shared by every file in the same upload batch. If omitted, the server creates a separate queue.                         |
| `fps`            | No                              | Video sampling rate, at least `0.25`; defaults to `1.0`.                                                                       |
| `generic_type`   | No                              | Explicit `geospatial`, `pathology`, `timeseries`, or `tabular` family. Other ordinary file types are detected from the upload. |
| `primary_column` | No                              | CSV row mode only: the column used as the main text field. Use with `generic_type=tabular`.                                    |
| `thumbnail`      | No                              | Optional image thumbnail.                                                                                                      |

```bash
SESSION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"

curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/projects/$PROJECT_ID/upload-data/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -F "session_id=$SESSION_ID" \
  -F "file=@./images/road.jpg"
```

For this single-image example, expect one new item. Save the returned `datasource_id` and `upload_session_id`; the latter should match your `SESSION_ID`. Depending on the family, the response can be `201 Created` or `202 Accepted`, and may also include `generic_type`, `upload_status`, or `task_id`.

For video, add `-F "fps=2.0"`. For CSV row mode, add `-F "generic_type=tabular"` and optionally `-F "primary_column=text"`. A CSV uploaded without an explicit family can require configuration in the platform before processing can finish.

## Upload a local folder or file list

The ordinary upload endpoint accepts one file per request. It does not accept a local directory path, a JSON list of local paths, or a JSON list of URLs as a media manifest. Read your folder or list locally and upload each selected file with the same `session_id`.

This Bash example uploads both images and recursively includes JPG, JPEG, and PNG files in any subfolders. It saves each response in `upload-responses/`:

```bash
set -euo pipefail
SESSION_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"
mkdir -p upload-responses

find ./images -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) -print0 |
  while IFS= read -r -d '' FILE; do
    curl --fail-with-body --silent --show-error \
      -X POST "$UNITLAB_API_URL/api/sdk/projects/$PROJECT_ID/upload-data/" \
      -H "Authorization: Api-Key $UNITLAB_API_KEY" \
      -F "session_id=$SESSION_ID" \
      -F "file=@$FILE" \
      --output "upload-responses/$(basename "$FILE").json"
  done

printf 'Batch Queue: %s\n' "$SESSION_ID"
```

With the example folder, expect `road.jpg.json` and `crossing.jpg.json` in `upload-responses/`, each containing its item ID. Keep these responses so you can associate local filenames with `datasource_id`. The shared UUID creates a Batch Queue, not a Data Group. Ordinary project uploads do not recreate your local subfolder hierarchy. Use unique basenames when you intend to match imported labels by filename, or match by the returned item IDs.

For grouped annotation views, upload to workspace Assets, create groups and layouts, and attach them to the project. See [Assets and folders API](/api-sdk-cli/apis/assets-and-folders-api.md) and [Grouped files and layouts](https://docs.unitlab.ai/documentation/data/data-groups-and-layouts). For connected cloud paths, use the [Cloud storage API](/api-sdk-cli/apis/cloud-storage-api.md).

## Monitor processing and inspect failures

Start polling after all files in your batch have been sent. Transfer success does not mean that video extraction, medical processing, or other preparation has completed.

```bash
curl --fail-with-body --silent --show-error \
  "$UNITLAB_API_URL/api/sdk/projects/$PROJECT_ID/upload-sessions/$SESSION_ID/status/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY"
```

For the two-image folder, a successful final response looks like this:

```json
{
  "status": "completed",
  "total": 2,
  "completed": 2,
  "processing": 0,
  "failed": 0
}
```

Poll with backoff and a deadline until `processing` is `0`. The final `status` is `completed`, `partial_failure`, or `failed`. Check `failed` even when processing has stopped. Counts represent server-side data items and can differ from local file counts, for example when DICOM slices become one volume or CSV rows become separate items.

Inspect the paginated item results:

```bash
curl --fail-with-body --silent --show-error \
  "$UNITLAB_API_URL/api/sdk/projects/$PROJECT_ID/upload-sessions/$SESSION_ID/data/?limit=100" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY"
```

Verify that the results contain `road.jpg` and `crossing.jpg`, with no failed item. Each result contains `datasource_id`, `file_name`, `generic_type`, and `upload_status`. Follow the response's `next` link to read all pages. Preserve these IDs for label imports. After a request timeout, inspect the queue and item list before retrying the upload to avoid duplicate items.

## Finalize medical form uploads

When sending DICOM files through `upload-data/`, reuse one `session_id` for the series and finalize once all files have transferred:

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/projects/$PROJECT_ID/medical-upload-sessions/$SESSION_ID/finalize/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY"
```

Finalization groups DICOM slices into volume items while their frame preparation completes. Then poll the Batch Queue. The SDK also finalizes medical batches sent through the ordinary form uploader; for NIfTI and NRRD files this grouping step is a no-op. Medical volumes uploaded through the direct multipart lifecycle below are already complete volumes and do not require this finalization call.

## Upload large media directly

Supported geospatial and pathology files, plus video, medical, audio, and PDF files, can use direct multipart upload. The endpoint retains the name `tiled-uploads`. Use it when required by the ordinary upload limit and only when the server supports it.

| Method | Endpoint                                                 | Purpose                                         |
| ------ | -------------------------------------------------------- | ----------------------------------------------- |
| POST   | `/api/sdk/projects/{project_id}/tiled-uploads/initiate/` | Reserve an upload and receive signed part URLs. |
| POST   | `/api/sdk/projects/{project_id}/tiled-uploads/refresh/`  | Renew the upload token and selected part URLs.  |
| POST   | `/api/sdk/projects/{project_id}/tiled-uploads/complete/` | Confirm uploaded parts and start processing.    |
| POST   | `/api/sdk/projects/{project_id}/tiled-uploads/abort/`    | Cancel an unfinished upload.                    |

1. Send JSON to `initiate/` with `file_name`, `file_size` in bytes, and the shared `session_id`. Optional `generic_type` disambiguates the supported family; optional `fps` controls video sampling.
2. Read `upload_token`, `datasource_id`, `part_size`, `part_urls`, and `expires_in`. Each part URL includes its `part_number`.
3. Split the file using the returned `part_size`, then `PUT` each raw chunk to its signed URL. Do not send the Unitlab API key to storage URLs. Save each response's `ETag`.
4. If needed, send `{"upload_token":"...","part_numbers":[1,2]}` to `refresh/` and use the refreshed token and URLs.
5. Send the token and every part's number and ETag to `complete/`:

```json
{
  "upload_token": "TOKEN_FROM_INITIATE_OR_REFRESH",
  "parts": [
    {"part_number": 1, "etag": "ETAG_FROM_PART_1"},
    {"part_number": 2, "etag": "ETAG_FROM_PART_2"}
  ]
}
```

Completion returns `202 Accepted`; poll the Batch Queue. To abandon an unfinished transfer, send `{"upload_token":"..."}` to `abort/`. The SDK handles splitting, retries, URL renewal, and completion for you.

LiDAR scenes have their own `pointcloud-uploads/` and `pointcloud-folder-uploads/` routes. Do not send a scene ZIP to the ordinary media endpoint or use a LiDAR token with `tiled-uploads/`. See [LiDAR data preparation](https://docs.unitlab.ai/documentation/annotations/lidar-annotation) and the [SDK project upload guide](/api-sdk-cli/sdks/project-uploads.md) for supported scene input.

## Data Unit and Batch Queue endpoints

| Method | Endpoint                                                                        | Purpose                                                |
| ------ | ------------------------------------------------------------------------------- | ------------------------------------------------------ |
| GET    | `/api/sdk/projects/{project_id}/data-units/`                                    | List loose and grouped work units.                     |
| GET    | `/api/sdk/projects/{project_id}/data-units/{unit_id}/`                          | Retrieve one Data Unit.                                |
| GET    | `/api/sdk/projects/{project_id}/upload-info/`                                   | Read accepted formats and upload limits.               |
| POST   | `/api/sdk/projects/{project_id}/upload-data/`                                   | Upload one multipart form file.                        |
| POST   | `/api/sdk/projects/{project_id}/medical-upload-sessions/{session_id}/finalize/` | Finalize medical files sent through the form uploader. |
| GET    | `/api/sdk/projects/{project_id}/upload-sessions/`                               | List Batch Queues.                                     |
| GET    | `/api/sdk/projects/{project_id}/upload-sessions/{session_id}/`                  | Read queue details and processing counts.              |
| GET    | `/api/sdk/projects/{project_id}/upload-sessions/{session_id}/status/`           | Read processing counts.                                |
| GET    | `/api/sdk/projects/{project_id}/upload-sessions/{session_id}/data/`             | Inspect item results.                                  |

Data Unit filters are `search`, `data_type`, `status`, `batch_queue`, and `kind`. Paginated lists accept `limit` and `offset`; follow `next` until it is empty.

```bash
curl --fail-with-body --silent --show-error \
  "$UNITLAB_API_URL/api/sdk/projects/$PROJECT_ID/data-units/?data_type=image&status=annotate&kind=group" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY"
```

## Attach governed workspace data

| Method | Endpoint                                                                      | Purpose                                     |
| ------ | ----------------------------------------------------------------------------- | ------------------------------------------- |
| POST   | `/api/sdk/projects/{project_id}/attach-data/preview/`                         | Validate selection without mutation         |
| POST   | `/api/sdk/projects/{project_id}/attach-data/commit/`                          | Attach reviewed folders or Dataset versions |
| GET    | `/api/sdk/projects/{project_id}/attached-sources/`                            | List source links                           |
| GET    | `/api/sdk/projects/{project_id}/attached-sources/{source_id}/detach-preview/` | Preview detach impact                       |
| POST   | `/api/sdk/projects/{project_id}/attached-sources/{source_id}/detach/`         | Detach one source link                      |

Replace `DATASET_ID` below with the dataset UUID and `3` with the published version you want to attach. The preview and commit must use the same body.

```bash
ATTACH_BODY='{"folder_ids":[],"dataset_ids":[],"dataset_versions":[{"dataset_id":"DATASET_ID","version_number":3}],"fps":2.0}'
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 "$ATTACH_BODY"
```

Commit the same body only after preview resolves the intended asset count. If commit returns a queue ID, poll until `processing` is zero, then inspect `failed` and queue data.

## Next steps

When processing succeeds, [import existing labels](/api-sdk-cli/apis/annotations-and-workflow-api.md) or open the project to annotate the images. Use [Assets and folders](/api-sdk-cli/apis/assets-and-folders-api.md) when you need reusable files or grouped views.

***

> **Related Unitlab capability guides:** [multimodal data curation](https://unitlab.ai/en/data-curation)
