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, lonDegrees. Required.
datumThe 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.
heightgeoid18 (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_datumFor height=ellipsoid: wgs84 or nad83 (default: datum). NAD83(2011) ellipsoid heights are 0.3–1.6 m higher than WGS 84 ones.
epochWhen 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

CoverageContiguous 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.
AccuracyEach 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.
HeightsNAD83(2011) ellipsoid heights in metres (NAVD88 + GEOID18).
DatumCoordinates 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 dataWhere 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.
UpdatesNew and updated USGS data is picked up within a day.
FormatGeographic 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.