Object Detection and Identification (ODI)

Learn how to add Object Detection and Identification (ODI) to your tasking workflow. ODI applies automated detection models to your SAR imagery and delivers structured results identifying objects of interest within the scene.

ODI was previously called Detect & Classify (D&C). The product, the detection models, and the delivery method stay the same. The derivative product type changes from DC-ATR to ODI, and the GeoJSON output uses a new schema. See Migrating from DC-ATR.

What is Object Detection and Identification?

When you task a satellite image through the ICEYE API, you receive SAR data products (GRD, SLC, etc.). ODI goes a step further: it runs detection models on the acquired imagery and returns GeoJSON files containing the locations and classifications of detected objects.

The legacy types DC_AIRCRAFT, DC_VESSELS, and DC_VEHICLES are no longer supported, and DC-ATR is deprecated — use ODI instead. See the deprecation notice for the legacy types and Migrating from DC-ATR for DC-ATR.

Available detection models

The following derivative product types are available:

Derivative product type Description Delivered file

ODI

Object Detection and Identification: detection and identification of objects of interest (aircraft, vessels, vehicles). Request this type going forward. Requires derivatives to be enabled on your contract. Contact your ICEYE representative for any changes needed.

{GRD_basename}_ODI.geojson

DC-ATR

Deprecated. Will be removed on January 4, 2027. Until then it is still accepted and delivers the previous output: a _DC.geojson file in the previous GeoJSON format. Use ODI instead.

{GRD_basename}_DC.geojson

The GeoJSON filename follows the ICEYE file naming convention of the source GRD or GRD-COG product, with the suffix above. For example, a source product ICEYE_SZFYKK_20261001T000000Z_1234567_X52_SLEDF_GRD.tif yields ICEYE_SZFYKK_20261001T000000Z_1234567_X52_SLEDF_GRD_ODI.geojson when you request ODI.

How it works

ODI is not a separate API. It extends the existing tasking workflow with two additions:

  1. You include a derivativeProductTypes parameter when creating a task

  2. After the task is DONE, you retrieve the detection results from a dedicated derivatives endpoint

The rest of the workflow (authentication, task creation, monitoring) stays the same.

Prerequisites

Before using ODI, make sure you have:

  • A contract with ODI enabled — derivative product types must be enabled in your contract. Contact your ICEYE representative.

  • Valid authentication credentials — see Authentication.

  • A supported imaging mode — ODI is available only for specific imaging modes. Your contract must also allow the required products for the selected mode.

Part 1: Creating a task with ODI

Creating an ODI task is identical to creating a regular task, with one addition: the derivativeProductTypes field in the request body.

The derivativeProductTypes parameter

This parameter accepts an array of derivative product type strings. Add it alongside your usual task parameters:

"derivativeProductTypes": ["ODI"]
ODI uses the GRD or GRD-COG product as input, so you must include it explicitly in the productTypes parameter. Having it as a default product on your contract is not sufficient.

For the full list of validation rules and error codes, see Create task — Derivative product types.

Full request example

  • cURL

  • Python

  • JavaScript

curl --location "${API_BASE_URL}/api/tasking/v2/tasks" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json, application/problem+json" \
  --header "Authorization: Bearer ${API_ACCESS_TOKEN}" \
  --data '{
    "acquisitionWindow": {
      "start": "2026-03-01T00:00:00Z",
      "end": "2026-03-02T00:00:00Z"
    },
    "contractID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "imagingMode": "SPOTLIGHT_EXTENDED_DWELL",
    "pointOfInterest": {
      "lat": 60.1699,
      "lon": 24.9384
    },
    "productTypes": ["GRD"],
    "derivativeProductTypes": ["ODI"]
  }'
import requests

task_payload = {
    "acquisitionWindow": {
        "start": "2026-03-01T00:00:00Z",
        "end": "2026-03-02T00:00:00Z"
    },
    "contractID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "imagingMode": "SPOTLIGHT_EXTENDED_DWELL",
    "pointOfInterest": {
        "lat": 60.1699,
        "lon": 24.9384
    },
    "productTypes": ["GRD"],
    "derivativeProductTypes": ["ODI"]
}

response = requests.post(
    f"{API_BASE_URL}/api/tasking/v2/tasks",
    headers={
        "Content-Type": "application/json",
        "Accept": "application/json, application/problem+json",
        "Authorization": f"Bearer {access_token}"
    },
    json=task_payload
)

task = response.json()
print(f"Task created: {task['id']}, status: {task['status']}")
const taskPayload = {
  acquisitionWindow: {
    start: "2026-03-01T00:00:00Z",
    end: "2026-03-02T00:00:00Z"
  },
  contractID: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  imagingMode: "SPOTLIGHT_EXTENDED_DWELL",
  pointOfInterest: {
    lat: 60.1699,
    lon: 24.9384
  },
  productTypes: ["GRD"],
  derivativeProductTypes: ["ODI"]
};

const response = await fetch(`${API_BASE_URL}/api/tasking/v2/tasks`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json, application/problem+json",
    "Authorization": `Bearer ${accessToken}`
  },
  body: JSON.stringify(taskPayload)
});

const task = await response.json();
console.log(`Task created: ${task.id}, status: ${task.status}`);

Response

On success, you receive a 201 status code. The response confirms which derivative product types were requested:

{
  "id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "pointOfInterest": {
    "lat": 60.1699,
    "lon": 24.9384
  },
  "acquisitionWindow": {
    "start": "2026-03-01T00:00:00Z",
    "end": "2026-03-02T00:00:00Z"
  },
  "createdAt": "2026-02-18T10:00:00Z",
  "updatedAt": "2026-02-18T10:00:00Z",
  "imagingMode": "SPOTLIGHT_EXTENDED_DWELL",
  "contractID": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "status": "RECEIVED",
  "exclusivity": "PRIVATE",
  "priority": "COMMERCIAL",
  "sla": "SLA_8H",
  "eula": "STANDARD",
  "incidenceAngle": {
    "min": 20,
    "max": 35
  },
  "lookSide": "ANY",
  "passDirection": "ANY",
  "productTypes": ["GRD"],
  "derivativeProductTypes": ["ODI"]
}

The derivativeProductTypes array in the response mirrors what you requested. If a derivative type is not supported by your contract, the API returns an error.

Part 2: Monitoring the task

ODI runs automatically in the background as part of task processing. Monitoring it is no different from any other task: you poll the task status, and you never poll the derivatives endpoint to check whether results are ready.

When the derivatives are ready depends on whether ODI is an SLA product under your contract. If it is, they are ready once the task reaches FULFILLED. If it is not, they are ready once the task reaches DONE. Because DONE never comes before FULFILLED, waiting for DONE guarantees your derivatives are ready either way — so unless you want to act on the earlier FULFILLED signal, simply wait for DONE.

Polling for status

  • cURL

  • Python

  • JavaScript

curl --location "${API_BASE_URL}/api/tasking/v2/tasks/${taskID}" \
  --header "Accept: application/json, application/problem+json" \
  --header "Authorization: Bearer ${API_ACCESS_TOKEN}"
import time

while True:
    response = requests.get(
        f"{API_BASE_URL}/api/tasking/v2/tasks/{task_id}",
        headers={"Authorization": f"Bearer {access_token}"}
    )
    task = response.json()
    print(f"Status: {task['status']}")

    # Waiting for DONE is always safe. SLA-backed ODI is ready earlier, at
    # FULFILLED -- add "FULFILLED" here only to act on that earlier signal.
    if task["status"] in ["DONE", "FAILED", "REJECTED", "CANCELED"]:
        break

    time.sleep(30)
async function waitForCompletion(taskId) {
  // Waiting for DONE is always safe. SLA-backed ODI is ready earlier, at
  // FULFILLED -- add "FULFILLED" here only to act on that earlier signal.
  const terminalStates = ["DONE", "FAILED", "REJECTED", "CANCELED"];

  while (true) {
    const response = await fetch(
      `${API_BASE_URL}/api/tasking/v2/tasks/${taskId}`,
      { headers: { "Authorization": `Bearer ${accessToken}` } }
    );
    const task = await response.json();
    console.log(`Status: ${task.status}`);

    if (terminalStates.includes(task.status)) return task;

    await new Promise(resolve => setTimeout(resolve, 30000));
  }
}

Once the task reaches DONE, all requested products have finished processing. At that point, move on to Part 3 to retrieve your results.

Part 3: Retrieving ODI results

Once your derivatives are ready (see Part 2 for the readiness rule), retrieve them from the derivatives endpoint with a single request — there is no need to poll.

Request

  • cURL

  • Python

  • JavaScript

curl --location "${API_BASE_URL}/api/tasking/v2/tasks/${taskID}/derivatives" \
  --header "Accept: application/json, application/problem+json" \
  --header "Authorization: Bearer ${API_ACCESS_TOKEN}"
response = requests.get(
    f"{API_BASE_URL}/api/tasking/v2/tasks/{task_id}/derivatives",
    headers={"Authorization": f"Bearer {access_token}"}
)

if response.status_code == 404:
    print("No derivatives available yet (task not DONE?)")
elif response.status_code == 200:
    print("All derivatives ready")
    derivatives = response.json()
    for derivative in derivatives["data"]:
        print(f"Type: {derivative['derivativeProductType']}")
        for asset in derivative.get("assets", []):
            print(f"  Download: {asset['href']}")
const response = await fetch(
  `${API_BASE_URL}/api/tasking/v2/tasks/${taskId}/derivatives`,
  { headers: { "Authorization": `Bearer ${accessToken}` } }
);

if (response.status === 404) {
  console.log("No derivatives available yet (task not DONE?)");
} else if (response.status === 200) {
  console.log("All derivatives ready");
  const derivatives = await response.json();
  for (const derivative of derivatives.data) {
    console.log(`Type: ${derivative.derivativeProductType}`);
    for (const asset of derivative.assets || []) {
      console.log(`  Download: ${asset.href}`);
    }
  }
}

Understanding the response status codes

A request made once your derivatives are ready returns 200 with your results:

200 (All derivatives ready)

All requested derivative products have been processed and are available for download.

404 (Not found)

No derivative products are available yet. You’ll see this if you query before your derivatives are ready — wait for the task to reach DONE (or FULFILLED for SLA-backed ODI) and request again.

For the full list of error responses, see Task derivatives.

Response body

The response contains a data array with one entry per derivative product. Each derivative entry contains:

  • id — unique identifier for the derivative processing request

  • imageReference — UUID reference to the source image

  • imageID — alternative string identifier for the source image (optional)

  • derivativeProductType — type of derivative product (ODI, DC-ATR if the task was requested with the deprecated type, or a historical DC_AIRCRAFT / DC_VESSELS / DC_VEHICLES value on stored records)

  • assets — array of downloadable files with signed URLs. The filename in href uses _ODI.geojson for ODI and _DC.geojson for DC-ATR.

{
  "data": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "imageReference": "c2ac555e-27ac-4c6a-bf87-717fa81539e3",
      "derivativeProductType": "ODI",
      "assets": [
        {
          "type": "geojson",
          "href": "https://XXXXXXXXXXXX.cloudfront.net/XXXX/ICEYE_SZFYKK_20261001T000000Z_1234567_X52_SLEDF_GRD_ODI.geojson?Expires=..."
        }
      ]
    }
  ]
}
Download URLs are signed and expire after 1 hour. Do not store them for long-term access, instead request a fresh URL from the API when you need to download again.

Part 4: Downloading ODI results

The assets array contains signed URLs pointing to GeoJSON files. Download them like any other file:

  • cURL

  • Python

  • JavaScript

curl --location "${DERIVATIVE_ASSET_URL}" \
  --output ICEYE_SZFYKK_20261001T000000Z_1234567_X52_SLEDF_GRD_ODI.geojson
for derivative in derivatives["data"]:
    for asset in derivative.get("assets", []):
        download_response = requests.get(asset["href"])
        filename = asset["href"].split("?")[0].rsplit("/", 1)[-1]
        with open(filename, "wb") as f:
            f.write(download_response.content)
        print(f"Downloaded {filename}")
for (const derivative of derivatives.data) {
  for (const asset of derivative.assets || []) {
    const downloadResponse = await fetch(asset.href);
    const content = await downloadResponse.text();
    const filename = asset.href.split("?")[0].split("/").pop();
    // Save or process the GeoJSON content
    console.log(`Downloaded ${filename}: ${content.length} bytes`);
  }
}

For the structure of the downloaded files, see Part 5.

Part 5: Reading the ODI GeoJSON output

Each ODI derivative is delivered as a GeoJSON FeatureCollection with one feature per detected object.

File name

The file name is the name of the source image followed by the _ODI.geojson suffix, for example:

ICEYE_SZFYKK_20261001T000000Z_1234567_X52_SLEDF_GRD_ODI.geojson

Schema version

Each file declares the GeoJSON schema version at the top level with "$schema": "v6". If the detection model returns no result for a scene, the file is an empty FeatureCollection ({"type":"FeatureCollection","features":[]}) without $schema or file-level properties. The file-level properties also include model_version, the version of the detection model that produced the file (for example v2.0.3).

Detection properties

Each detection (features[].properties) contains the following fields, among others:

Field Description

object_name

The most specific class the model resolved for the object.

model_confidence

Confidence score of the detection. If you filter detections by a confidence threshold, apply it to this field.

class_id

Taxonomy ID (UUID) of the most specific level the model resolved.

Hierarchical classification

Every detection carries its label at each level of the classification hierarchy, from the broadest to the most specific. The model fills a level only when its prediction reaches that depth; the deeper levels are null.

Field Description Example values

detect

Domain of the object

air, land

classify

Broad object category

large fixed wing aircraft, helicopter, large vehicle

recognise

Object type within the category

fighter aircraft, Il-76 variant

identify

Specific model or class

Condor, Candid/Midas

detect_id, classify_id, recognise_id, identify_id

Taxonomy ID (UUID) of each level. The field is left out when its level is null.

7c0ff6e7-1720-5b15-b43c-0595087fd7d6

Examples

The following excerpts of features[].properties show two detections. The first resolves to the identify level:

{
  "object_name": "Candid/Midas",
  "model_confidence": 0.75,
  "detect": "air",
  "classify": "large fixed wing aircraft",
  "recognise": "Il-76 variant",
  "identify": "Candid/Midas"
}

The second stops at the classify level, so recognise and identify are null:

{
  "object_name": "helicopter",
  "model_confidence": 0.98,
  "detect": "air",
  "classify": "helicopter",
  "recognise": null,
  "identify": null
}

The full GeoJSON schema and the list of supported classes are shared on an individual basis. Reach out to your ICEYE representative for details.

Migrating from DC-ATR

DC-ATR is deprecated and will be removed on January 4, 2027. The new hierarchical fields are additional information; the following steps are all that is needed to keep consuming the output as before:

  1. Order the new product type — request ODI instead of DC-ATR in derivativeProductTypes.

  2. Update file name matching — if you find or parse delivered files by name, look for the _ODI.geojson suffix instead of _DC.geojson.

  3. Read the class from object_name where you read class_name. The values are the same.

  4. Read the score from model_confidence where you read confidence or normalised_score. If you filter detections by a confidence threshold, apply it to model_confidence.

  5. Read the version from model_version in the file-level properties where you read taxonomy_version.

  6. Allow the new fields in strict schemas — if you validate files against a fixed schema, allow "$schema": "v6" and the new fields detect, classify, recognise, identify, detect_id, classify_id, recognise_id, and identify_id.

Item Previous New

Derivative product type

DC-ATR

ODI

GeoJSON file name suffix

_DC.geojson

_ODI.geojson

Class name (features[].properties)

class_name

object_name

Score (features[].properties)

confidence, normalised_score

model_confidence

Version (file-level properties)

taxonomy_version

model_version

Complete workflow summary

Here is the end-to-end ODI workflow:

1. Create task (with derivativeProductTypes and GRD or GRD-COG in productTypes)
       |
       v
2. Poll task status (GET /tasks/{id}) until DONE
       |
       v
3. Download SAR products (GET /tasks/{id}/products)
       |
       v
4. Retrieve derivatives (GET /tasks/{id}/derivatives)
       |
       '-- 200: results ready (task is DONE), returns GeoJSON assets
              |
              v
       Download GeoJSON files from asset URLs
API call Frequency Trigger Stop condition

POST Create Task

Once

User requests ODI

After task created

GET Task Status

Periodic

After task created

Status is DONE

GET Task Products

Once

Status is DONE

After download

GET Task Derivatives

Once

Status is DONE

After download

You never poll the derivatives endpoint — you wait on the task status, then retrieve derivatives with a single request. See Part 2 for the readiness rule.