Searching the Catalog
This guide shows methods for using pystac with Hydrosat's SpatioTemporal Asset Catalog (STAC) API to search for data that meets your criteria.
Topics covered include:
Searching by collection name
Searching by area of interest, including with a point geometry, polygon geometry, and bounding box
Searching by date range
Searching by metadata tag
1. Import dependencies.
import json
import pystac
from pystac_client import Client
import base642. Connect to the STAC API with your account credentials.
To run the next cell, you should have set up a creds.json file containing your username and password in the same directory as this notebook.
with open('creds.json') as f:
creds = json.loads(f.read())This next cell will endecode the username:password combination and use it to authorize access to the STAC API given by the cat_url endpoint. You don't need to make any changes here.
userpass = f"{creds['username']}:{creds['password']}"
b64 = base64.b64encode(userpass.encode()).decode()
headers = {'Authorization':'Basic ' + b64}
cat_url = 'https://stac.hydrosat.com/'
catalog = Client.open(cat_url, headers)3. Define your search terms.
The catalog contains VZ-1 imagery from around the globe. Let's start with a simple search of the catalog limited only by the collection of interest and the maximum total number of items.
Be patient! You may need to wait several seconds for the response to your query.
collection_id = "vz-l2"search = catalog.search(
collections = collection_id,
max_items=100) # Return a maximum of 500 items in collection
items_in_collection = list(search.item_collection())
print(f"Found {len(items_in_collection)} items.\n")Found 68 items.We'll write a function called show_footprints that uses geopandas to plot the bounding boxes for each item returned from our search.
⚠️ Note: Certain IDEs will render this map differently.
If you're using VSCode, you'll see multiple interactive maps in this Jupyter notebook.
If you're using Spyder, you'll need to run through entire notebook and open
search-map.html, which is saved at the end, in a browser window.
import geopandas as gpd
def show_footprints(items, color, map=None):
gdf = gpd.GeoDataFrame.from_features(items, crs="epsg:4326")[["geometry"]]
gdf['ID'] = [item.id for item in items]
gdf['Date'] = [item.datetime.date().strftime('%Y-%m-%d') for item in items]
gdf['Time (UTC)'] = [item.datetime.time().strftime('%H:%M:%S') for item in items]
if map is None: # if this is the first call, create a new map
return gdf.explore(color=color, style_kwds={"fillOpacity": 0.7, "weight": 1})
else: # if we have already created a map, add to it
return gdf.explore(m=map, color=color, style_kwds={"fillOpacity": 0.7, "weight": 1})Let's try it out.
items_in_collection_color = 'red'
m = show_footprints(items_in_collection, color=items_in_collection_color)
m
In the map, the red squares represent scenes available in the vz-l2 collection.
3.1 Searching by area of interest
We can add more criteria to catalog.search() to find data that intersects a specific location.
3.1.1 Searching with a point geometry
We'll start by defining a point geometry. This geometry will be used as an input to catalog.search(). Any imagery that overlaps this point will be returned.
point_geom = {'type': 'Point', 'coordinates': [116.7478, -32.7153]}search = catalog.search(
collections = collection_id,
intersects = point_geom
)
items_intersecting_point = list(search.item_collection())
print(f"Found {len(items_intersecting_point)} items.\n")Found 1 items.We'll plot the item footprint on a map, as before.
items_intersecting_point_color = 'orange'
m = show_footprints(items_intersecting_point, color=items_intersecting_point_color, map=m)
m
3.1.2 Searching with a polygon geometry
We can use any GeoJSON geometry type with intersects, including a MultiPoint, LineString, MultiLineString, Polygon, or MultiPolygon. For example, we can define:
poly_geom = {'type': 'Polygon', 'coordinates': [[
[
-115.02617069030356,
33.51297730941181
],
[
-108.63401434955702,
33.51297730941181
],
[
-108.63401434955702,
39.09484428364607
],
[
-115.02617069030356,
39.09484428364607
],
[
-115.02617069030356,
33.51297730941181
]
]]}And repeat the search:
search = catalog.search(
collections = collection_id,
intersects = poly_geom
)
items_intersecting_poly = list(search.item_collection())
print(f"Found {len(items_intersecting_poly)} items.\n")Found 6 items.Let's plot the items we found.
items_intersecting_poly_color = 'yellow'
m = show_footprints(items_intersecting_poly, color=items_intersecting_poly_color, map=m)
m
3.1.3 Searching with a bounding box
Alternatively, we can search with a bounding box instead of using intersects.
# [west, south, east, north]
bbox = [-123.044, 35.907, -118.834, 40.053]search = catalog.search(
collections = collection_id,
bbox = bbox
)
items_intersecting_bbox = list(search.item_collection())
print(f"Found {len(items_intersecting_bbox)} items.\n")Found 12 items.Let's map the items.
items_intersecting_bbox_color = 'lime'
m = show_footprints(items_intersecting_bbox, color=items_intersecting_bbox_color, map=m)
m
3.2. Searching by date range
We can also specify the start and end dates (and times) for the search. Let's try another search.
start_date = "2025-07-01"
start_time = "T00:00:00Z"
end_date = "2025-07-15"
end_time = "T00:00:00Z"search = catalog.search(
collections = collection_id,
datetime = [start_date+start_time, end_date+end_time]
)
items_in_daterange = list(search.item_collection())
print(f"Found {len(items_in_daterange)} items.\n")Found 6 items.Again, we'll visualize the scene footprints.
items_in_daterange_color = 'cyan'
m = show_footprints(items_in_daterange, color=items_in_daterange_color, map=m)
m
3.3 Searching by metadata tag
Anything listed under an item's properties is acceptable for use as a search filter (although some are more sensible filters than others). Let's look at an individual item to see the available properties.
items_in_daterange[0].properties{'start_datetime': '2025-07-14T02:23:28.436021Z',
'end_datetime': '2025-07-14T02:23:38.388531Z',
'created': '2025-07-23T23:03:32.797Z',
'hydrosat:scene_id': 'VZ01_L2_20250714_022333',
'datetime': '2025-07-14T02:23:33.412276Z',
'processing:lineage': 'Hydrosat Level-2 LST and SR processing',
'eo:cloud_cover': 0.4135154214440202,
'eo:snow_cover': 0,
'hydrosat:capture_type': 'image',
'hydrosat:day_night': 'day',
'instruments': ['liri-v1-01', 'viri-v1-01'],
'platform': 'vanzyl-01',
'processing:software': {'vz-l1b': 'hydrosat.vz_l1b==0.0.0+dev-2025-07-14T16:56:16+0000',
'vz-liri-l0': 'hydrosat.vz_liri_l0==0.18.2.dev133+g9f3cc60',
'vz-liri-l0b': 'hydrosat.vz_liri_l0b==0.18.2.dev32+g1debc9d',
'vz-liri-l1a': 'hydrosat.vz_liri_l1a==0.0.0+dev-2025-07-14T16:48:42+0000',
'vz-telemetry': 'hydrosat.vz_telemetry==0.18.2.dev18+ga0564b2',
'vz-viri-l0': 'hydrosat.vz_viri_l0==0.18.2.dev47+g16aefb1',
'vz-viri-l1a': 'hydrosat.vz_viri_l1a==0.0.0+dev-2025-07-14T16:43:48+0000',
'vz-l2': 'hydrosat.vz_l2==0.0.0+dev-2025-07-14T17:07:52+0000'},
'proj:centroid': {'lat': -31.928, 'lon': 119.885},
'proj:epsg': 32750,
'sat:orbit_state': 'descending',
'sat:platform_international_designator': '2024-149AN',
'updated': '2025-07-23T23:03:32.797Z',
'version': '0.18.1-86-gdbf5836',
'view:azimuth': 91.194,
'view:off_nadir': 0.167,
'view:sun_azimuth': 28.053,
'view:sun_elevation': 31.07}Let's try it out.
We'll use the view:off_nadir property to constrain our search. Specifically, we only want images acquired with a predetermined off-nadir angle. To streamline our result, we'll use sortby to sort the items by increasing off-nadir angle. If we wanted to sort in descending order, we could substitute the "+" character in sortby for "-".
We'll also use the max_items argument to limit our search. Only that number of items with the smallest off-nadir angles in the collection will be returned.
search = catalog.search(
collections = collection_id,
query = {"view:off_nadir": {"gte": -2, "lte": 2}},
sortby=["+properties.view:off_nadir"],
max_items=5
)
items_nadir = list(search.item_collection())
print(f"Found {len(items_nadir)} items.\n")Found 5 items.Time to plot.
items_nadir_color = 'fuchsia'
m = show_footprints(items_nadir, color=items_nadir_color, map=m)
m.save("search-map.html")
m
4. Summary of approaches
Whether you're interested in searching for data by AOI, date range, metadata tag, or all of the above, you can configure catalog.search() to find what you need.
Let's summarize the number of items we retrieved with each search above.
n_items = [len(i) for i in [items_in_collection, items_intersecting_point, items_intersecting_poly, items_intersecting_bbox, items_in_daterange, items_nadir]]
colors = [items_in_collection_color, items_intersecting_point_color, items_intersecting_poly_color, items_intersecting_bbox_color, items_in_daterange_color, items_nadir_color]from matplotlib import pyplot as plt
plt.figure(figsize=(12,3))
plt.barh(y=range(len(n_items)), width=n_items[::-1], height=0.6,
color=colors[::-1],
edgecolor='k',
zorder=2);
plt.yticks(range(len(n_items)),
['In collection', 'Intersects point geometry', 'Intersects polygon geometry', 'Intersects bounding box', 'In date range', 'Within ± off-nadir angle°'][::-1]);
plt.xlabel('Number of items')
plt.grid(which='major', axis='x', color='lightgray')
plt.tight_layout()
plt.show()
Last updated
Was this helpful?