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 provides - for example, 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.

HTTP API

The REST API exposes per-image operations for uploading images and annotations, fetching image details, deleting images, and managing image tags and metadata.

Upload an Image

Use the REST API to upload an image from a local file or URL.

When you pass image=, Roboflow fetches the URL from its own servers. If it cannot fetch the URL (bad URL, private address, or the host answers with a 4xx status), the response is a 400 with a message that starts with "Could not fetch image URL". If the host answers with a 5xx status or the connection fails, the response is a 502 with retryable: true, so you can send the same request again later.

Parameters

Querystring parameters accepted by the API:

api_key: Obtain from https://app.roboflow.com/account/api
image: [Optional] URL of the image to add. Use if your image is hosted elsewhere (Required when you don't POST a base64 encoded image in the request body).
name: [Optional] The filename of the image (if not set, we will try to infer it).
batch: [Optional] Group images under a batch with this name
tag: [Optional] Can be specified multiple times. Add tags to uploaded image.
split: [Optional] One of: train, valid, or test (defaults to train).
sequence_number: [Optional] If you want to keep the order of your images in the dataset, you can uploaded images increasing sequence numbers.
sequence_size: [Optional] The total number of images in the sequence. Defaults to 100,000 if not set.
inference_id: [Optional] The inference ID passed returned from a roboflow inference detection. This inference_id allows the image to be correlated with a roboflow detection in Model Monitoring (enterprise feature).

Linux or macOS

Uploading a local file called YOUR_IMAGE.jpg using multipart/form-data (recommended):

curl -F name=YOUR_IMAGE.jpg -F split=train \
-F file=@YOUR_IMAGE.jpg \
"https://api.roboflow.com/dataset/YOUR_DATASET_NAME/upload?\
api_key=$ROBOFLOW_API_KEY"

Alternatively, uploading a base64 encoded image:

base64 -i YOUR_IMAGE.jpg | curl -d @- \
"https://api.roboflow.com/dataset/your-dataset/upload?\
api_key=$ROBOFLOW_API_KEY&\
name=YOUR_IMAGE.jpg&\
split=train&\
batch=BATCH_NAME_FOR_UPLOAD"

Uploading an image hosted on the web via its URL (don't forget to URL encode it):

curl -X POST "https://api.roboflow.com/dataset/your-dataset/upload?\
api_key=$ROBOFLOW_API_KEY&\
image=https%3A%2F%2Fi.imgur.com%2FPEEvqPN.png&\
name=201-956-1246.png&\
split=train"

Windows

You will need to install curl for Windows and GNU's base64 tool for Windows. The easiest way to do this is to use the git for Windows installer which also includes the curl and base64 command line tools when you select "Use Git and optional Unix tools from the Command Prompt" during installation.

Then you can use the same commands as above.

Kotlin

Uploading with base64 encoded image:

import java.io.*
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import java.util.*

fun main() {
    // Get Image Path
    val filePath = System.getProperty("user.dir") + System.getProperty("file.separator") + "YOUR_IMAGE.jpg"
    val file = File(filePath)

    // Base 64 Encode
    val encodedFile: String
    val fileInputStreamReader = FileInputStream(file)
    val bytes = ByteArray(file.length().toInt())
    fileInputStreamReader.read(bytes)
    encodedFile = String(Base64.getEncoder().encode(bytes), StandardCharsets.US_ASCII)
    val API_KEY = "" // Your API Key
    val DATASET_NAME = "your-dataset" // Set Dataset Name (Found in Dataset URL)

    // Construct the URL
    val uploadURL = "https://api.roboflow.com/dataset/" +
            DATASET_NAME + "/upload" +
            "?api_key=" + API_KEY +
            "&name=YOUR_IMAGE.jpg" +
            "&split=train"

    // Http Request
    var connection: HttpURLConnection? = null
    try {
        // Configure connection to URL
        val url = URL(uploadURL)
        connection = url.openConnection() as HttpURLConnection
        connection.requestMethod = "POST"
        connection.setRequestProperty("Content-Type",
                "application/x-www-form-urlencoded")
        connection.setRequestProperty("Content-Length",
                Integer.toString(encodedFile.toByteArray().size))
        connection.setRequestProperty("Content-Language", "en-US")
        connection.useCaches = false
        connection.doOutput = true

        //Send request
        val wr = DataOutputStream(
                connection.outputStream)
        wr.writeBytes(encodedFile)
        wr.close()

        // Get Response
        val stream = connection.inputStream
        val reader = BufferedReader(InputStreamReader(stream))
        var line: String?
        while (reader.readLine().also { line = it } != null) {
            println(line)
        }
        reader.close()
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        connection?.disconnect()
    }
}
main()

Adding an Image Hosted Elsewhere via URL:

import java.io.BufferedReader
import java.io.DataOutputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

fun main() {
    val imageURL = "https://i.imgur.com/PEEvqPN.png" // Replace Image URL
    val API_KEY = "" // Your API Key
    val DATASET_NAME = "your-dataset" // Set Dataset Name (Found in Dataset URL)

    // Upload URL
    val uploadURL = ("https://api.roboflow.com/dataset/" + DATASET_NAME + "/upload" + "?api_key=" + API_KEY
            + "&name=YOUR_IMAGE.jpg" + "&split=train" + "&image="
            + URLEncoder.encode(imageURL, "utf-8"))

    // Http Request
    var connection: HttpURLConnection? = null
    try {
        // Configure connection to URL
        val url = URL(uploadURL)
        connection = url.openConnection() as HttpURLConnection
        connection.requestMethod = "POST"
        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
        connection.setRequestProperty("Content-Length", Integer.toString(uploadURL.toByteArray().size))
        connection.setRequestProperty("Content-Language", "en-US")
        connection.useCaches = false
        connection.doOutput = true

        // Send request
        val wr = DataOutputStream(connection.outputStream)
        wr.writeBytes(uploadURL)
        wr.close()

        // Get Response
        val stream = connection.inputStream
        val reader = BufferedReader(InputStreamReader(stream))
        var line: String?
        while (reader.readLine().also { line = it } != null) {
            println(line)
        }
        reader.close()
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        connection?.disconnect()
    }
}

main()

Android (Java)

Uploading with base64 encoded image:

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class UploadLocal {
    public static void main(String[] args) throws IOException {
        // Get Image Path
        String filePath = System.getProperty("user.dir") + System.getProperty("file.separator") + "YOUR_IMAGE.jpg";
        File file = new File(filePath);

        // Base 64 Encode
        String encodedFile;
        FileInputStream fileInputStreamReader = new FileInputStream(file);
        byte[] bytes = new byte[(int) file.length()];
        fileInputStreamReader.read(bytes);
        encodedFile = new String(Base64.getEncoder().encode(bytes), StandardCharsets.US_ASCII);

        String API_KEY = ""; // Your API Key
        String DATASET_NAME = "your-dataset"; // Set Dataset Name (Found in Dataset URL)

        // Construct the URL
        String uploadURL =
                "https://api.roboflow.com/dataset/"+
                        DATASET_NAME + "/upload" +
                        "?api_key=" + API_KEY +
                        "&name=YOUR_IMAGE.jpg" +
                        "&split=train";

        // Http Request
        HttpURLConnection connection = null;
        try {
            //Configure connection to URL
            URL url = new URL(uploadURL);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Type",
                    "application/x-www-form-urlencoded");

            connection.setRequestProperty("Content-Length",
                    Integer.toString(encodedFile.getBytes().length));
            connection.setRequestProperty("Content-Language", "en-US");
            connection.setUseCaches(false);
            connection.setDoOutput(true);

            //Send request
            DataOutputStream wr = new DataOutputStream(
                    connection.getOutputStream());
            wr.writeBytes(encodedFile);
            wr.close();

            // Get Response
            InputStream stream = connection.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
}

Adding an Image Hosted Elsewhere via URL:

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public class UploadHosted {
    public static void main(String[] args) {
        String imageURL = "https://i.imgur.com/PEEvqPN.png"; // Replace Image URL
        String API_KEY = ""; // Your API Key
        String DATASET_NAME = "your-dataset"; // Set Dataset Name (Found in Dataset URL)

        // Upload URL
        String uploadURL = "https://api.roboflow.com/dataset/" + DATASET_NAME + "/upload" + "?api_key=" + API_KEY
                + "&name=YOUR_IMAGE.jpg" + "&split=train" + "&image="
                + URLEncoder.encode(imageURL, StandardCharsets.UTF_8);

        // Http Request
        HttpURLConnection connection = null;
        try {
            // Configure connection to URL
            URL url = new URL(uploadURL);
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

            connection.setRequestProperty("Content-Length", Integer.toString(uploadURL.getBytes().length));
            connection.setRequestProperty("Content-Language", "en-US");
            connection.setUseCaches(false);
            connection.setDoOutput(true);

            // Send request
            DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
            wr.writeBytes(uploadURL);
            wr.close();

            // Get Response
            InputStream stream = connection.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
}

View Uploaded Images in Roboflow

Images uploaded via the API can be found in the Annotate tab, under the unassigned column and marked as uploaded via API.

If you specify a batch upload parameter, your image will still be found in the Annotate tab but instead of going to the uploaded via API batch it will be found in the batch you specified.

Upload an Annotation

If you already have existing annotations, along with your images, you can upload them to Roboflow.

This works with any of our supported annotation formats that use an annotation file that references the file name of the uploaded image.

Did you know? You can drag and drop (or select) the annotation files along with your images on the Upload page of the app without using the API.

Add prediction=true to save the annotation as a model prediction instead of ground truth. If the image is still in an upload batch and is not in an annotation job yet, Roboflow moves it into a Review job for that batch.

Add predictionRouting=unassigned to skip that move. The annotation still saves and the image is still marked as annotated, but it stays in its batch and stays unassigned. Use predictionRouting=review to ask for the default routing. Both values need prediction=true, and you cannot use them with jobName.

Example

Attaching a VOC XML annotation to an image with ID abc123 in the your-dataset dataset called YOUR_ANNOTATION.xml:

cat YOUR_ANNOTATION.xml | curl -d @- \
"https://api.roboflow.com/dataset/your-dataset/annotate/abc123?\
api_key=YOUR_KEY&\
name=YOUR_ANNOTATION.xml"

Attaching a Darknet TXT annotation to an image with ID abc123 in the your-dataset dataset called YOUR_ANNOTATION.txt using a json labelmap - in this case we need to send the contents of the annotation file in a json instead of just sending it as the body.

#!/bin/bash
# store the annotation as a json-compatible string
txt_content=$(cat YOUR_ANNOTATION.txt | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/\\n/' | tr -d '\n')
# create a json string with the annotation file and the label map [0=flower, 1=leaf]
json_payload="{ \"annotationFile\": \"$txt_content\", \"labelmap\":{\"0\":\"flower\", \"1\":\"leaf\"} }"

# upload the annotation + labelmap
echo $json_payload | curl -H "Content-Type: application/json" -d @- \
"https://api.roboflow.com/dataset/cultura-pepino-dark/annotate/abc123?\
api_key=YOUR_KEY&\
name=YOUR_ANNOTATION.txt"

Get Details About an Image

You can fetch details of a specific image using the REST API.

To fetch details of a specific image, make a GET request to the following API endpoint.

https://api.roboflow.com/:workspace/:project/images/:image_id

Here is an example request to the API to fetch the details of an image

curl -X GET "https://api.roboflow.com/my-workspace/my-project-name/images/image-id?api_key=$ROBOFLOW_API_KEY" \
-H 'Content-Type: application/json'

This endpoint returns a JSON object containing the following information about the image:

{
    "image":
        "id": string,
        "name": string,
        "annotation": {
            "key": string,
            "width": number,
            "height": number,
            "boxes": Array<{
                "label": string,
                "x": number,
                "y": number,
                "width": number,
                "height": number
            }>
        },
        "labels": string[],
        "split": string,
        "tags": string[],
        "created": number,
        "urls": {
            "original": string,
            "thumb": string,
            "annotation": string
        },
        "embedding": number[]
    }
}

Delete an Image from a Dataset

You can remove images from a Dataset using the REST API.

To remove images from a Dataset, make a DELETE request to the following API endpoint, passing the image IDs in the endpoint.

https://api.roboflow.com/:workspace/:project/images

Here is an example request to the API to remove images

curl "https://api.roboflow.com/my-workspace/my-project/images?api_key=$ROBOFLOW_API_KEY" \
  -X DELETE \
  -H "Content-Type: application/json" \
  -d '{"images": ["1", "2"]}'

This endpoint returns a 204 status if the operation was successful.

List, Add, and Remove Image Tags

You can assign tags to specific images on Roboflow using the REST API

To add, remove, and set tags to images hosted on Roboflow, make POST request to the following API endpoint. Use the Search API to retrieve the image ID associated with the image name:

https://api.roboflow.com/:workspace/:project/images/:image_id/tags

Here is an example request to the API (can "add", "remove", or "set" a tag):

curl -X POST "https://api.roboflow.com/my-workspace/my-project-name/images/image-id/tags?api_key=$ROBOFLOW_API_KEY" \
-H 'Content-Type: application/json' \
--data \
'{
    "operation": "add",
    "tags": "image_tag_test"
}'

This endpoint accepts the following values in the POST body:

{
     // // options are ["add", "remove", "set"]
     "operation": string,
     
     // array of strings of the tags
     "tags": string[],
     
}

The API will add the tag to the specified image in Roboflow (remember to pass in the image ID to the post request and not the image name).

Update Image Metadata and Tags

You can write custom metadata and tags to images in your workspace using the REST API. There are two endpoints: one for updating a single image synchronously, and one for updating up to 1,000 images in a batch.

Both endpoints require an API key with the image:tag scope.

Request Body

Both endpoints accept the same fields (the batch endpoint wraps them in an updates array):

- imageId (string) - required for batch update in body, inferred ffrom the path in single update
- metadata (object) - Key-value pairs to set on the image's user metadata.
- removeMetadata (string[]) - Metadata keys to delete from the image.
- addTags (string[]) - Tags to add to the image.
- removeTags (string[]) - Tags to remove from the image.

You must include at least one of these fields. You cannot set and remove the same metadata key or tag in the same request. Metadata keys that start with _rf_internal_ are reserved for Roboflow and are rejected.

Single Image

Update metadata and tags for a single image.

POST https://api.roboflow.com/:workspace/images/:image/metadata?api_key=YOUR_API_KEY
Example
curl -X POST "https://api.roboflow.com/my-workspace/images/abc123/metadata?api_key=$ROBOFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": { "camera": "front", "blur_score": 0.8 },
    "addTags": ["reviewed"]
  }'
Response
{
    "success": true
}

Batch Update

Update metadata and tags for multiple images asynchronously. Accepts up to 1,000 images per request.

POST https://api.roboflow.com/:workspace/images/metadata?api_key=YOUR_API_KEY
Example
curl -X POST "https://api.roboflow.com/my-workspace/images/metadata?api_key=$ROBOFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "updates": [
      {
        "imageId": "abc123",
        "metadata": { "camera": "front" },
        "addTags": ["reviewed"]
      },
      {
        "imageId": "def456",
        "removeTags": ["needs-review"]
      }
    ]
  }'
Response

Returns 202 with a task ID. Poll the task URL to check progress.

{
    "taskId": "task-id-here",
    "url": "https://api.roboflow.com/my-workspace/asynctasks/task-id-here"
}

See Async Tasks for how to poll the task status.

Errors

- 400 - Empty body, malformed field types, or conflicting fields (e.g. same key in both metadata and removeMetadata).
- 404 - Image not found in the workspace (single image endpoint only).

Python SDK

Project exposes the per-image operations that complement the bulk upload_dataset 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.

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

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:

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.

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.

Fetch an image's metadata

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):

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

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

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.

roboflow image upload photo.jpg -p PROJECT_ID

Options

FlagDescription
-p, --projectProject ID (required)
-a, --annotationPath to an annotation file
-m, --labelmapPath to a label map file
-s, --splitDataset split: train, valid, or test. The default is train.
-t, --tagComma-separated tag names
-M, --metadataMetadata as a JSON string
--is-predictionMark the upload as a prediction
-b, --batchBatch name

Examples

Upload an image with an annotation:

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

Upload an image with tags and metadata:

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.

MCP Server

Connect your AI agent to the MCP Server and it can find and update images with these tools:

ToolDescription
images_searchSearch for images inside a project.
images_update_metadataUpdate metadata and tags on a single image.
images_batch_update_metadataBatch-update metadata and tags on multiple images.
annotations_saveSave an annotation for an existing image.