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:
-
Catalog: Access existing satellite imagery that has already been captured
-
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:
-
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:
Terminal states (task won’t progress further): DONE, REJECTED, CANCELED, FAILED
What each status means:
| Status | What It Means | What’s Available |
|---|---|---|
|
ICEYE accepted your task |
Task details only |
|
ICEYE scheduled the imaging opportunity |
Task details + Scene (imaging plan) |
|
Imaging complete, SLA products ready |
Task + Scene + SLA Products |
|
All products ready |
Task + Scene + All Products |
|
ICEYE rejected the task |
Task details |
|
User canceled the task |
Task details |
|
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, orDONE -
Products: Only call when status is
FULFILLEDorDONE
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.
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
-
Use webhooks instead of polling
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!
Explore the code
-
See how token caching works (
backend/api/routes/auth.py) -
See contract-driven UI (
frontend/src/components/TaskCreation.jsx) -
See polling pattern (
frontend/src/components/TaskMonitoring.jsx) -
See visual timeline (
frontend/src/components/TaskTimeline.jsx)