Skip to content

Search with pystac-client and curl

The catalog routes follow STAC API 1.0, so standard STAC tools work. Search needs a key in lean v1.

POST is the better form for anything beyond a simple query: long geometries, many ids and the cloud filter.

Terminal window
curl -s -X POST https://api.satellitedata.net/v1/search \
-H "X-API-Key: $SDN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"collections": ["sentinel-2-l2a", "landsat-ot-l2"],
"bbox": [-93.3, 44.9, -93.2, 45.0],
"datetime": "2026-06-01T00:00:00Z/2026-08-31T23:59:59Z",
"limit": 10,
"query": {"eo:cloud_cover": {"lte": 40}}
}'
  • collections takes canonical ids or aliases, so sentinel-2-c1-l2a and landsat-c2-l2 work too.
  • bbox is west, south, east, north in longitude and latitude.
  • datetime is an RFC 3339 instant or interval. An open end is two dots: 2026-06-01T00:00:00Z/...
  • intersects takes a GeoJSON geometry instead of bbox (not both).
  • query is the STAC query extension. Operators include lt, lte, gt, gte, eq and in.
  • sortby and fields work too, for example "sortby": [{"field": "properties.eo:cloud_cover", "direction": "asc"}].
Terminal window
curl -s "https://api.satellitedata.net/v1/search?collections=sentinel-2-l2a&bbox=-93.3,44.9,-93.2,45.0&datetime=2026-07-01T00:00:00Z/2026-07-31T23:59:59Z&limit=5&sortby=properties.eo:cloud_cover" \
-H "X-API-Key: $SDN_API_KEY"

On GET, query and intersects are JSON strings (URL-encode them), and sortby takes a - prefix for descending order.

Terminal window
curl -s "https://api.satellitedata.net/v1/collections/landsat-ot-l2/items?bbox=-93.3,44.9,-93.2,45.0&limit=5" \
-H "X-API-Key: $SDN_API_KEY"

A single item is anonymous:

Terminal window
curl -s https://api.satellitedata.net/v1/collections/sentinel-2-l2a/items/S2C_T15TVK_20260712T171233_L2A

pystac-client opens the landing page, reads the conformance classes and follows paging links for you. Pass the key as a header:

import os
from pystac_client import Client
client = Client.open(
"https://api.satellitedata.net/v1",
headers={"X-API-Key": os.environ["SDN_API_KEY"]},
)
search = client.search(
collections=["sentinel-2-l2a", "landsat-ot-l2"],
bbox=[-93.3, 44.9, -93.2, 45.0],
datetime="2026-06-01/2026-08-31",
query={"eo:cloud_cover": {"lte": 40}},
max_items=50,
)
for item in search.items():
print(item.id, item.collection_id, item.properties.get("eo:cloud_cover"))

pystac-client searches with POST by default. Use query=, not filter=: lean v1 does not list the filter extension in its conformance classes, and a CQL2 filter is rejected with invalid-filter.

A search answers a GeoJSON FeatureCollection. Each feature is a STAC item:

  • id: the native id (see Collections and ids).
  • collection: the canonical collection id.
  • properties: datetime, platform (such as sentinel-2c or landsat-8), eo:cloud_cover, and mission properties such as s2:processing_baseline, s2:mgrs_tile, landsat:wrs_path and landsat:wrs_row.
  • assets: links to the upstream Cloud-Optimized GeoTIFFs.
  • links: rewritten to point at api.satellitedata.net/v1.

Asset links point at the public upstream files. Sentinel-2 files are free to read over HTTPS. Landsat files are in the requester-pays usgs-landsat S3 bucket, so reading them directly costs you the S3 request charges. To avoid that, let the API read the pixels with tiles or process.

The answer has a next link when more results exist. For POST search the next link has method: POST, a body holding the token, and merge: true, which tells you to merge that body into your original request; pystac-client does this automatically. Follow next until it is absent.

  • CQL2 filters (filter in CQL2 text on GET and CQL2 JSON on POST), with GET /v1/collections/{collectionId}/queryables listing the properties you can filter on.
  • The optical-sr collection, which searches every optical sensor at once and removes duplicate overpasses.
  • Anonymous search under per-IP limits.