Build a tasking application

This guide teaches you how satellite tasking works and how to integrate ICEYE’s APIs into your application. We’ll use the ICEYE Tasking Demo as a concrete reference throughout.

What is satellite tasking?

ICEYE’s APIs support two main use cases:

  1. Catalog: Access existing satellite imagery that has already been captured

  2. Tasking: Request new satellite captures for specific locations and times

This guide focuses on tasking: how to request ICEYE’s satellite constellation to capture imagery for you.

Why tasking is powerful

  • On-demand imagery: Request captures of any location, anytime

  • Precise control: Specify exactly when and how you want the capture

  • Custom parameters: Control imaging angle, priority, delivery speed

  • Guaranteed coverage: Get fresh imagery of your specific area of interest

How tasking works

Tasking is fundamentally asynchronous, you’re requesting future satellite captures, not instant results.

The workflow:

ICEYE Satellite Tasking Workflow
  • Task creation: The user submits the imaging request specifying location, time window, and imaging parameters

  • ICEYE scheduling: ICEYE’s scheduler finds the optimal imaging opportunity based on constraints and satellite availability

  • Satellite imaging: Satellite captures your target and downlinks data to ground stations

  • Product delivery: Raw satellite data is processed into usable imagery products

How the demo handles the async nature:

The demo uses a 3-step interface that mirrors the API workflow:

Step 1: Contract Selection → User chooses which contract to use

Step 2: Task Creation → User specifies location, timing, and parameters

Step 3: Task Monitoring → The app tracks progress and displays products to the user when ready

This step-by-step approach makes the async process feel natural. Users understand they’re going through a workflow, not waiting for a single operation to complete.

Part 1: Authentication

OAuth2 token management

ICEYE uses OAuth2 Client Credentials flow. You exchange your credentials for an access token that’s valid for 60 minutes.

How to get an access token and cache it for usage

# backend/api/routes/auth.py

# Simple in-memory cache
_token_cache = {"token": None, "expires_at": None}

async def get_iceye_token():
    # Return cached token if still valid
    if _token_cache["token"] and datetime.now() < _token_cache["expires_at"]:
        return _token_cache["token"]

    # Get new token from ICEYE
    auth_url = f"{ICEYE_AUTH_URL}/v1/token"
    credentials = f"{client_id}:{client_secret}"
    base64_credentials = base64.b64encode(credentials.encode()).decode()

    response = await client.post(
        url=auth_url,
        headers={'Authorization': f'Basic {base64_credentials}'},
        data={'grant_type': 'client_credentials'}
    )

    # Cache with 5-minute safety margin
    access_token = response.json()['access_token']
    expires_in = response.json()['expires_in']
    _token_cache["token"] = access_token
    _token_cache["expires_at"] = datetime.now() + timedelta(seconds=expires_in - 300)

    return access_token
The token is temporary (expires in 60 minutes).

Part 2: Understanding Contracts

What is a Contract?

A contract defines your imaging capabilities with ICEYE. Think of it as your personalized menu it specifies:

  • Which imaging modes you can order

  • What priorities are available to you

  • What SLA options you have

Why contracts matter: Every task must be linked to a contract. The contract ensures you only order what you have access to.

Fetching your contracts

# backend/api/routes/contracts.py
@router.get("")
async def get_contracts():
    token = await get_iceye_token()

    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{os.getenv('ICEYE_API_URL')}/company/v1/contracts",
            headers={"Authorization": f"Bearer {token}"}
        )

    return response.json()

Response structure:

{
  "data": [
    {
      "id": "abababab-ddddd-eeeee-abcd-12312331",
      "name": "Demo contract",
      "imagingModes": {
        "allowed": ["SPOT", "SCAN", "STRIP"],
        "default": "SCAN"
      },
      "priority": {
        "allowed": ["COMMERCIAL"],
        "default": "COMMERCIAL"
      },
      "sla": {
        "allowed": ["SLA_24H", "SLA_48H", "SLA_72H"],
        "default": "SLA_24H"
      }
    }
  ]
}

Key structure:

  • allowed: Array of valid options for your contract

  • default: Default value for the field

The contract-driven UI pattern

Because contracts define what the user can or cannot use, you should: let contract data drive your UI.

Instead of hardcoding options:

// What if user's contract doesn't allow BACKGROUND?
<select>
  <option>COMMERCIAL</option>
  <option>BACKGROUND</option>
</select>

Use contract data:

// Only shows what user's contract allows
<select value={formData.priority}>
  {contract.priority?.allowed?.map(p => (
    <option key={p} value={p}>{p}</option>
  ))}
</select>

See it in frontend/src/components/TaskCreation.jsx.

When you populate dropdowns from contract.allowed arrays, users can never select invalid options.

Real-world example:

  • User A’s contract: priority.allowed = ["COMMERCIAL", "BACKGROUND"] → sees both

  • User B’s contract: priority.allowed = ["COMMERCIAL"] → sees only one

How the demo initializes form defaults:

const [formData, setFormData] = useState({
  imaging_mode: contract.imagingModes?.allowed?.[0] || '',
  priority: contract.priority?.default || contract.priority?.allowed?.[0] || 'COMMERCIAL',
  sla: contract.sla?.default || contract.sla?.allowed?.[0] || 'SLA_24H'
})

Part 3: Checking feasibility (optional)

Before creating a task, you can check if your request is feasible. This helps avoid rejected tasks and gives users confidence before committing.

When to use feasibility checks

  • To validate whether the ICEYE constellation can schedule your proposed task

  • To check if a request fits within your budget (when a pricing model is included in your contract)

How feasibility works

The feasibility endpoint accepts an array of the same parameters as task creation, but instead of creating a task, it returns whether ICEYE’s constellation can fulfill the request.

Backend implementation:

# backend/api/routes/tasks.py
@router.post("/feasibility")
async def check_feasibility(req: FeasibilityRequest):
    """
    Check if a tasking request is feasible before creating a task.
    Returns feasibility status and details about potential imaging opportunities.
    """
    token = await get_iceye_token()

    # Build the payload (same structure as task creation)
    payload = [{
        "contractID": req.contract_id,
        "imagingMode": req.imaging_mode,
        "pointOfInterest": {"lat": req.location.lat, "lon": req.location.lon},
        "acquisitionWindow": {"start": req.acquisition_window.start, "end": req.acquisition_window.end}
    }]

    # Add optional parameters if provided
    if req.incidence_angle:
        payload[0]["incidenceAngle"] = req.incidence_angle
    if req.look_side:
        payload[0]["lookSide"] = req.look_side

    async with httpx.AsyncClient(timeout=120.0) as client:
        response = await client.post(
            f"{ICEYE_API_URL}/tasking/v2/feasibility",
            headers={"Authorization": f"Bearer {token}"},
            json=payload
        )

    return response.json()
The feasibility API accepts an array of task requests, allowing you to check multiple tasks in a single call. The demo sends a single-item array for simplicity.

Frontend integration

The demo adds a "Check Feasibility" button alongside "Create Task":

// frontend/src/components/TaskCreation.jsx
const handleCheckFeasibility = async () => {
  setFeasibilityLoading(true)
  setFeasibilityResult(null)

  try {
    // Reuse the same buildTaskData() function used for task creation
    const taskData = buildTaskData()
    const result = await api.checkFeasibility(taskData)
    setFeasibilityResult(result)
  } catch (err) {
    setFeasibilityResult({ error: err.message })
  } finally {
    setFeasibilityLoading(false)
  }
}
Reuse your buildTaskData() function for both feasibility checks and task creation. This ensures consistency between what you check and what you submit.

Understanding the response

The response contains a feasibility array with results for each task request:

{
  "feasibility": [
    {
      "id": "0",
      "result": "FEASIBLE"
    }
  ]
}
The id field is an index (starting from 0) that matches each result to the corresponding task in your request array. This is useful when checking multiple tasks in a single call.

Possible result values:

  • FEASIBLE – The request can be scheduled

  • INFEASIBLE – The request cannot be scheduled with the given constraints

See the Check Feasibility API reference for complete parameter documentation.

Part 4: Creating tasks

Required parameters

Every task needs four essential pieces of information:

1. Which contract (contractID)

"contractID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

2. Where to image (pointOfInterest)

"pointOfInterest": {
  "lat": 60.1699,
  "lon": 24.9384
}

3. How to image (imagingMode)

"imagingMode": "SCAN"

Must be from your contract’s imagingModes.allowed list.

4. When to image (acquisitionWindow)

"acquisitionWindow": {
  "start": "2025-11-06T00:00:00Z",
  "end": "2025-11-08T00:00:00Z"
}

Must be within 14 days from now, minimum 24-hour window.

Optional parameters - fine-tuning your request

Incidence Angle: Satellite viewing angle

"incidenceAngle": {
  "min": 10,
  "max": 45
}
  • Narrower range = more specific requirements, may be harder to schedule

  • Wider range = gives ICEYE’s scheduler more flexibility

Look Side: Which side of the satellite

"lookSide": "ANY"  // "LEFT", "RIGHT", or "ANY"

Pass Direction: Orbit direction

"passDirection": "ANY"  // "ASCENDING" (south to north), "DESCENDING" (north to south), or "ANY"
Using "ANY" for look side and pass direction gives ICEYE’s scheduler maximum flexibility to find the optimal imaging opportunity. This can result in faster scheduling.

Priority: Task urgency

"priority": "COMMERCIAL"  // "COMMERCIAL" or "BACKGROUND"

SLA: Product delivery speed

"sla": "SLA_24H"  // "SLA_12H", "SLA_24H", "SLA_48H", "SLA_72H"

Defines how quickly you need products after capture completes.

How the demo creates tasks

The user inputs the data in the Task creation page. Then:

Frontend prepares the data:

// frontend/src/components/TaskCreation.jsx
const handleSubmit = async (e) => {
  e.preventDefault()

  const taskData = {
    contract_id: contract.id,
    location: {
      lat: parseFloat(formData.latitude),
      lon: parseFloat(formData.longitude)
    },
    imaging_mode: formData.imaging_mode,
    acquisition_window: {
      start: new Date(formData.start_date).toISOString(),
      end: new Date(formData.end_date).toISOString()
    },
    // Optional parameters - only include if specified
    incidence_angle: formData.incidence_angle_min && formData.incidence_angle_max ? {
      min: parseInt(formData.incidence_angle_min),
      max: parseInt(formData.incidence_angle_max)
    } : undefined,
    priority: formData.priority || undefined,
    sla: formData.sla || undefined
  }

  const task = await api.createTask(taskData)
  onTaskCreated(task)
}

Backend transforms and sends to ICEYE:

# backend/api/routes/tasks.py
def build_task_payload(task: TaskRequest) -> dict:
    payload = {
        "contractID": task.contract_id,
        "imagingMode": task.imaging_mode,
        "pointOfInterest": {
            "lat": task.location.lat,
            "lon": task.location.lon
        },
        "acquisitionWindow": {
            "start": task.acquisition_window.start,
            "end": task.acquisition_window.end
        }
    }

    # Only include optional parameters if provided
    if task.incidence_angle:
        payload["incidenceAngle"] = task.incidence_angle

    if task.priority:
        payload["priority"] = task.priority

    return payload
Only send optional parameters if the user explicitly set them. This lets ICEYE use optimal defaults for everything else.

Why this matters: ICEYE’s scheduler can optimize better when you give it flexibility. Don’t over-constrain your tasks unless you have specific requirements.

Part 5: Monitoring tasks

Understanding task status

When you create a task, it goes through several status changes:

ICEYE Satellite Tasking Statuses

Terminal states (task won’t progress further): DONE, REJECTED, CANCELED, FAILED

What each status means:

Status What It Means What’s Available

RECEIVED

ICEYE accepted your task

Task details only

ACTIVE

ICEYE scheduled the imaging opportunity

Task details + Scene (imaging plan)

FULFILLED

Imaging complete, SLA products ready

Task + Scene + SLA Products

DONE

All products ready

Task + Scene + All Products

REJECTED

ICEYE rejected the task

Task details

CANCELED

User canceled the task

Task details

FAILED

Imaging couldn’t be completed

Task details

The polling pattern

Since tasks are asynchronous, you need to poll for status updates.

Instead of polling you can use the Notification API that offers webhooks. To keep things simple, we use the poll mechanism in this demo application.

How the demo implements polling:

// frontend/src/components/TaskMonitoring.jsx
const refreshTask = useCallback(async () => {
  const updatedTask = await api.getTask(task.id)
  setTask(updatedTask)

  // Load scene data when status changes
  // Scene data evolves: ACTIVE = planned parameters, FULFILLED/DONE = actual capture data
  const shouldLoadScene =
    (updatedTask.status === 'ACTIVE' && lastSceneStatusRef.current !== 'ACTIVE') ||
    (updatedTask.status === 'FULFILLED' && lastSceneStatusRef.current !== 'FULFILLED') ||
    (updatedTask.status === 'DONE' && lastSceneStatusRef.current !== 'DONE')

  if (shouldLoadScene) {
    lastSceneStatusRef.current = updatedTask.status
    const sceneData = await api.getTaskScene(task.id)
    setScene(sceneData)
  }

  // Stop polling at terminal states
  const isTerminalState = ['DONE', 'FAILED', 'REJECTED', 'CANCELED'].includes(updatedTask.status)
  if (isTerminalState) {
    clearInterval(pollingIntervalRef.current)
  }
}, [task.id])

useEffect(() => {
  // Initial refresh
  refreshTask()

  // Poll every 5 seconds
  pollingIntervalRef.current = setInterval(refreshTask, 5000)

  // Cleanup when component unmounts
  return () => {
    if (pollingIntervalRef.current) clearInterval(pollingIntervalRef.current)
  }
}, [refreshTask])
Don’t call scene or products endpoints blindly. Check the task status first:
  • Scene: Only call when status is ACTIVE, FULFILLED, or DONE

  • Products: Only call when status is FULFILLED or DONE

Making progress visible

The demo uses a visual timeline to show progress:

// frontend/src/components/TaskTimeline.jsx
const stages = [
  { key: 'RECEIVED', label: 'Received' },
  { key: 'ACTIVE', label: 'Scheduled' },
  { key: 'FULFILLED', label: 'Captured' },
  { key: 'DONE', label: 'Complete' }
]

// Highlight completed stages
{stages.map((stage, index) => (
  <div className={`stage ${currentStageIndex >= index ? 'completed' : ''}`}>
    {stage.label}
  </div>
))}
Visual feedback is crucial for async processes. A timeline showing "Received → Scheduled → Captured → Complete" helps users understand where their task is in the process.

Part 6: Scene details

What is scene data?

When you create a task, you request parameters (e.g., "incidence angle 10-45°").

Scene data tells you ICEYE’s planned imaging parameters:

Before imaging (status ACTIVE): Scene shows the planned parameters: when and how ICEYE intends to capture. These can change until the image is captured as ICEYE scheduler constantly optimizes requests.

After imaging (status FULFILLED/DONE): Scene shows the actual parameters: what was used during capture

When scene becomes available

Scene data is available when task status is ACTIVE or later.

Request:

GET /tasking/v2/tasks/{task_id}/scene
Authorization: Bearer {token}

Response:

{
  "imagingTime": {
    "start": "2025-11-08T19:55:19Z",
    "end": "2025-11-08T19:55:34Z"
  },
  "duration": 15,
  "incidenceAngle": "37.38",
  "lookSide": "RIGHT",
  "passDirection": "ASCENDING"
}
Scene parameters may differ from your request because ICEYE’s scheduler optimizes within your specified constraints. Before imaging, scene shows the plan. After imaging, it shows what actually happened.

This tells users exactly when and how the satellite will capture their target.

Part 7: Accessing products

Understanding product delivery

Products are delivered in stages:

FULFILLED: SLA products are ready

  • Delivered within your requested SLA (e.g., 24 hours after capture)

  • Typically includes GRD (Ground Range Detected) format

  • Sufficient for most applications

DONE: All products are ready

  • May include additional formats (e.g., SLC - Single Look Complex)

  • Takes longer to process

  • Final delivery

Fetching products

Request:

GET /tasking/v2/tasks/{task_id}/products
Authorization: Bearer {token}

Response:

{
  "data": [
    {
      "id": "product-uuid",
      "productType": "GRD",
      "status": "available",
      "url": "https://download.iceye.com/products/...",
      "expiresAt": "2025-11-15T20:00:00Z",
      "size": 1234567890
    }
  ]
}
Product download URLs are signed and temporary (expire after 1 hour). Don’t store the URLs — store the product ID and fetch a fresh URL when needed.

Part 8: Canceling tasks

You can cancel tasks before imaging starts (status RECEIVED or ACTIVE).

Request:

PATCH /tasking/v2/tasks/{task_id}
Authorization: Bearer {token}
Content-Type: application/json

{
  "status": "CANCELED"
}

How the demo implements cancellation:

// frontend/src/components/TaskMonitoring.jsx
const handleCancel = async () => {
  if (window.confirm('Are you sure you want to cancel this task?')) {
    try {
      await api.cancelTask(task.id)
      // Status will update to CANCELED on next poll
    } catch (err) {
      setError(err.message)
    }
  }
}
Cancellation is just a status update. ICEYE handles the rest (stopping the scheduling, freeing resources, etc.). You can’t cancel after imaging starts. See the task cancellation documentation for information on fees, restrictions, and cancellation policy.

Part 9: Complete API call flow - when, what, and why

This section shows exactly when the demo calls each API, what triggers the call, and why it’s called at that moment.

API flow diagram

Click to view the complete API call flow diagram

Summary: API call rules

API Frequency Trigger Stop Condition

GET Contracts

Once

Component mount

After successful load

POST Feasibility

Optional

User clicks "Check Feasibility"

After result received

POST Task

Once

User clicks "Create"

After task created

GET Task Status

Every 5s

Polling loop

Terminal state reached

GET Scene

On status change

Status changes to ACTIVE, FULFILLED, or DONE

Reloads for each status to get updated parameters

GET Products

On status change

Status → FULFILLED or DONE

After load for each status

PATCH Cancel

Once

User clicks "Cancel"

After successful cancel

Real-world considerations

Beyond the demo

The demo is great for learning, but production applications need:

1. Persistent storage

  • Store tasks in a database

  • Users can return later to check status

  • Task history and filtering

2. Notifications

3. Better UX for long waits

  • "We’ll email you when ready"

  • Background processing

  • Task queue management

4. Production infrastructure

  • Rate limiting

  • Monitoring and logging

  • Error tracking

Next steps

Once you’ve mastered creating tasks, learn how to manage and monitor your existing tasks – listing, filtering, and navigating through your task history.

Try the demo

Find the demo code on GitHub and test it yourself!

Video walkthrough

Watch this demo of the application in action:

Explore the code