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:

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

  2. 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

public

The entire ICEYE public catalog. Anyone can browse it. Items include only low-resolution assets (thumbnails).

private

Images you already own (from tasking or catalog purchases). When queried alone, the response includes all products including high-resolution download links.

Contracts

Your commercial agreement with ICEYE. A contract determines:

  • Which collections you can access

  • Your pricing model (price per frame depends on the contract)

  • The account used for purchasing

The catalog workflow

  1. Authenticate: Get an OAuth2 access token

  2. Browse / Search: Find images using filters (location, date, imaging mode)

  3. Check price (optional): Get the cost for a specific frame under your contract

  4. Purchase: Buy the frame

  5. Track delivery: Poll purchase status until products are ready

  6. 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 contractID is 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

listCatalogItems

GET /catalog/v2/items

Simple browsing with query parameters, and cursor-based pagination

searchCatalogItems

POST /catalog/v2/search

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

bbox

Bounding box [west, south, east, north] in WGS84 coordinates

datetime

Date-time range in ISO 8601 format: start/end

collections

Array: ["public"], ["private"], or ["public", "private"]

contractID

Required when collections is specified. Validates access to the requested collections.

limit

Maximum results per page (default: 10)

query

STAC query extension for metadata filtering (e.g., filter by imaging mode)

sortby

Array of sort conditions (e.g., [{"field": "properties.start_datetime", "direction": "desc"}])

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:

  1. Search with POST /catalog/v2/search, which returns results and a cursor

  2. Get next pages with GET /catalog/v2/items?cursor=…​ (not POST)

  3. Repeat until no more cursor in 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.

Contract selection for purchase

The demo lets users select which contract to use at purchase time, not at search time. This is because:

  • Search only needs a contractID for collection validation

  • Price and purchase are where the contract choice truly matters (different pricing, different billing)

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 private collection

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

received

Order received, processing not started

active

Order is being processed

closed

Order complete, products are available for download

canceled

Order was canceled

failed

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

Try the demo

Find the demo code on GitHub and test it yourself!

Explore the code