ESA BIOMASS Simulated Data Access and Visualization (Token-secured)
Authors: Rajat Shinde (UAH), Alex Mandel (Development Seed), Sheyenne Kirkland (UAH), Harshini Girish (UAH), Jamison French (Development Seed), Henry Rodman (Development Seed), Chuck Daniels (Development Seed), Zac Deziel (Development Seed), Brian Freitag (NASA), Francesco Ferrante (SERCO), Cristiano Lopes (ESA)
Date: December 16, 2025
Description: This notebook documents how to access and visualize the ESA BIOMASS simulated data hosted on the ESA MAAP server. It is an example illustrating data access from ESA server based on ESA MAAP Token using the NASA MAAP Authorization.
What you will do
Understand the product and file organization.
Obtain an ESA data access offline token.
Convert offline token to a short-lived data access token
Access a simulated raster using the token.
Visualize in Python.
Run this notebook
Run this notebook
To access and run this tutorial within MAAP’s Algorithm Development Environment (ADE), please refer to the Getting started with the MAAP section of our documentation.
Disclaimer: it is highly recommended to run a tutorial within MAAP’s ADE, which already includes packages specific to MAAP, such as maap-py. Running the tutorial outside of the MAAP ADE may lead to errors.
Prerequisites
An active ESA MAAP portal account with access initialized.
OAuth2 client credentials for your ESA realm.
Python packages:
requests,rasterio,numpy,matplotlib(optional:pystac-client,stackstac).
Importing and Installing Packages
[2]:
# Install if needed. Comment out if already available.
# !pip install pystac-client rasterio requests matplotlib
import os
import numpy as np
import requests
import matplotlib.pyplot as plt
import rasterio
from rasterio.io import MemoryFile
from pystac_client import Client
from pathlib import Path
Getting the ESA MAAP Long Lasting Token
This explains how to retrieve a long lasting token (hereafter, offline token) from the ESA MAAP portal using your browser and NASA EDL login. The offline token is valid for a 90-day period.
Open the token page in your browser: https://portal.maap.eo.esa.int/ini/services/auth/token/
Steps
Navigate to the URL above.
Choose NASA Earthdata Login (EDL) when prompted and authorize access.
After successful authorization you will see a token page showing your short‑lived access token string.
The below screenshots illustrate the process for each steps.
Portal entry page:

NASA EDL authorization screen:

Token page after authorization:

Copy the offline token value from the token page for use in the next cell.
Converting ESA MAAP Offline Token to get Short-lived Data Access Token
The steps to get short-lived data access token are described here, and also illustrated below.
All API keys and tokens are stored in a credentials.txt file located in the home directory of user.
Steps
Create a
credentials.txtfile in the home directory of the user.Edit and copy the following snippet to the
credentials.txt.
CLIENT_ID=offline-token
CLIENT_SECRET=p1eL7uonXs6MDxtGbgKdPVRAmnGxHpVE
OFFLINE_TOKEN=your_esamaap_offline_token_here
The CLIENT_ID and CLIENT_SECRET should be as defined above.
Run the below cells to get a short-lived data access token in
token.
[3]:
# --- Path to credentials.txt ---
CREDENTIALS_FILE = (Path.home() / "credentials.txt").resolve()
def load_credentials(file_path=CREDENTIALS_FILE):
"""Read key-value pairs from a credentials file into a dictionary."""
creds = {}
if not file_path.exists():
raise FileNotFoundError(f"Credentials file not found: {file_path}")
with open(file_path, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
key, value = line.split("=", 1)
creds[key.strip()] = value.strip()
return creds
# --- ESA MAAP API ---
def get_token():
"""Use OFFLINE_TOKEN to fetch a short-lived access token."""
creds = load_credentials()
OFFLINE_TOKEN = creds.get("OFFLINE_TOKEN")
CLIENT_ID = creds.get("CLIENT_ID")
CLIENT_SECRET = creds.get("CLIENT_SECRET")
if not all([OFFLINE_TOKEN, CLIENT_ID, CLIENT_SECRET]):
raise ValueError("Missing OFFLINE_TOKEN, CLIENT_ID, or CLIENT_SECRET in credentials file")
url = "https://iam.maap.eo.esa.int/realms/esa-maap/protocol/openid-connect/token"
data = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"grant_type": "refresh_token",
"refresh_token": OFFLINE_TOKEN,
"scope": "offline_access openid"
}
response = requests.post(url, data=data)
response.raise_for_status()
response_json = response.json()
access_token = response_json.get('access_token')
if not access_token:
raise RuntimeError("Failed to retrieve access token from IAM response")
return access_token
token = get_token()
Note
Treat tokens as secrets. Do not commit them to version control or share publicly.
Now we pass the data access token as header in our subsequent requests.
[4]:
headers = {"Authorization": f"Bearer {token}"}
# STAC API that backs the MAAP/ESA STAC Browser
STAC_API_URL = "https://catalog.maap.eo.esa.int/catalogue/" # <-- note the /catalogue/ suffix
# Connect to the STAC API landing page (NOT the browser link)
stac = Client.open(STAC_API_URL, headers=headers)
print("Connected to STAC:", STAC_API_URL)
# Quick sanity check: list a few collections (should include BiomassSimulated variants)
cols = list(stac.get_collections())
print("Collections found:", len(cols))
print([c.id for c in cols[:10]])
Connected to STAC: https://catalog.maap.eo.esa.int/catalogue/
Collections found: 290
['series', 'datasets', 'EOP:ESA:Sentinel-2', 'EOP:ESA:CopDem', 'EarthCAREL2Products_MAAP', 'Landsat8.Collection2.European.Coverage_', 'JAXAL2InstChecked_MAAP', 'JAXAL2Products_MAAP', 'BiomassLevel2a', 'BiomassLevel1bIOC']
[5]:
# List only collections that contain the word "Simulated"
simulated_cols = [c for c in cols if "Simulated" in (c.id or "") or "Simulated" in (c.title or "")]
print(f"Found {len(simulated_cols)} collections containing 'Simulated':")
for c in simulated_cols:
print(f" - {c.id} :: {c.title}")
# If you want to inspect one of them (e.g., BiomassSimulated)
sim_col = [c for c in simulated_cols if "BiomassSimulated" in c.id]
if sim_col:
print("\nSelected BiomassSimulated collection:", sim_col[0].id)
print("Description:", sim_col[0].description)
Found 1 collections containing 'Simulated':
- BiomassSimulated :: Biomass Simulated data
Selected BiomassSimulated collection: BiomassSimulated
Description: Biomass Simulated data products
Testing Data Access
The below cell defines the URL of a specific tile in the ESA BIOMASS Simulated data collection that we want to test access on.
[6]:
time_day = "2017-02-25/2017-02-25"
search = stac.search(collections=["BiomassSimulated"], datetime=time_day, limit=1000)
items = list(search.get_items())
print("Total items:", len(items))
Total items: 5
Here we make a request to the example tile using the token to check that authentication is working.
[7]:
# Pick the first item whose ID contains S3_DGM__ (matches ...__1S or ...__1M)
s3_item = None
for it in items:
if "S3_DGM__1S" in it.id:
s3_item = it
break
if s3_item is None:
raise RuntimeError("No item with 'S3_DGM__1S' in ID found in the current item list.")
print("Picked item:", s3_item.id, "from collection:", s3_item.collection_id)
asset_key, asset_href = None, None
for k, a in s3_item.assets.items():
mt = (a.media_type or "").lower()
href = a.get_absolute_href() or a.href
if ("tiff" in mt) or href.lower().endswith((".tif", ".tiff")):
asset_key, asset_href = k, href
break
if asset_href is None:
raise RuntimeError(f"No TIFF/COG assets on {s3_item.id}")
print("Asset:", asset_key, "→", asset_href)
r = requests.get(asset_href, headers=headers, stream=True, timeout=180)
print("HTTP status:", r.status_code)
if r.status_code != 200:
# Print a bit of the error text to see what the server says
print("Response text (first 500 chars):")
print(r.text[:500])
raise RuntimeError(f"Fetch failed: HTTP {r.status_code}")
Picked item: BIO_S3_DGM__1S_20170225T094537_20170225T094558_I_G01_M02_C01_T017_F001_01_DCJ9SK from collection: BiomassSimulated
Asset: enclosure_tiff → https://catalog.maap.eo.esa.int/data/biomass-pdgs-01/BiomassSimulated/2017/02/25/BIO_S3_DGM__1S_20170225T094537_20170225T094558_I_G01_M02_C01_T017_F001_01_DCJ9SK/BIO_S3_DGM__1S_20170225T094537_20170225T094558_I_G01_M02_C01_T017_F001_01_DCJ9SK/measurement/bio_s3_dgm__1s_20170225t094537_20170225t094558_i_g01_m02_c01_t017_f001_i_abs.tiff
HTTP status: 200
Load the raster into memory and visualize
If access is successful, this cell reads the raster data in memory and plots a quicklook visualization.
[8]:
if r.status_code != 200:
raise RuntimeError(f"Fetch failed: HTTP {r.status_code}")
with MemoryFile(r.content) as mem:
with mem.open() as ds:
arr = ds.read(1)
prof = ds.profile
valid = arr[np.isfinite(arr)]
if valid.size == 0:
raise RuntimeError("No valid pixels found.")
print("Shape:", arr.shape)
print("CRS:", prof.get("crs"))
print("Transform:", prof.get("transform"))
print("Min/Max:", float(np.nanmin(valid)), float(np.nanmax(valid)))
h, w = arr.shape
fig_w = 2
fig_h = max(3, min(10, fig_w * (h / w))) # keep the tall aspect but readable
eps = 1e-6
arr_db = 20*np.log10(np.maximum(arr, eps)) # magnitude -> dB (use 10*log10 if arr is power)
valid = arr_db[np.isfinite(arr_db)]
vmin, vmax = np.nanpercentile(valid, (2, 98))
plt.figure(figsize=(fig_w, fig_h), dpi=160)
im = plt.imshow(arr_db, cmap="gray", vmin=vmin, vmax=vmax, interpolation="nearest")
plt.colorbar(im, label="Backscatter (dB)", shrink=0.6)
plt.axis("off")
plt.tight_layout()
Shape: (6321, 2251)
CRS: None
Transform: | 1.00, 0.00, 0.00|
| 0.00, 1.00, 0.00|
| 0.00, 0.00, 1.00|
Min/Max: 32.63134765625 2673.335205078125
Using Rasterio for Data Access
The below cell shows another method to load the data directly with Rasterio by passing the token in GDAL HTTP headers.
[ ]:
# Works if your GDAL/Rasterio build supports HTTP headers with /vsicurl/.
rio_env = rasterio.Env(GDAL_HTTP_HEADERS=f"Authorization: Bearer {token}")
with rio_env:
with rasterio.open(asset_href) as ds:
arr = ds.read(1)
valid = arr[np.isfinite(arr)]
print("Shape:", arr.shape)
print("CRS:", ds.crs)
print("Transform:", ds.transform)
print("Min/Max:", float(valid.min()), float(valid.max()))
Downloading File Locally
Here we define a helper function to save authenticated downloads to disk for later use.
[ ]:
def save_auth_file(url: str, token: str, out_path: str, chunk: int = 1 << 20) -> str:
headers = {"Authorization": f"Bearer {token}"}
with requests.get(url, headers=headers, stream=True) as resp:
resp.raise_for_status()
with open(out_path, "wb") as f:
for part in resp.iter_content(chunk_size=chunk):
if part:
f.write(part)
return out_path
# Example:
save_auth_file(asset_href, token, asset_href.split('/')[-1])
The example BIOMASS simulated data tile is successfully downloaded locally.