USGS terrain for CesiumJS
3D terrain for the contiguous United States from USGS 3DEP: 1 m lidar wherever USGS has it, 10 m everywhere else. Built by Katapult Engineering for Katapult LiDAR.
Access requires an API key
Katapult LiDAR includes access. To use the terrain in your own application, contact Katapult Engineering for an API key.
Tiles are quantized-mesh 1.0 with vertex normals, and cover everything USGS has published. See the data status page for coverage against USGS and the daily update checks.
Use it
1. The client module Recommended
A small ES module (no dependencies) that reads the tile archives directly with HTTP range requests, answered from Cloudflare's cache. It is the fastest option.
import { createUsgsTerrainProvider } from "https://kpterrain.com/sdk/usgs-terrain.js";
const viewer = new Cesium.Viewer("cesiumContainer");
viewer.terrainProvider = await createUsgsTerrainProvider(Cesium, { key: "YOUR_API_KEY" });
Pass your Cesium namespace: the Cesium global, or import * as Cesium from "cesium".
Copy the file into your project if you prefer to pin a version.
2. Stock Cesium
Standard terrain URLs, for tools that only accept a terrain provider URL:
viewer.terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(
new Cesium.Resource({
url: "https://tiles.kpterrain.com/terrain/conus/",
queryParameters: { key: "YOUR_API_KEY" },
}),
{ requestVertexNormals: true });
Elevation API
Heights of the same terrain at points, with the same API key. One point per GET, up to 1,000
per POST. Full reference, datum guidance and code examples:
Elevation API docs.
https://api.kpterrain.com/v1/elevation?lat=40.1093&lon=-77.0275&datum=wgs84&key=YOUR_API_KEY
{"height": 173.486, "height_type": "geoid18", "vertical_reference": "NAVD88 (GEOID18)",
"input": {"lat": 40.1093, "lon": -77.0275, "datum": "WGS 84 (G2296)", "epoch": 2026.7618},
"sampled_at": {"lat": 40.109290684, "lon": -77.027491805, "datum": "NAD83(2011)"},
"source": "usgs-3dep-1m", "source_date": "2025-08-05", "zoom": 17}
lat, lon | Degrees. Required. |
|---|---|
datum | The datum of lat/lon, required: wgs84 or
nad83 (NAD83(2011)). Coordinates read off these tiles, Katapult LiDAR, or GIS data labelled EPSG:4326
that is really NAD83 are nad83. Raw GNSS fixes and Google Maps coordinates are wgs84.
The two differ by 1–1.7 m in CONUS; passing the wrong one samples the wrong spot. |
height | geoid18 (default): NAVD88 orthometric height, given on US land only.
egm96: height above the EGM96 geoid, the best match for Google's elevations; on land only.
ellipsoid: ellipsoid height. |
output_datum | For height=ellipsoid: wgs84 or nad83
(default: datum). NAD83(2011) ellipsoid heights are 0.3–1.6 m higher than WGS 84 ones. |
epoch | When the WGS 84 coordinates were observed: a decimal year
(2022.5) or a date (2022-07-01), 1990–2100. Default: now, right for Google and fresh GNSS. Positions
in WGS 84 drift about 2 cm a year against NAD83. |
POST https://api.kpterrain.com/v1/elevation?key=YOUR_API_KEY
{"datum": "wgs84", "height": "egm96", "points": [{"lat": 40.1093, "lon": -77.0275}, {"lat": 39.3, "lon": -116.2}]}
A batch answers results in the order given, each with its lat and
lon; a point that can't be answered gets "height": null with an error, and
the batch is still HTTP 200. Errors are JSON {"error", "detail"}: 400 invalid_parameter or
invalid_json, 404 no_terrain (or not_found for another path), 405
method_not_allowed, 413 too_many_points or body_too_large (over 1,000 points or
128 KB), 422 geoid_not_valid_here (a geoid height where that geoid doesn't apply: ask for
ellipsoid), 502 upstream (retry). A missing or revoked key gets Cloudflare's own 403 page,
not JSON, which browsers report as a network error.
source says what the height comes from: usgs-3dep-1m (with the USGS date),
usgs-3dep-10m, copernicus-glo90 or ocean. USGS's 10 m data reaches a
little into Canada and Mexico; there the answer carries "in_us": false and has no GEOID18 height. Heights follow the mesh Cesium
draws: about 4 cm RMS (9 cm at the 99th percentile) from the 1 m lidar, about 0.4 m RMS from the
10 m data. Near the edges of 1 m units and their holes the height may come from the 10 m data.
Comparing with Google: Google documents its elevations only as "relative to local mean sea level";
they appear to be EGM96, which in CONUS sits 0.6 m below to 1.7 m above NAVD88. Use
height=egm96 to compare like with like, and geoid18 for survey and engineering work.
The data
| Coverage | Contiguous US. Zooms 0–15 from the USGS 1/3 arc-second (~10 m) DEM; zooms 13–17 from USGS S1M (1 m lidar) where it exists, blended into the 10 m data at its edges. Cesium refines zoom 17 for closer views. |
|---|---|
| Accuracy | Each tile stays within half of Cesium's per-level error up to zoom 15, a quarter at zoom 16 and 10 cm at zoom 17. Neighbouring tiles share their edges exactly (no cracks), and normals come from the source data. |
| Heights | NAD83(2011) ellipsoid heights in metres (NAVD88 + GEOID18). |
| Datum | Coordinates are NAD83(2011) labelled WGS84 with no datum shift (the EPSG:9774 null convention that GDAL, PROJ, Esri, USGS services and Cesium ion use), so the terrain lines up with NAD83 lidar and survey data. Data in an ITRF frame (raw GNSS, some imagery) sits about 1–1.5 m away horizontally. |
| Beyond US data | Where USGS has no data (most of Canada and Mexico near the border, the Canadian halves of the Great Lakes) heights come from Copernicus GLO-90 (90 m); the ocean is at sea level. |
| Updates | New and updated USGS data is picked up within a day. |
| Format | Geographic TMS (EPSG:4326), quantized-mesh 1.0, oct-encoded vertex normals, gzip. Stored in
PMTiles v3 archives, listed in a Cesium layer.json. |
Attribution
Show the credit the terrain provider supplies (Cesium does so automatically). Elevation data: U.S. Geological Survey 3D Elevation Program (public domain). Outside US coverage: Copernicus DEM GLO-90 © DLR e.V. 2010–2014 and © Airbus Defence and Space GmbH 2014–2018, provided under COPERNICUS by the European Union and ESA.
For AI agents
Step-by-step instructions for coding assistants and agents are at /llms.txt.