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 |
|---|---|---|
|
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. |
|
|
Deprecated. Will be removed on January 4, 2027. Until then it is still accepted and delivers the previous output: a |
|
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:
-
You include a
derivativeProductTypesparameter when creating a task -
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(orFULFILLEDfor 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-ATRif the task was requested with the deprecated type, or a historicalDC_AIRCRAFT/DC_VESSELS/DC_VEHICLESvalue on stored records) -
assets— array of downloadable files with signed URLs. The filename inhrefuses_ODI.geojsonforODIand_DC.geojsonforDC-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 |
|---|---|
|
The most specific class the model resolved for the object. |
|
Confidence score of the detection. If you filter detections by a confidence threshold, apply it to this field. |
|
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 |
|---|---|---|
|
Domain of the object |
|
|
Broad object category |
|
|
Object type within the category |
|
|
Specific model or class |
|
|
Taxonomy ID (UUID) of each level. The field is left out when its level is |
|
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:
-
Order the new product type — request
ODIinstead ofDC-ATRinderivativeProductTypes. -
Update file name matching — if you find or parse delivered files by name, look for the
_ODI.geojsonsuffix instead of_DC.geojson. -
Read the class from
object_namewhere you readclass_name. The values are the same. -
Read the score from
model_confidencewhere you readconfidenceornormalised_score. If you filter detections by a confidence threshold, apply it tomodel_confidence. -
Read the version from
model_versionin the file-level properties where you readtaxonomy_version. -
Allow the new fields in strict schemas — if you validate files against a fixed schema, allow
"$schema": "v6"and the new fieldsdetect,classify,recognise,identify,detect_id,classify_id,recognise_id, andidentify_id.
| Item | Previous | New |
|---|---|---|
Derivative product type |
|
|
GeoJSON file name suffix |
|
|
Class name ( |
|
|
Score ( |
|
|
Version (file-level properties) |
|
|
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.