How to download SRTM DEMs for Python hydrology workflows
Querying the OpenTopography Global DEM API with requests, validating the binary payload with rasterio, and caching results in a project-local directory is the fastest route to a reproducible SRTM tile in a Python hydrology pipeline. This page is part of the SRTM and LiDAR Data Acquisition cluster within the Hydrology Data Preparation & DEM Processing domain. The approach here complements the earthaccess-based NASA Earthdata workflow described in the parent cluster; it is better suited to quick bounding-box queries that do not need a separate NASA account.
The diagram below shows how a single function call moves through authentication, caching, download, and validation before returning a verified GeoTIFF path:
Prerequisites
This page assumes the base environment from the SRTM and LiDAR Data Acquisition cluster (rasterio>=1.3, requests>=2.31, Python 3.9+). Beyond that baseline, you need:
- OpenTopography API key — free registration at portal.opentopography.org; inject via an environment variable, never hardcoded.
- GDAL >= 3.4 linked to your
rasteriobuild (conda install -c conda-forge gdal rasterioon Windows to avoid DLL issues). - Write-accessible local directory for the tile cache; at least 500 MB free per large-watershed request.
- No
earthaccessor NASA Earthdata account is required for this approach.
Core technique: bounding-box query against the OpenTopography Global DEM endpoint
The OpenTopography Global DEM API accepts a spatial bounding box, a DEM product identifier (SRTMGL1 for 30 m or SRTMGL3 for 90 m), and an API key, then streams a GeoTIFF in WGS84 (EPSG:4326) directly to the caller. The entire tile is returned as a single binary response — there is no pagination or tile-splitting on the client side for requests under roughly 10,000 km².
The key design decisions in a production wrapper are:
- Filename-based caching keyed on bbox coordinates and resolution string so that repeated pipeline runs skip the network entirely.
- Exponential-backoff retry on HTTP 429 responses — the API enforces fair-use limits and a bare
response.raise_for_status()loop will crash long overnight jobs. - Immediate structural validation with
rasterioafter every write — a corrupted or empty GeoTIFF will silently pass through downstream DEM pit filling algorithms and produce nonsensical flow direction grids if not caught here.
Annotated code example
import logging
import os
import time
from pathlib import Path
import rasterio
import requests
logger = logging.getLogger(__name__)
def download_srtm_dem(
bbox: tuple[float, float, float, float],
output_dir: str = "dem_cache",
resolution: str = "SRTMGL1",
api_key: str | None = None,
max_retries: int = 3,
) -> Path:
"""Download an SRTM tile for *bbox* from the OpenTopography Global DEM API.
Parameters
----------
bbox:
``(south, north, west, east)`` in decimal degrees, WGS84 (EPSG:4326).
Verify against your watershed CRS before calling — inverted coordinates
return a valid HTTP 200 with an empty GeoTIFF.
resolution:
``'SRTMGL1'`` for 30 m (~1 arc-second) or ``'SRTMGL3'`` for 90 m.
api_key:
OpenTopography API key. Required for every Global DEM request since
mid-2023. Pass via ``os.environ['OPENTOPOGRAPHY_API_KEY']`` in production.
max_retries:
Number of retry attempts on HTTP 429 (rate-limit) before raising.
"""
if not api_key:
raise ValueError(
"OpenTopography requires an API key for all Global DEM requests. "
"Register at https://portal.opentopography.org/ and store the key "
"in the OPENTOPOGRAPHY_API_KEY environment variable."
)
cache_dir = Path(output_dir)
cache_dir.mkdir(parents=True, exist_ok=True)
south, north, west, east = bbox
# Cache filename encodes all request parameters so hits are deterministic.
out_path = cache_dir / f"srtm_{resolution}_{west}_{east}_{south}_{north}.tif"
if out_path.exists():
logger.info("Cache hit — skipping download: %s", out_path)
return out_path
url = "https://portal.opentopography.org/API/globaldem"
params = {
"demtype": resolution,
"south": south,
"north": north,
"west": west,
"east": east,
"outputFormat": "GTiff",
"API_Key": api_key, # required since mid-2023
}
logger.info(
"Requesting %s tile for bbox=(%s, %s, %s, %s)",
resolution, south, north, west, east,
)
response = None
for attempt in range(max_retries):
response = requests.get(url, params=params, timeout=120)
if response.status_code == 429:
# Exponential backoff: 5 s, 10 s, 20 s …
wait_seconds = (2 ** attempt) * 5
logger.warning(
"Rate limited (attempt %d/%d). Retrying in %ds …",
attempt + 1, max_retries, wait_seconds,
)
time.sleep(wait_seconds)
continue
response.raise_for_status()
break
else:
raise RuntimeError(
f"OpenTopography rate limit persisted after {max_retries} attempts. "
"Reduce request frequency or split the bbox into smaller tiles."
)
# Write raw bytes — do not decode; GeoTIFF is a binary format.
out_path.write_bytes(response.content)
logger.info("Wrote %d bytes to %s", len(response.content), out_path)
# Validate structural integrity before returning. A 200 OK response can
# still contain an HTML error page or an empty raster if the bbox is outside
# SRTM coverage or the key lacks the required permission tier.
with rasterio.open(out_path) as src:
if src.count == 0 or src.width == 0 or src.height == 0:
out_path.unlink() # remove corrupt file from cache
raise ValueError(
f"Downloaded file is empty or not a valid GeoTIFF: {out_path}. "
"Check that the bbox intersects SRTM coverage and the API key is active."
)
logger.info(
"Validated %s | shape=%s | CRS=%s | nodata=%s",
out_path.name, src.shape, src.crs, src.nodata,
)
return out_path
# ---------------------------------------------------------------------------
# Usage example — Southern California watershed, 30 m SRTM
# ---------------------------------------------------------------------------
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
dem_path = download_srtm_dem(
bbox=(34.5, 35.2, -118.5, -117.8), # (south, north, west, east)
output_dir="data/raw/srtm",
resolution="SRTMGL1",
api_key=os.environ["OPENTOPOGRAPHY_API_KEY"],
)
print(f"DEM ready: {dem_path}")
Parameter reference
| Parameter | Accepted values | Effect on hydrology |
|---|---|---|
resolution |
SRTMGL1 (30 m), SRTMGL3 (90 m) |
30 m is standard for basin-scale flow accumulation; 90 m smooths channel networks and underestimates drainage density |
outputFormat |
GTiff (only option via this endpoint) |
Returns a single-band int16 GeoTIFF in EPSG:4326 with EGM96 vertical datum |
API_Key |
32-character alphanumeric string | Required; absent key returns HTTP 401; invalid key returns 403 |
timeout (requests) |
Integer seconds; 120 recommended | Large bbox tiles can exceed 400 MB and take 60+ s on slow connections |
max_retries |
2–5; default 3 | Higher values tolerate shared-API environments but increase total wall time |
Worked example: output interpretation
A successful download and validation log looks like this:
INFO: Requesting SRTMGL1 tile for bbox=(34.5, 35.2, -118.5, -117.8)
INFO: Wrote 47382110 bytes to data/raw/srtm/srtm_SRTMGL1_-118.5_-117.8_34.5_35.2.tif
INFO: Validated srtm_SRTMGL1_-118.5_-117.8_34.5_35.2.tif | shape=(2520, 2520) | CRS=EPSG:4326 | nodata=-32768
Key values to verify:
- shape: For a 0.7° × 0.7° bbox at SRTMGL1 resolution, expect roughly 2,520 × 2,520 pixels. Shapes much smaller than expected indicate the bbox clipped the edge of SRTM coverage.
- CRS = EPSG:4326: Raw SRTM is delivered in geographic coordinates (degrees), not meters. Before running flow routing algorithms or computing slope, reproject to a metric CRS with
rasterio.warp.reproject. - nodata = -32768: SRTM void pixels carry this sentinel value. They appear as gaps over water bodies, radar-shadow zones in steep terrain, and locations outside the 56°S–60°N acquisition strip. Pass the file to a DEM pit filling algorithm after reprojection to close these voids before deriving flow direction.
- Anomaly — shape (0, 0): The raster is empty. Most common cause is a bbox that falls entirely outside SRTM coverage (polar regions, open ocean) or a malformed coordinate order.
- Anomaly — file size < 10 KB: The API returned an HTML error page rather than a GeoTIFF. Check the
response.contentbefore writing, or inspect the file with a text editor.
Gotchas and edge cases
-
Coordinate order is (south, north, west, east), not (west, south, east, north). The OpenTopography API uses a non-standard lat/lon-first order. Passing a Shapely or GeoPandas bounds tuple (which is minx, miny, maxx, maxy) without reordering silently produces a valid-looking request that returns a tile for the wrong geographic extent.
-
SRTM voids are int16 -32768, not NaN. If you apply a mask or nodata comparison using floating-point NaN before reprojecting, the void pixels will slip through and corrupt flow accumulation. Always use
src.nodatafrom the rasterio dataset metadata when masking. -
Large bounding boxes trigger server-side chunking. Requests covering more than roughly 10,000 km² may silently return a partial tile or time out before the full payload arrives. Split into ≤ 5° × 5° sub-tiles, download each independently, and merge with
rasterio.mergeorrasterio.windowsbefore conditioning. -
The
API_Keyparameter name is case-sensitive. The API rejectsapi_keyorapikeywith a 401 even when the key itself is valid. UseAPI_Keyexactly as shown. -
Re-projecting before pit filling can introduce new voids. Bilinear or cubic resampling interpolates across nodata boundaries and may deposit fractional values at void edges. Reproject with
resampling=Resampling.neareston the initial pass, apply DEM pit filling, then optionally re-smooth with a higher-quality resampling kernel if slope accuracy is critical.
Related topics
- SRTM and LiDAR Data Acquisition — parent cluster covering
earthaccess-based NASA Earthdata downloads and LiDAR point-cloud retrieval - DEM Pit Filling Algorithms — next step after download: filling SRTM voids and removing artificial sinks before flow routing
- Best Practices for Filling Sinks in High-Resolution LiDAR Data — contrast: LiDAR-specific sink-filling considerations vs. SRTM void handling
- Coordinate Reference System Alignment — how to reproject the downloaded WGS84 GeoTIFF to a metric CRS before hydrologic analysis
- Hydrology Data Preparation & DEM Processing — domain overview linking all preprocessing stages from acquisition through watershed delineation