Elevation API
Send one coordinate or up to a thousand and receive DEM elevation in metres, in the same order. The source, datum and model metadata change together with the active release; /coverage.json identifies it. Release 6, built on Mapterhorn, is live.
Every request needs an API key; see Authentication.
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.0719, 51.0447],
zoom: 11,
});
const [metres] = await unmap.elevation.get([-114.0719, 51.0447]);
unmap.map!.on("click", async (e) => {
const [here] = await unmap.elevation.get([e.lngLat.lng, e.lngLat.lat]);
console.log(here === null ? "no data here" : `${here} m`);
});@unmap/elevation is the standalone client; @unmap/sdk exposes the same object as
unmap.elevation. Everything below is the HTTP contract underneath both.
Elevation
GET /elevation
near(required, unlesspointsis given): a single coordinate aslng,lat, e.g.near=-114.0719,51.0447. This is the spelling every other unmap endpoint that takes an anchor uses.points(required, unlessnearis given): several coordinates in one request,|-separatedlng,latpairs, e.g.-114.0719,51.0447|-115.5708,51.1784. Up to 1000 coordinates.
Exactly one of the two. Sending both is a 400.
A batch is bounded by the request URL as well as by the coordinate count. A URL is limited to
16 KB, and the separators are percent-encoded on the wire, so roughly 700 coordinates at six
decimal places fit before the URL runs out: in practice that ceiling arrives before the 1000
cap does. Past it the request is refused at the edge with a 414, before the gateway sees it,
so there is no error envelope to read. @unmap/elevation checks the URL it built and throws
first, naming how many of your coordinates fit.
One request is one billed call whether it carries a single coordinate or all 1000, the same rule
/route and /isochrone follow: batching coordinates into one request costs nothing extra over
asking for them one at a time.
curl "https://api.unmap.dev/elevation?near=-114.0719,51.0447&key=YOUR_KEY"import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({ key: "um_live_..." });
const [metres] = await unmap.elevation.get([-114.0719, 51.0447]);With release 6 active, the HTTP response has this shape:
{
"elevations": [1046.2],
"units": "m",
"datum": "mixed",
"resolution": null,
"model": "mixed",
"sampling": {
"zoom": 12,
"tile_size": 512,
"height_increment_m": 0.5,
"interpolation": "bilinear"
},
"meta": {
"source": "Mapterhorn / underlying elevation suppliers: Global digital elevation model, publisher snapshot 0.0.13",
"source_updated_at": "2026-09-11T14:58:22.000Z",
"source_version": "Mapterhorn 0.0.13; unmap terrain release 5",
"data_version": "5",
"attribution": "© Mapterhorn and underlying elevation suppliers. Supplier notices and licences: https://mapterhorn.com/attribution"
}
}elevations: metres rounded to the nearest tenth, one entry per coordinate. Negative and zero values are valid; missing data isnull.units: always"m".datum:"mixed"for Mapterhorn,"CGVD2013"for Canadian MRDEM. The global mosaic does not convert every source to a common vertical datum.resolution:nullfor the mixed global DEM,30for MRDEM. Null here means varying native resolution, not a missing elevation. Sampling spacing is separate.model:"mixed"for Mapterhorn: some regions describe bare ground (DTM), others surface height (DSM), including vegetation or buildings. Absent on the Canadian MRDEM response.sampling: global sampling uses bilinear interpolation at zoom 12 on 512 px tiles. The 0.5 m increment describes height encoding, not measurement accuracy. Absent on the Canadian MRDEM response.meta: where the numbers came from, in the same shape Places, Transit and Road events use.attributionis the string you are required to display; the rest says how fresh the DEM is. Note that it sits beside the values rather than wrapping them: this endpoint shipped before the{ data, meta }envelope and keeps the body shape it published.
A malformed near, or entry inside points, is a 400 naming the value at fault: near=200,51
answers near: lng must be a number between -180 and 180. More than 1000 coordinates in points
is a 400 too, as is sending both near and points. Those bodies are
{ "error": "<the rule>", "code": "bad_request" }. A longitude or latitude outside WGS84 is that
400.
An unconfigured terrain archive is 404
{ "error": "terrain archive not configured", "code": "not_found" }, the same body as an
unconfigured terrain tile. An unknown, unverified or missing selected global release is 503 with code service_unavailable; it does not fall back to another DEM. A tile that will not decode is 502
{ "error": "elevation sample failed", "code": "upstream_error" }. A coordinate the archive
does not cover stays 200 with null.
What null means
null in elevations means the archive covers nothing at that coordinate. It never means sea
level, and the client does not substitute a number for a hole in the data. Check for null
before doing arithmetic on a result, the same way you would check for a missing field from any
other API: a silent zero there would read as a real measurement at the coast, which is exactly
the wrong place to be wrong.
The dataset and coverage
Relief and numeric elevation use the same immutable copy of the Mapterhorn global planet archive. It combines regional elevation products with a global Copernicus GLO-30 fallback. Native resolutions, vertical datums and terrain/surface models vary; this is not a uniform bare-earth DTM, national LiDAR or survey product. Supplier licences and credits are recorded at Attribution.
The archive covers Web Mercator, approximately 85.0511°S to 85.0511°N. Coordinates beyond that
range return null; longitude interpolation wraps across ±180°. Missing tile data also returns
null. Encoded ocean zero may be publisher fill and is not bathymetry or proof of a measured sea level.
Sources
| Field | Release 6 (Mapterhorn, live) | Canadian MRDEM (serves when TERRAIN_RELEASE is unset) |
|---|---|---|
datum | "mixed" | "CGVD2013" |
resolution | null (native source resolution varies) | 30 |
model | "mixed" (terrain and surface models) | Absent |
sampling | z12, 512 px, 0.5 m height increment, bilinear | Absent |
meta.source | Mapterhorn and underlying suppliers | NRCan MRDEM-30 |
Do not label global values as CGVD2013 or bare-ground height. Encoding quantization is not
measurement accuracy, and an archive publication or copy date is not the acquisition date of every
contributing survey. Negative values and zero remain valid; zero in the ocean may be fill.
Missing tile data and coordinates beyond approximately ±85.0511° return null; corruption
returns 502, and an unavailable selected release returns 503 without automatic source fallback.
Release 6 combines the copied Mapterhorn mosaic with an audited regional MRDEM-30 correction. Its transition collar blends the two sources; global metadata remains mixed. The correction does not make the global dataset a uniform 30 m DTM or a single vertical datum.
NRCan MRDEM-30 serves with its Canadian coverage, CGVD2013 datum, 30 m nominal cells and
extrapolation within roughly one tile of its coverage edge. /terrain/4/{z}/{x}/{y}.png and the
Canadian vector contours keep that source independently of the active DEM. Routing graphs retain
the source recorded at their build; global elevation does not make routing global.
Keep style attribution enabled and display the elevation response's attribution where you use the values. Attribution preserves the supplier catalogue alongside the Canadian MRDEM and Copernicus notices.
Limits and billing
One request is one call, whether it carries a single coordinate or a full batch of 1000 (or the roughly 700 the 16 KB URL limit leaves room for, whichever you reach first). There is no add-on: elevation is available on every plan, at the same per-call rate as the rest of the gateway.
See Errors for the shared error envelope.
Next steps
- Find the elevation is this endpoint as a live example you can copy.
- Errors for the failure contract every endpoint shares.
- Maps API for the terrain tiles that render the same model.