Build a catalog application
This guide teaches you how the ICEYE Catalog works and how to integrate ICEYE’s Catalog APIs into your application. We’ll use the ICEYE Catalog Explorer as a concrete reference throughout.
What is the ICEYE Catalog?
ICEYE’s APIs support two main use cases:
-
Tasking: Request new satellite captures for specific locations and times
-
Catalog: Access existing satellite imagery, including the public catalog and your own private images
This guide focuses on catalog: how to browse, search, price, and purchase satellite imagery from ICEYE’s archive.
Why the Catalog is powerful
-
Instant access: Browse thousands of existing SAR images worldwide
-
Advanced search: Filter by location, date, imaging mode, resolution, and more
-
Transparent pricing: Check prices before committing to a purchase
-
Unified access: Both public catalog images and your private images (from tasking or previous purchases) are accessible through the same API
Key concepts
Before diving in, let’s clarify the terminology:
STAC items
The Catalog follows the SpatioTemporal Asset Catalog (STAC) specification. Every entry in the catalog is a STAC item, a GeoJSON Feature containing:
-
Geometry: The footprint polygon of the image on Earth
-
Properties: Metadata (acquisition mode, resolution, date, orbit, etc.)
-
Assets: Links to thumbnails and downloadable products
Frames
A frame is the basic unit you purchase. For most imaging modes (Spot, Dwell), one image = one frame. For Strip and Scan modes, a long acquisition is split into multiple frames.
When you purchase a frame, you get all products for that frame (e.g., both GRD and SLC formats, except for Scan mode which only produces GRD).
Collections
The two main collections are:
| Collection | Description |
|---|---|
|
The entire ICEYE public catalog. Anyone can browse it. Items include only low-resolution assets (thumbnails). |
|
Images you already own (from tasking or catalog purchases). When queried alone, the response includes all products including high-resolution download links. |
The catalog workflow
-
Authenticate: Get an OAuth2 access token
-
Browse / Search: Find images using filters (location, date, imaging mode)
-
Check price (optional): Get the cost for a specific frame under your contract
-
Purchase: Buy the frame
-
Track delivery: Poll purchase status until products are ready
-
Download: Access the full-resolution imagery products
How the demo handles this flow:
The demo uses a 2-tab interface:
Explore Catalog → Search, browse results on a map, view details, check price, and purchase
Purchase History → Track all purchases, view products, and access download links
Part 1: Authentication
All ICEYE API calls require an OAuth2 access token. See Authentication for how to obtain one, and Manage Access Tokens for production caching patterns.
The demo implements token caching with automatic refresh in backend/api/routes/auth.py. Every route in the demo calls get_iceye_token() before making API requests.
Part 2: Understanding Contracts
What is a Contract?
A contract defines your commercial relationship with ICEYE. Think of it as your account configuration. It specifies:
-
Which catalog collections you can access
-
Your pricing model for catalog purchases
-
The billing account for purchases
Why contracts matter for catalog:
-
The
contractIDis required when filtering by collection (public/private) -
Pricing depends on your contract: the same frame can have different prices under different contracts
-
Every purchase must be linked to a contract
Fetching your contracts
GET /company/v1/contracts
Authorization: Bearer {token}
Response:
{
"data": [
{
"id": "abababab-ddddd-eeeee-abcd-12312331",
"name": "Demo contract",
"catalog_collections": {
"allowed": ["public"],
"default": "public"
}
}
]
}
Display the contract name in your UI instead of the raw UUID. This makes the interface much more user-friendly.
|
Part 3: Browsing and searching the catalog
The Catalog API provides two ways to find images:
| Endpoint | Method | Best for |
|---|---|---|
|
|
Simple browsing with query parameters, and cursor-based pagination |
|
|
Advanced search with STAC query extension, GeoJSON intersects, complex filters |
Search with POST
The POST /catalog/v2/search endpoint supports richer filtering (see the demo’s catalog.py for a full implementation).
POST /catalog/v2/search
Authorization: Bearer {token}
Content-Type: application/json
Request body:
{
"bbox": [5.1, 44.2, 5.8, 44.9],
"datetime": "2023-01-01T00:00:00Z/2024-12-31T23:59:59Z",
"collections": ["public"],
"contractID": "your-contract-id",
"limit": 10
}
Key parameters:
| Parameter | Description |
|---|---|
|
Bounding box |
|
Date-time range in ISO 8601 format: |
|
Array: |
|
Required when |
|
Maximum results per page (default: 10) |
|
STAC query extension for metadata filtering (e.g., filter by imaging mode) |
|
Array of sort conditions (e.g., |
When you specify collections, you must also provide contractID. If you omit both, the search defaults to the public catalog.
|
Response structure:
{
"data": [
{
"id": "ICEYE_ARCHIVE_00000",
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[5.3, 44.5], [5.6, 44.5], [5.6, 44.7], [5.3, 44.7], [5.3, 44.5]]]
},
"properties": {
"frame_id": "12345_1",
"start_datetime": "2024-06-15T08:30:00Z",
"sar:product_type": "GRD",
"iceye:processing_mode": "spotlight"
},
"assets": {
"thumbnail-png": { "href": "https://..." }
}
}
],
"cursor": "eyJhbGciOiJ..."
}
Each item in data is a STAC item (GeoJSON Feature). The cursor field is present when more pages are available.
Pagination
| For general pagination patterns and best practices, see Handle Pagination. This section covers a catalog-specific behavior: the initial search uses POST, but subsequent pages use GET. |
Catalog search uses cursor-based pagination with a twist:
-
Search with
POST /catalog/v2/search, which returns results and acursor -
Get next pages with
GET /catalog/v2/items?cursor=…(not POST) -
Repeat until no more
cursorin the response
// frontend/src/components/CatalogSearch.jsx
// Initial search (POST)
const data = await api.searchItems(body)
setResults(data.data || [])
setCursor(data.cursor || null)
// Load more (uses GET /items with cursor, not POST)
async function handleLoadMore() {
const params = { cursor, limit }
if (isPrivate) {
params.contractID = contractId
}
const data = await api.listItems(params)
setResults(prev => [...prev, ...(data.data || [])])
setCursor(data.cursor || null)
}
When paginating private collection results, pass the same contractID. The cursor is only valid for the same query parameters used when it was generated.
|
Displaying results on a map
Each STAC item includes a GeoJSON geometry (the image footprint) and a bounding box, making it natural to display results on a map:
// frontend/src/components/MapView.jsx
// Convert items to a GeoJSON FeatureCollection
const geojsonData = {
type: 'FeatureCollection',
features: items.map(item => ({
type: 'Feature',
geometry: item.geometry,
properties: {
id: item.id,
frame_id: item.properties?.frame_id,
product_type: item.properties?.['sar:product_type'],
processing_mode: item.properties?.['iceye:processing_mode'],
start_datetime: item.properties?.start_datetime,
}
}))
}
// Render with react-leaflet's GeoJSON component
<GeoJSON data={geojsonData} onEachFeature={onEachFeature} style={styleFeature} />
| The demo uses Shift+drag to draw a bounding box on the map. Other approaches include click-to-place polygon vertices, geocoding search, or manual coordinate input. |
Thumbnails
Each STAC item includes thumbnail assets that you can display directly:
const thumbUrl = item.assets?.['thumbnail-png']?.href
|| item.assets?.['thumbnail']?.href
|| null
Thumbnail URLs are pre-signed and can be loaded directly from the frontend without additional authentication. They do expire, so avoid caching them long-term.
Part 4: Checking prices
Before purchasing, you can check the price of a frame. Pricing depends on your contract: the same frame may have different prices under different contracts.
Get frame price
GET /catalog/v2/price?contractID={contractID}&frameID={frameID}&eula=STANDARD
Authorization: Bearer {token}
Response:
{
"amount": 1000,
"currency": "USD"
}
The amount is in the currency’s minor unit (e.g., cents for USD). For example, an amount of 1000 with currency: "USD" means $10.00. Your application must convert to major units for display.
|
Handling pricing errors
Not all contracts have a pricing plan configured. The demo handles this gracefully:
// frontend/src/components/ItemDetail.jsx
async function handleGetPrice() {
try {
const data = await api.getFramePrice(contractId, frameId)
setPrice(data)
} catch (err) {
const msg = err.message || ''
if (
msg.includes('no Pricing Plan') ||
msg.includes('not allowed to query price') ||
msg.includes('OUT_OF_BOUND_CONTRACT') ||
msg.includes('Invalid Contract')
) {
setPrice({ unavailable: true })
} else {
setError(msg)
}
}
}
| If your contract doesn’t have a pricing plan, contact your ICEYE representative. You can still purchase; the price check is optional. |
Part 5: Purchasing frames
What you get when you purchase
It’s important to understand: you purchase frames, not individual STAC items.
When you purchase a frame:
-
You get all products associated with that frame (e.g., both GRD and SLC formats)
-
For multi-frame acquisitions (Strip, Scan), you must purchase each frame separately
-
The purchased images appear in your
privatecollection
Making a purchase
See the demo’s purchases.py for a full implementation.
POST /catalog/v2/purchases
Authorization: Bearer {token}
Content-Type: application/json
Request body:
{
"contractID": "your-contract-id",
"frameID": "12345_1",
"eula": "STANDARD"
}
contractID and frameID are required. eula is optional (defaults to STANDARD).
Response:
{
"id": "purchase-uuid",
"frameID": "12345_1",
"contractID": "your-contract-id",
"createdAt": "2025-01-15T10:30:00Z",
"status": "active",
"eula": "STANDARD"
}
Part 6: Tracking purchases
Understanding purchase status
After purchasing, the order goes through processing:
| Status | Description |
|---|---|
|
Order received, processing not started |
|
Order is being processed |
|
Order complete, products are available for download |
|
Order was canceled |
|
Order could not be fulfilled |
Listing all purchases
GET /catalog/v2/purchases
Authorization: Bearer {token}
Response:
{
"data": [
{
"id": "purchase-uuid",
"frameID": "12345_1",
"contractID": "your-contract-id",
"createdAt": "2025-01-15T10:30:00Z",
"status": "closed",
"eula": "STANDARD"
}
]
}
Returns all purchases for the current user (across all contracts), sorted by most recently modified.
The purchase history is account-level, not contract-level. Each purchase object contains a contractID field, which you can use for client-side filtering.
|
Getting purchase products
Once a purchase reaches closed status, you can access the products:
GET /catalog/v2/purchases/{purchaseId}/products
Authorization: Bearer {token}
This returns full STAC items with download links:
// frontend/src/components/MyImages.jsx
async function togglePurchaseProducts(purchaseId) {
const data = await api.getPurchaseProducts(purchaseId)
// data.data contains STAC items with:
// - properties (frame_id, acquisition_mode, product_type, etc.)
// - assets (thumbnail URLs, product download URLs)
}
| Product download URLs are signed and temporary. Don’t store them. Fetch fresh ones when the user wants to download. |
Part 7: Private vs Public collections
Public collection
-
Contains all images in the ICEYE public catalog
-
Anyone with API access can browse
-
Items include only low-resolution assets (thumbnails)
-
Images can be purchased
Private collection
-
Contains images you already own (from tasking orders or catalog purchases)
-
Items include all products with full-resolution download links
-
Images don’t need to be purchased, they’re already yours
How the demo distinguishes them
// frontend/src/components/ItemDetail.jsx
const isPrivate = collection === 'private'
// Private images: show download links
{isPrivate && downloadAssets.length > 0 && (
<div className="assets-section">
<h4>Available Assets</h4>
{downloadAssets.map(asset => (
<a href={asset.href} target="_blank" rel="noopener noreferrer">
{asset.title || asset.key}
</a>
))}
</div>
)}
// Public images: show purchase workflow (hidden after successful purchase)
{!isPrivate && frameId && !purchaseResult && (
<div className="purchase-section">
<h4>Purchase this Frame</h4>
{/* Contract selector, price check, purchase button */}
</div>
)}
When a user searches the private collection, they see their own images with download links. When they search public, they see the full catalog with purchase options. This is a natural way to separate "my images" from "images I can buy".
|
Part 8: Complete API call flow
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 | Notes |
|---|---|---|---|
GET Contracts |
Once |
App startup |
Populates contract selectors |
POST Search |
On demand |
User clicks "Search" |
Returns STAC items with thumbnails |
GET Items |
On demand |
User clicks "Load More" |
Cursor-based pagination from search |
GET Price |
On demand |
User clicks "Check Price" |
Optional, requires contractID + frameID |
POST Purchase |
Once per frame |
User clicks "Purchase" |
Creates the order |
GET Purchases |
On demand |
User opens Purchase History tab |
Lists all purchases |
GET Purchase Products |
On demand |
User expands a purchase |
Returns STAC items with download links |
Real-world considerations
Beyond the demo
The demo is great for learning, but production applications need:
1. Persistent storage
-
Cache search results and purchase history
-
Store user preferences and saved searches
2. Notifications
-
Use webhooks to get notified when purchases complete, instead of manual polling
3. Better download management
-
Queue downloads for large orders
-
Handle URL expiration gracefully (re-fetch when needed)
4. Production infrastructure
-
Rate limiting
-
Monitoring and logging
-
Error tracking
Next steps
-
Learn about building a tasking application to request new satellite captures
-
Explore pagination patterns for handling large result sets
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 catalog search proxy (
backend/api/routes/catalog.py) -
See map integration (
frontend/src/components/MapView.jsx) -
See purchase flow (
frontend/src/components/ItemDetail.jsx) -
See purchase history (
frontend/src/components/MyImages.jsx)