Discover Data via the STAC API

Datasets hosted at eodc are cataloged by making use of the STAC (SpatioTemporal Asset Catalog) specifications. The catalog service is available as STAC API via https://stac.eodc.eu/api/v1 to enable users to discover and search for datasets filtering by space, time and other attributes. In the following we will demonstrate the use of the STAC API and open-source Python libraries to run search queries against multiple STAC API instances.

Connecting to the eodc STAC catalogue

In this example, we are going to make use of a popular STAC client for Python, the pystac-client. The library is already installed in this environment, but can be manually installed anywhere else via pip install pystac-client.

try:
    from pystac_client import Client
except ImportError:
    %pip install pystac-client
    from pystac_client import Client

try:
    from IPython.display import Image
except ImportError:
    %pip install IPython
    from IPython.display import Image

try:
    from rich.console import Console
except ImportError:
    %pip install rich
    from rich.console import Console

import rich.table

try:
    import geopandas
except ImportError:
    %pip install geopandas
    import geopandas
eodc_catalog = Client.open(
    "https://stac.eodc.eu/api/v1",
)

eodc_catalog.title
'EODC Data Catalogue'

Searching for collections

All data in the catalog is stored in so-called collections, which are named, for example, after the satellite mission.

for collection in eodc_catalog.get_collections():
    print(collection)
<CollectionClient id=AI4SAR_SIG0>
<CollectionClient id=ASA_IMP_1P>
<CollectionClient id=ASA_IMS_1P>
<CollectionClient id=AUSTRIA_GROUND_MOTION>
<CollectionClient id=AUT_DEM>
<CollectionClient id=BOA_LANDSAT_8>
<CollectionClient id=BOA_SENTINEL_2>
<CollectionClient id=CGLS_SSM_1KM>
<CollectionClient id=climatedt>
<CollectionClient id=climatedt-austria>
<CollectionClient id=clms>
<CollectionClient id=clms-vpp>
<CollectionClient id=COP_DEM>
<CollectionClient id=CORINE_LAND_COVER>
<CollectionClient id=DOP_AUT_K_KLAGENFURT>
<CollectionClient id=DOP_AUT_K_OSTTIROL>
<CollectionClient id=DOP_AUT_K_TAMSWEG>
<CollectionClient id=DOP_AUT_K_VILLACH>
<CollectionClient id=DOP_AUT_K_WOLFSBERG>
<CollectionClient id=DOP_AUT_K_ZELL_AM_SEE>
<CollectionClient id=DOP_AUT_K_ZELTWEG>
<CollectionClient id=DOP_AUT_ST_BISCHOFSHOFEN>
<CollectionClient id=DOP_AUT_ST_GRAZ>
<CollectionClient id=DOP_AUT_ST_KLAGENFURT>
<CollectionClient id=DOP_AUT_ST_MARIAZELL>
<CollectionClient id=DOP_AUT_ST_MURTAL>
<CollectionClient id=DOP_AUT_ST_SUEDBURGENLAND>
<CollectionClient id=DOP_AUT_ST_VILLACH>
<CollectionClient id=DOP_AUT_ST_WINDISCHGARSTEN>
<CollectionClient id=DROUGHT_VULNERABILITY>
<CollectionClient id=DSM_AUT>
<CollectionClient id=ERS_ENVISAT_NRB>
<CollectionClient id=GFM>
<CollectionClient id=incal-hourly>
<CollectionClient id=INTRA_FIELD_CROP_GROWTH_POTENTIAL>
<CollectionClient id=RUCIO_SENTINEL2_MFCOVER>
<CollectionClient id=SAR_IMP_1P>
<CollectionClient id=SAR_IMS_1P>
<CollectionClient id=SENTINEL1_ALPS_WETSNOW>
<CollectionClient id=SENTINEL1_GMR0>
<CollectionClient id=SENTINEL1_GRD>
<CollectionClient id=SENTINEL1_GRD_COVERAGE>
<CollectionClient id=SENTINEL1_HPAR>
<CollectionClient id=Sentinel-1_Lacken_Extent>
<CollectionClient id=SENTINEL1_MPLIA>
<CollectionClient id=Sentinel-1_Reed_Extent>
<CollectionClient id=SENTINEL1_SCENE_GMR0>
<CollectionClient id=SENTINEL1_SIG0_20M>
<CollectionClient id=SENTINEL1_SLC>
<CollectionClient id=Sentinel-2-Greenness-Austria>
<CollectionClient id=SENTINEL2_GRI_L1C>
<CollectionClient id=SENTINEL2_L1C>
<CollectionClient id=SENTINEL2_L1C_COVERAGE>
<CollectionClient id=SENTINEL2_L2A>
<CollectionClient id=sentinel2-landsat8-l2f>
<CollectionClient id=SENTINEL2_MFCOVER>
<CollectionClient id=SENTINEL3_SRAL_L2>
<CollectionClient id=SENTINEL5P_DAILY_AUT>
<CollectionClient id=spartacus-daily>
<CollectionClient id=SSM-RT0-SIG0-R-EXTR>
<CollectionClient id=topo-dc-austria-dsm>
<CollectionClient id=topo-dc-austria-dtm>
<CollectionClient id=topo-dc-austria-ndsm>
<CollectionClient id=VEGETATION_CHANGE_AUSTRIA>

On static as well as dynamic catalogues we cann also make use of the links attributes which lets us quickly examinate, for instance, the number of available collections.

collections = list(eodc_catalog.get_collections())

print(f"The EODC STAC currently features {len(collections)} collections.")
The EODC STAC currently features 64 collections.

Individual collections can be searched for.

collection = eodc_catalog.get_collection("SENTINEL2_L1C")
collection
print(f"This collection contains data in the following temporal inteval: {collection.extent.temporal.to_dict()}")
This collection contains data in the following temporal inteval: {'interval': [['2015-07-04T00:00:00Z', None]]}

STAC Items

Simlarly to before, we can use the collection client instance to iterate over the items contained in the collection. The server must provide the /collections/<collection_id>/items endpoint to support this feature automatically. This can be useful to manually filter items or extract information programmatically. The get_all_items() method again returns an iterator.

items = collection.get_all_items()

Load 10 items with cloud cover less than 10%

items10 = []
for n, item in enumerate(items):
    if len(items10) == 10:
        break
    cloud_cover = item.properties.get("eo:cloud_cover")
    if cloud_cover < 10:
        print(f"Append item {item.id} with {cloud_cover:.2f}% cloud cover")
        items10.append(item)
Append item S2B_MSIL1C_20250120T120359_R066_T30VVQ_20250120T153429 with 0.00% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T30VVP_20250120T153429 with 2.81% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T30VVN_20250120T153429 with 0.00% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T30VUQ_20250120T153429 with 8.88% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T30VUP_20250120T153429 with 9.69% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T30VUN_20250120T153429 with 9.39% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T29VPK_20250120T153429 with 9.80% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T29VPH_20250120T153429 with 8.96% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T29VNK_20250120T153429 with 6.02% cloud cover
Append item S2B_MSIL1C_20250120T120359_R066_T29VNJ_20250120T153429 with 6.60% cloud cover

If the item provides a preview image we can look at it.

Image(url=items10[4].assets["thumbnail"].href, width=500)

Search for items in a collection with filter criterias

First we set the temporal and spatial extent.

There are two options for a spatial extent.

  1. A polygon in GEOJSON

  2. A bounding box (bbox)

console = Console()

time_range = "2023-05-01/2024-05-01"

# GEOJSON can be created on geojson.io
# Area around the Neusiedler See
area_of_interest = {
"coordinates": [
          [
            [
              16.685331259653253,
              48.001346032803355
            ],
            [
              16.621884871275512,
              47.902601630022275
            ],
            [
              16.62588718725482,
              47.81041047247777
            ],
            [
              16.664809254423375,
              47.774602171781936
            ],
            [
              16.96808652311867,
              47.76771348708101
            ],
            [
              16.963971948548988,
              48.00956486424042
            ],
            [
              16.685331259653253,
              48.001346032803355
            ]
          ]
        ],
        "type": "Polygon"
      }
# Bounding box of Austria
#bbox_aut = [9.25, 46.31, 17.46, 49.18]

We search for Sentinel-2 data, that matches our filter criteria

search = eodc_catalog.search(
    collections=["SENTINEL2_L1C"],
    intersects=area_of_interest,
    #bbox = bbox_aut,
    datetime=time_range
)

items_eodc = search.item_collection()
console.print(f"On eodc we found {search.matched()} items for the given search query")
On eodc we found 84 items for the given search query
df = geopandas.GeoDataFrame.from_features(items_eodc.to_dict(), crs="epsg:4326")

#print the first three rows of the dataframe
df.head(3)
geometry created datetime platform grid:code providers s2:tile_id instruments view:azimuth constellation ... s2:degraded_msi_data_percentage s2:reflectance_conversion_factor proj:code published deprecated s2:mgrs_tile s2:granule_id cube:dimensions processing:level processing:facility
0 POLYGON ((17.64985 48.7214, 16.36028 48.74498,... 2025-07-14T11:57:30.706907Z 2024-03-10T09:57:29.024000Z sentinel-2b MGRS-33UXP [{'url': 'https://earth.esa.int/web/guest/home... S2B_OPER_MSI_L1C_TL_2BPS_20240310T120343_A0366... [msi] 287.541639 sentinel-2 ... 0.0274 1.016055 EPSG:32633 NaN NaN NaN NaN NaN NaN NaN
1 POLYGON ((17.24198 47.82959, 16.3366 47.84592,... 2025-07-14T11:57:22.169453Z 2024-03-10T09:57:29.024000Z sentinel-2b MGRS-33TXN [{'url': 'https://earth.esa.int/web/guest/home... S2B_OPER_MSI_L1C_TL_2BPS_20240310T120343_A0366... [msi] 287.066707 sentinel-2 ... 0.0350 1.016055 EPSG:32633 NaN NaN NaN NaN NaN NaN NaN
2 POLYGON ((16.36028 48.74498, 16.33433 47.75741... 2025-07-08T21:01:59.407148Z 2024-03-07T09:47:39.024000Z sentinel-2b MGRS-33UXP [{'url': 'https://earth.esa.int/web/guest/home... S2B_OPER_MSI_L1C_TL_2BPS_20240307T115148_A0365... [msi] 104.289009 sentinel-2 ... 0.0213 1.017595 EPSG:32633 NaN NaN NaN NaN NaN NaN NaN

3 rows × 37 columns

Now we can select the item with the least (min) cloud cover. Data providers exposing STAC can make use of a number of STAC extensions. Some collections implement the so-called eo extension, which can be used to sort items by cloudiness.

selected_item = min(items_eodc, key=lambda item: item.properties["eo:cloud_cover"])

selected_item

Print the Thumbnail of the item

Image(url=selected_item.assets["thumbnail"].href, width=500)

Each STAC item has one or more Assets, which include links to actual files. So let’s print a list with all assets.

table = rich.table.Table(title="Assets in STAC Item")
table.add_column("Asset Key", style="cyan", no_wrap=True)
table.add_column("Description")
for asset_key, asset in selected_item.assets.items():
    table.add_row(asset_key, asset.title)

console.print(table)
                  Assets in STAC Item                   
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Asset Key           Description                     ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ B01                │ Coastal aerosol (band 1) - 60m  │
│ B02                │ Blue (band 2) - 10m             │
│ B03                │ Green (band 3) - 10m            │
│ B04                │ Red (band 4) - 10m              │
│ B05                │ Red edge 1 (band 5) - 20m       │
│ B06                │ Red edge 2 (band 6) - 20m       │
│ B07                │ Red edge 3 (band 7) - 20m       │
│ B08                │ NIR 1 (band 8) - 10m            │
│ B09                │ NIR 3 (band 9) - 60m            │
│ B10                │ Cirrus (band 10) - 60m          │
│ B11                │ SWIR 1 (band 11) - 20m          │
│ B12                │ SWIR 2 (band 12) - 20m          │
│ B8A                │ NIR 2 (band 8A) - 20m           │
│ visual             │                                 │
│ preview            │ Preview image found in Archive. │
│ safe-zip           │                                 │
│ thumbnail          │ Preview Image (converted)       │
│ safe-manifest      │                                 │
│ granule-metadata   │                                 │
│ inspire-metadata   │                                 │
│ product-metadata   │                                 │
│ datastrip-metadata │                                 │
└────────────────────┴─────────────────────────────────┘