> 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/annotations-and-workflow-api.md).

# Annotations and workflow API

Import annotation JSON over HTTP, resolve media and class mappings, commit labels, and manage review through workflow tasks.

{% hint style="info" %}
**Outcome:** Operate annotation work through stages and tasks with ownership, actions, overrides, and history.
{% endhint %}

Use annotation imports to preview model predictions, then commit them as editable labels. Use Workflow tasks for assignment, review, and approval. Importing labels does not approve them.

{% hint style="warning" %}
**Server availability:** Annotation Imports requires a Unitlab server version that implements the endpoints below. The Python client is available in `unitlab==3.1.3`, but installing the client does not enable the server feature. Confirm availability for your workspace before integrating; contact Unitlab if the import action or endpoints are unavailable.
{% endhint %}

## Import model pre-labels

For image predictions, COCO JSON supports bounding boxes, polygon segmentations, and instance masks encoded as compressed or uncompressed COCO RLE. Whole-image single-select or multi-select classes use `unitlab-image/1`, not COCO categories. YOLO TXT and ZIP label imports are not supported. See [Annotation imports](/api-sdk-cli/sdks/annotation-imports.md) for all formats, downloadable examples, mappings, and reimport behavior.

The base URL is `https://api.unitlab.ai`. Authenticate with `Authorization: Api-Key YOUR_KEY`. A user-created API key is required to commit so changes have an attributable author.

| Method | Endpoint                                                                   | Purpose                                                                                      |
| ------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| GET    | `/api/sdk/projects/{project_id}/annotation-imports/sources/`               | List prediction sources for repeat imports                                                   |
| GET    | `/api/sdk/projects/{project_id}/annotation-imports/media/?search=filename` | Find up to 50 media candidates for explicit mapping                                          |
| POST   | `/api/sdk/projects/{project_id}/annotation-imports/`                       | Upload a local label file or read a JSON key from a saved cloud connection; return a preview |
| POST   | `/api/sdk/projects/{project_id}/annotation-imports/{batch_id}/preview/`    | Recompute the preview with replacement mapping options                                       |
| POST   | `/api/sdk/projects/{project_id}/annotation-imports/{batch_id}/commit/`     | Commit the inspected preview using its token                                                 |

### Create-request fields

Send a local file with `multipart/form-data`, or use `application/json` to read labels from a saved cloud connection. Each request creates one import batch for one annotation document.

| Field                | Type                         | When to use it                                                                                                                  |
| -------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `annotation_file`    | File                         | Local UTF-8 `.json` or supported `.geojson` annotation document. Use this or the cloud-storage pair below.                      |
| `cloud_storage_id`   | UUID                         | Existing workspace connection, paired with `annotation_key`.                                                                    |
| `annotation_key`     | String                       | JSON object key relative to that connection's configured prefix.                                                                |
| `source_name`        | String, up to 200 characters | Name the prediction source when creating or deliberately reusing a named source.                                                |
| `source_id`          | UUID                         | Reuse a source returned by the sources endpoint. Supply exactly one of `source_name` or `source_id`.                            |
| `frame_mapping_file` | File                         | Local `unitlab-frame-map/1` JSON for COCO video frames.                                                                         |
| `frame_mapping_key`  | String                       | Frame-map key in the selected cloud connection, instead of a local map file.                                                    |
| `track_profile`      | String                       | `none` by default; use `attributes.track_id` when the COCO video annotations carry track IDs in that field.                     |
| `options`            | Object                       | Explicit media, class, object, and review mappings plus `stable_ids`. In multipart requests, send this object as a JSON string. |

COCO labels can contain image boxes, polygon segmentation, RLE instance masks, and supported keypoints. Native `unitlab-image/1` also supports whole-image classifications. Other accepted inputs include native `unitlab-video/1`, UUEF item JSON, Unitlab Audio JSON, and Unitlab GeoJSON. See the [format structures and examples](/api-sdk-cli/sdks/annotation-imports.md) before preparing the request. A general JSON list of filenames or URLs is not an annotation document.

The parser accepts files up to 80 MiB, but a deployment's HTTP upload limit can be lower. Split large inputs into valid smaller documents or use a connected-cloud JSON input when appropriate. Media must already belong to the target Project. Follow [Project data and Batch Queues API](/api-sdk-cli/apis/project-data-and-batch-queues-api.md) for uploads and readiness checks.

## Walkthrough: import one COCO prediction

### 1. Prepare the media and request values

Use a 1280 × 720 image named `road.jpg` and the [COCO bounding-box fixture](https://docs.unitlab.ai/api-sdk-cli/sdks/annotation-imports#download-example-annotation-files), saved as `coco-labels.json`:

```
road-scenes/
├── images/
│   └── road.jpg
└── labels/
    └── coco-labels.json
```

Run the following requests from `road-scenes/`, with `UNITLAB_API_KEY` already configured. Set the API base URL for this session:

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

Replace `PROJECT_ID` with the target project's UUID. First upload or link `road.jpg` into that project and wait for processing. The fixture's `images[0].file_name` is `road.jpg`; match it to the actual stored media reference, not to the label file's local directory. The importer does not download `coco_url`, `flickr_url`, or `source_url`.

### 2. Create a preview

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/projects/PROJECT_ID/annotation-imports/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -F "annotation_file=@./labels/coco-labels.json;type=application/json" \
  -F "source_name=detector-v1" \
  -F 'options={}'
```

The response includes `batch_id`, `source_id`, and `token`, plus the preview report. Inspect media matches, class mappings, conflicts, and blocked entries. Reuse the returned `source_id` instead of a new source name for predictions from the same model.

A successful create returns HTTP `201`. The preview contains:

| Response field                    | Meaning                                                               |
| --------------------------------- | --------------------------------------------------------------------- |
| `batch_id`, `source_id`           | Import-batch and prediction-source identifiers to retain.             |
| `format`                          | The detected annotation format.                                       |
| `media`, `classes`, `annotations` | Media matches, class decisions, and proposed annotation changes.      |
| `counts`                          | Counts by the annotation statuses actually present in this preview.   |
| `issues`                          | Unsupported or invalid entries requiring attention.                   |
| `token`                           | Token for this exact preview and the media/ontology state it checked. |

Preview creation stores import metadata but does not save annotation labels. Review the returned rows rather than treating HTTP success as approval of every prediction.

### 3. Inspect matches and refresh if needed

For the sample, check that the preview matches `road.jpg`, resolves **Car** as a box class, proposes the pixel box `[100, 200, 300, 180]`, and contains no issues or blocked rows. Keep the returned `batch_id`, `source_id` and `token`.

Use the media lookup endpoint to find candidate target IDs. It returns an object containing a `media` array of up to 50 matching candidates, each with `id`, `name`, and `family`. Narrow the search when a filename is ambiguous. The sources endpoint returns `sources`, with each source's `id` and `name`.

For a COCO file using `road.jpg` and category ID `1` as a box class, an explicit mapping can look like this:

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/projects/PROJECT_ID/annotation-imports/BATCH_ID/preview/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "media_mapping":{"road.jpg":"PROJECT_MEDIA_ID"},
    "class_mapping":{"1:bbox":{"target_id":"PROJECT_CLASS_ID"}},
    "stable_ids":false
  }'
```

Use the exact preview keys and actual target UUIDs. A native image document normally uses `media` as its media key. The preview request body is the options object itself, without an `options` wrapper, and it replaces the batch's prior options. Retain every mapping you still need. The response returns the refreshed preview and a new token.

Additional mappings are `object_mapping` from preview annotation-row key to an existing object ID, and `review_mapping` from preview media key to a Review-stage UUID when protected work needs to return to review. A class mapping can use `{"new_name":"Vehicle"}` instead of `target_id`. New classes cannot be added to a shared ontology.

Keep `stable_ids` false unless the model exporter preserves object identities across prediction runs. For video tracking, keep related frames together and provide the explicit COCO frame map; frame-like filenames alone do not identify a source video's frames.

### 4. Commit the inspected preview

Replace `BATCH_ID` and `PREVIEW_TOKEN` with the response values. Submit only after inspecting the preview.

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/projects/PROJECT_ID/annotation-imports/BATCH_ID/commit/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"token":"PREVIEW_TOKEN","decisions":{},"allow_partial":false}'
```

A stale token rejects the commit instead of silently overwriting changed work. Refresh the preview and inspect it again. Conflicts and human deletions default to keeping human work. Explicitly choose a partial commit only when you intend to leave blocked records behind.

The commit body requires `token`. Optional `decisions` maps preview annotation-row keys to `keep`, `incoming`, or `skip`; `allow_partial` defaults to false. The response includes the applicable `created`, `updated`, `kept`, `unchanged`, and `skipped` counts, plus `media_changed`, `classes_added`, `issues`, and `skipped_rows`. Inspect the actual returned counts and rows; a key can be absent when that category did not occur.

### 5. Check the saved result

On a fresh project image, this fixture creates one **Car** box. A repeat import can report kept, unchanged or updated labels instead. Inspect the commit report, open `road.jpg` in Unitlab, and check the label against the image.

The commit saves labels as draft work. An incoming `approved` field does not grant review approval. Follow the configured Annotate and Review stages below. Keep the batch ID, source ID and report with your import record.

## Read label JSON from S3

The same endpoint can read a label file through a saved cloud connection:

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/projects/PROJECT_ID/annotation-imports/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "cloud_storage_id":"STORAGE_ID",
    "annotation_key":"labels/predictions.json",
    "source_name":"detector-v1",
    "options":{}
  }'
```

Replace ID placeholders with actual UUIDs. The key is relative to the connection's configured prefix, not an `s3://` URL. The file can describe images already linked from S3. A COCO video input can also provide `frame_mapping_key` and `track_profile`. See [Cloud storage API](/api-sdk-cli/apis/cloud-storage-api.md) to register the media first.

### Readiness and retries

| Situation                               | What to do                                                                                                                                                        |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Media is still processing               | Wait for its Batch Queue, then refresh and inspect the label preview. The public HTTP API does not expose the frontend's deferred label-import scheduling action. |
| HTTP `400` validation failure           | Correct the format, mappings, or blocked records. Do not retry an unchanged request.                                                                              |
| HTTP `403` on commit                    | Check workspace access and use a user-created API key with an attributable author.                                                                                |
| HTTP `409` stale preview                | Refresh, inspect the changed preview, and commit with its new token.                                                                                              |
| A commit response was lost              | Retry the same batch's commit with the same decisions. A successfully committed batch returns its saved report.                                                   |
| A committed batch needs new predictions | Create a new batch under the existing prediction source; a committed batch cannot be refreshed.                                                                   |

The ordinary annotation-import path works on eligible ungrouped project items. [Data Groups and layouts](/api-sdk-cli/apis/assets-and-folders-api.md) are a separate data-organization flow, and grouped items are not label-import targets.

For the equivalent user interfaces, see [frontend label imports](https://docs.unitlab.ai/documentation/data/import-local-annotations) and [Python SDK label imports](/api-sdk-cli/sdks/annotation-imports.md).

## Review and workflow automation

### Stage and task discovery

| Method | Endpoint                                                           | Purpose                      |
| ------ | ------------------------------------------------------------------ | ---------------------------- |
| GET    | `/api/sdk/projects/{project_id}/workflow/stages/`                  | List stages and task counts  |
| GET    | `/api/sdk/projects/{project_id}/workflow/stages/{stage_id}/tasks/` | List stage tasks             |
| GET    | `/api/sdk/workflow-tasks/{task_id}/`                               | Retrieve complete task state |
| GET    | `/api/sdk/workflow-tasks/{task_id}/timeline/`                      | Retrieve task history        |

```bash
curl --fail-with-body --silent --show-error \
  "$UNITLAB_API_URL/api/sdk/projects/PROJECT_ID/workflow/stages/STAGE_ID/tasks/?include_unavailable=0" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY"
```

### Ownership and actions

| Method | Endpoint                                      | Body                                                     |
| ------ | --------------------------------------------- | -------------------------------------------------------- |
| POST   | `/api/sdk/workflow-tasks/{task_id}/claim/`    | Empty                                                    |
| POST   | `/api/sdk/workflow-tasks/{task_id}/assign/`   | `{"user_id":"USER_ID"}`                                  |
| POST   | `/api/sdk/workflow-tasks/{task_id}/release/`  | Empty                                                    |
| POST   | `/api/sdk/workflow-tasks/{task_id}/priority/` | `{"priority":10}`                                        |
| POST   | `/api/sdk/workflow-tasks/{task_id}/actions/`  | Action, idempotency key, expected stage, reason, comment |
| POST   | `/api/sdk/workflow-tasks/{task_id}/move/`     | Destination stage, reason, idempotency key               |

```bash
curl --fail-with-body --silent --show-error \
  -X POST "$UNITLAB_API_URL/api/sdk/workflow-tasks/TASK_ID/actions/" \
  -H "Authorization: Api-Key $UNITLAB_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "action":"approve",
    "idempotency_key":"YOUR_UUID",
    "expected_stage_id":"REVIEW_STAGE_ID",
    "rejection_reason":"",
    "comment":"QA policy checks passed"
  }'
```

Actions can include `complete`, `approve`, `reject`, and `skip`, only when the current Workflow exposes them. The expected stage protects against stale clients.

### Bulk manager operations

| Method | Endpoint                                                | Purpose                        |
| ------ | ------------------------------------------------------- | ------------------------------ |
| POST   | `/api/sdk/projects/{project_id}/workflow/tasks/assign/` | Assign explicit task IDs       |
| POST   | `/api/sdk/projects/{project_id}/workflow/tasks/move/`   | Dry-run or move explicit tasks |

Use `dry_run:true` before a bulk move. Preserve the reason and compare moved, skipped, and failed counts before treating the action as successful.

### Agent stage

An [Agent stage](https://docs.unitlab.ai/documentation/workflows/agent-stage) sends each task to a service you host by webhook. The service reads and saves the task's annotations with these routes, then completes the run. See [Pattern: Workflow worker](/api-sdk-cli/sdks/pattern-workflow-worker.md) for the webhook and a Python example, and [Annotation JSON reference](/api-sdk-cli/apis/annotation-json-reference.md) for the document shapes. Reading works on a task in any stage; saving and sending a result work only while the task is in an Agent stage. Any workspace API key can read; saving, upload links and results need a key created by a member who has the **View labeling interface** and **Create labels** permissions.

| Method | Endpoint                                                             | Purpose                                                                   |
| ------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| GET    | `/api/sdk/workflow-tasks/{task_id}/annotations/?only_keyframes=true` | Read every item of the task (one per Data Group panel) as a UUEF document |
| POST   | `/api/sdk/workflow-tasks/{task_id}/annotations/uploads/`             | Get an upload link for a document over 64 MiB                             |
| PUT    | `/api/sdk/workflow-tasks/{task_id}/annotations/`                     | Replace one item's annotations with a document                            |
| POST   | `/api/sdk/workflow-tasks/{task_id}/agent-result/`                    | Complete or fail the Agent run                                            |

**Read.** Each entry in `items` has `datasource_id`, `generic_type`, `revision` (the version read), `source_url` (a link to the original file, valid for 24 hours), `format`, and `document`: the item's annotations as one UUEF document, the shape a UUEF release exports. A large document comes as `document_url`, a download link, instead of `document`. With `only_keyframes=true` (the default), video and medical tracks list their keyframes only; `false` lists every frame. Objects hidden in the editor are included with `"visible": false`.

**Save.** Send `{"datasource_id", "format", "document"}`, or `{"datasource_id", "format", "document_upload_id"}` for an uploaded document. `format` is `uuef` (the default), `geojson` ([GeoJSON (geospatial models)](/api-sdk-cli/apis/annotation-json-reference.md#geojson-for-geospatial-models) naming project classes, for geospatial and pathology items) or `model` ([Prediction format (image models)](/api-sdk-cli/apis/annotation-json-reference.md#prediction-format-image-models), for image items, described in [Bring your own Models](https://docs.unitlab.ai/documentation/auto-labeling/bring-your-own-models#representative-image-request)).

* The document replaces the item's annotations completely; objects missing from it are deleted. There is no revision check, so the newest save wins.
* In tracks, only frames with `"keyframe": true` are saved, so a document read with either `only_keyframes` setting saves back unchanged.
* A document that carries tags (`data_tags` in UUEF, a root `tags` list in GeoJSON or the prediction format; an empty list clears them) replaces the item's tags; without that key they are kept. GeoJSON and prediction-format saves keep the item's item properties, which those formats cannot carry.
* Classes and properties are matched by their value in the project.
* These return `400`, and nothing is saved: a task that has left the Agent stage (a manager moved it out), an invalid document, an unknown class or property value, a format the item's data type does not take, a document read from another item, NaN or Infinity (not valid JSON), a NUL character in a text value, a document nested too deeply, and a request body over 64 MiB.
* Every object in the saved version is credited to the member who created the API key.
* The response is `{task_id, datasource_id, revision, history_id, created, updated, deleted}`, with object counts compared to the version it replaced.

**Results.** `{"run_id", "status": "complete"}` moves the task along the Agent stage's route. `{"run_id", "status": "failed", "error"}` keeps it in the stage, with `error` (up to 4,000 characters) shown in the stage queue. Sending the same result again is safe; a different result for a finished run, a result for an older run or for a run a manager stopped, and a result for a task that has left the Agent stage return `400`. A manager's **Retry**, or moving the task back into an Agent stage, starts a new run with a new `run_id`.

### Inspect completed work and approval evidence

`GET /api/sdk/projects/{project_id}/data-units/?status=complete&batch_queue=BATCH_QUEUE_ID` lists currently completed work units. Follow all pagination links. `approved` and `reviewed` are not supported aliases for this filter.

Complete is a current workflow state. It is not proof that a reviewer approved the item: a workflow without a Review stage or a manager override can also reach Complete. Inspect each task's timeline for the required review decisions and ensure no later change invalidates them. Task metadata and timelines do not return annotation geometry; read one task's annotations with the Agent stage read above, or retrieve label payloads through [Releases API](/api-sdk-cli/apis/releases-api.md).

{% hint style="warning" %}
Release creation does not have an approved-only filter. Checking completed tasks does not apply a filter to the subsequent release; `"completed_only": true` limits a release to items whose current status is Complete, which is not approval evidence. See [Pre-labels, review, and export](https://docs.unitlab.ai/api-sdk-cli/sdks/prelabels-review-and-export) for a controlled project or Batch Queue workflow.
{% endhint %}

***

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