> For the complete documentation index, see [llms.txt](https://docs.roboflow.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.roboflow.com/datasets/manage/manage-images.md).

# Manage Images

Upload, annotate, inspect, tag, and delete individual images through the REST API and Python SDK.

## About

Roboflow lets you upload, annotate, inspect, tag, and delete individual images in a project through the REST API and the Python SDK. Use these operations when you need finer control than the bulk dataset upload flow gives you (ex: uploading one image at a time from a stream, attaching annotations after the fact, or updating image metadata and tags). To bulk-import a whole labeled dataset at once, see [Upload a Dataset](/datasets/create-and-upload/upload-a-dataset.md).

## HTTP API

### Upload an Image

Send the image in one of three ways:

* A multipart file upload. This is the most efficient choice for local files.
* A base64 string as the whole request body. Use it when your client cannot build a multipart request.
* The `image` query parameter with a public URL. Roboflow fetches the image from its own servers, so nothing is sent in the body.

Each upload returns an image ID. Use it to upload an annotation for the image. Uploaded images appear on the Annotate page of the project, in the Unassigned column, in the batch you named or in "Uploaded via API".

{% openapi src="/files/s1lcjY8hbVRPyxw8CtS5" path="/dataset/{project}/upload" method="post" %}
[manage-images.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-40887ddc9b00931b5146ba136d7197ffc85e9e18%2Fmanage-images.yaml?alt=media)
{% endopenapi %}

### Upload an Annotation

Attach an annotation file to an image you already uploaded. This works with any [supported annotation format](https://roboflow.com/formats) whose file references the image's file name. You can also drag annotation files in with your images on the Upload page of the app.

By default the annotation adds the image to the Dataset, which takes it out of the batch, job, and review flow. Send `addToDataset=false` to keep the image in that flow.

For an Action Recognition Project, send one [`roboflow-video-coco` document](/datasets/create-and-upload/adding-data.md#import-video-segment-annotations) with the video's segments, and use the video ID in place of the image ID.

{% openapi src="/files/s1lcjY8hbVRPyxw8CtS5" path="/dataset/{project}/annotate/{imageId}" method="post" %}
[manage-images.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-40887ddc9b00931b5146ba136d7197ffc85e9e18%2Fmanage-images.yaml?alt=media)
{% endopenapi %}

### Get Details About an Image

Read an image's file name, split, tags, metadata, annotation, and image URLs. For a video, the response holds its poster image, a short-lived download link, and its [action segments](/datasets/annotate/annotate/annotate-video-segments.md).

{% openapi src="/files/s1lcjY8hbVRPyxw8CtS5" path="/{workspace}/{project}/images/{imageId}" method="get" %}
[manage-images.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-40887ddc9b00931b5146ba136d7197ffc85e9e18%2Fmanage-images.yaml?alt=media)
{% endopenapi %}

### Save an Annotation with a Version Check

Replace an image's annotation with JSON shapes without overwriting someone else's edit. First, get the image with `include=annotation`. Then send its `annotationGroupVersion` as `expectedGroupVersion`, even when the image has no saved annotation. If the annotation changed since you read it, the save fails with a 409 status. Each save returns the new version to use next time.

{% openapi src="/files/s1lcjY8hbVRPyxw8CtS5" path="/{workspace}/{project}/images/{imageId}/annotation" method="put" %}
[manage-images.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-40887ddc9b00931b5146ba136d7197ffc85e9e18%2Fmanage-images.yaml?alt=media)
{% endopenapi %}

### Delete an Image from a Dataset

Remove images from a project by ID. An image that no other project uses is deleted for good.

{% openapi src="/files/s1lcjY8hbVRPyxw8CtS5" path="/{workspace}/{project}/images" method="delete" %}
[manage-images.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-40887ddc9b00931b5146ba136d7197ffc85e9e18%2Fmanage-images.yaml?alt=media)
{% endopenapi %}

### Add and Remove Image Tags

Add, remove, or replace the tags on one image in a project. You need the image ID, not the file name: [search the project](/datasets/manage/manage-datasets/dataset-search.md#search-images-in-a-project) to find it.

{% openapi src="/files/s1lcjY8hbVRPyxw8CtS5" path="/{workspace}/{project}/images/{imageId}/tags" method="post" %}
[manage-images.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-40887ddc9b00931b5146ba136d7197ffc85e9e18%2Fmanage-images.yaml?alt=media)
{% endopenapi %}

### Update Image Metadata and Tags

Write [custom metadata](/datasets/create-and-upload/adding-data/image-metadata.md) and tags to images anywhere in your workspace. Use the single-image endpoint for one image. It applies the change before it responds.

{% openapi src="/files/s1lcjY8hbVRPyxw8CtS5" path="/{workspace}/images/{imageId}/metadata" method="post" %}
[manage-images.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-40887ddc9b00931b5146ba136d7197ffc85e9e18%2Fmanage-images.yaml?alt=media)
{% endopenapi %}

To change up to 1,000 images in one request, use the batch endpoint. It runs in the background and returns a task URL. Poll it with the [Async Tasks API](https://docs.roboflow.com/reference/platform/rest-api/async-tasks) until the task finishes. Its result counts the images that succeeded and lists the ones that failed.

{% openapi src="/files/s1lcjY8hbVRPyxw8CtS5" path="/{workspace}/images/metadata" method="post" %}
[manage-images.yaml](https://2252499186-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FNVfRxNsvCh4GOyqrjmzX%2Fuploads%2Fgit-blob-40887ddc9b00931b5146ba136d7197ffc85e9e18%2Fmanage-images.yaml?alt=media)
{% endopenapi %}

## Python SDK

`Project` exposes the per-image operations that complement the bulk [`upload_dataset`](/datasets/create-and-upload/upload-a-dataset.md#python-sdk) flow. Use these when you need finer control over single-image uploads, want to attach annotations after the fact, or are ingesting images one-at-a-time from a stream.

### Upload an image (with optional annotation)

`Project.upload()` is the high-level "do the right thing" helper. It accepts a single image plus an optional matching annotation file and ships both to the project in one call.

```python
import roboflow

rf = roboflow.Roboflow(api_key="YOUR_API_KEY")
project = rf.workspace().project("my-detector")

result = project.upload(
    image_path="./photo.jpg",
    annotation_path="./photo.xml",   # optional; matched VOC / COCO / etc. annotation
    split="train",                    # train | valid | test
    batch_name="ingest-2026-05",     # optional; groups uploads in the web UI
    tag_names=["camera-A", "indoor"], # optional; apply tags
    is_prediction=False,              # set True for model-generated annotations awaiting review
    num_retry_uploads=2,              # retries on transient upload failures
)
print(result)
```

`single_upload()` is a lower-level variant that takes the same arguments and returns the raw API responses for both the image and (if provided) the annotation.

### Upload an image only

```python
project.upload_image(
    image_path="./photo.jpg",
    split="train",
    batch_name="ingest-2026-05",
    tag_names=["camera-A"],
)
```

Useful when annotations don't exist yet and the image goes straight to a labeler.

### Validate an image before uploading

`check_valid_image()` runs Roboflow's local size / format checks without hitting the API:

```python
if project.check_valid_image("./photo.jpg"):
    project.upload_image("./photo.jpg")
```

### Attach an annotation to an existing image

`save_annotation()` posts an annotation against an image that's already in the project. Useful for adding labels created elsewhere, or for promoting a model prediction to ground truth.

```python
project.save_annotation(
    image_id="<image-id>",
    annotation_path="./photo.xml",
    is_prediction=False,
    annotation_overwrite=True,    # replace any existing annotation
)
```

Pass `annotation_labelmap="./labelmap.yaml"` to map class indices into class names if your annotation format requires it.

Saving with `is_prediction=True` can move the image into a review job. See [where predictions land](/datasets/create-and-upload/upload-a-dataset.md#where-predictions-land).

### Fetch an image's metadata

```python
info = project.image("<image-id>")
print(info["name"], info["split"], info["annotations"])
```

Returns image metadata, current split, and annotation status.

### Delete images

Project-level (only deletes images that belong to this project):

```python
project.delete_images(["<image-id-1>", "<image-id-2>"])
```

Workspace-level (removes images regardless of which projects reference them - use with care):

```python
workspace.delete_images(["<image-id-1>", "<image-id-2>"])
```

### A note on uploads in v1.3.6+

As of `roboflow` 1.3.6, the SDK uploads the original image bytes rather than re-encoding via Pillow. This restores parity with the web uploader and lets the Roboflow server deduplicate uploads by SHA-256. If you have automation that uploads the same image twice (e.g. to add it to multiple batches), you'll see the second upload succeed without consuming additional storage credits.

## CLI

Use the CLI to upload one image and its optional annotation, tags, or metadata.

```bash
roboflow image upload photo.jpg -p PROJECT_ID
```

### Options

<table data-search="false"><thead><tr><th>Flag</th><th>Description</th></tr></thead><tbody><tr><td><code>-p</code>, <code>--project</code></td><td>Project ID (required)</td></tr><tr><td><code>-a</code>, <code>--annotation</code></td><td>Path to an annotation file</td></tr><tr><td><code>-m</code>, <code>--labelmap</code></td><td>Path to a label map file</td></tr><tr><td><code>-s</code>, <code>--split</code></td><td>Dataset split: <code>train</code>, <code>valid</code>, or <code>test</code>. The default is <code>train</code>.</td></tr><tr><td><code>-t</code>, <code>--tag</code></td><td>Comma-separated tag names</td></tr><tr><td><code>-M</code>, <code>--metadata</code></td><td>Metadata as a JSON string</td></tr><tr><td><code>--is-prediction</code></td><td>Mark the upload as a prediction</td></tr><tr><td><code>-b</code>, <code>--batch</code></td><td>Batch name</td></tr></tbody></table>

### Examples

Upload an image with an annotation:

```bash
roboflow image upload photo.jpg -p my-project -a annotation.xml -s valid
```

Upload an image with tags and metadata:

```bash
roboflow image upload photo.jpg -p my-project -t "outdoor,daytime" -M '{"camera_id":"cam001"}'
```

To upload a directory or `.zip` archive, see [Upload a Dataset](/datasets/create-and-upload/upload-a-dataset.md).

## MCP Server

Connect your AI agent to the [MCP Server](https://docs.roboflow.com/agents/mcp-server) and it can find and update images with these tools:

<table data-search="false"><thead><tr><th width="290">Tool</th><th>Description</th></tr></thead><tbody><tr><td><code>images_search</code></td><td>Search for images inside a project.</td></tr><tr><td><code>images_update_metadata</code></td><td>Update metadata and tags on a single image.</td></tr><tr><td><code>images_batch_update_metadata</code></td><td>Batch-update metadata and tags on multiple images.</td></tr><tr><td><code>annotations_save</code></td><td>Save an annotation for an existing image.</td></tr></tbody></table>
