Ski areas
Every request needs an API key; see Authentication.
The Ski add-on serves Canadian ski areas built from OpenStreetMap. It has three parts: an API of
ski areas with their elevation, run, lift, aspect and drive-time facts; the 3D geometry of each
area's lifts; and a ski overlay that draws runs and lifts on any style in the colours a skier
already reads. All three come from one immutable release, and every response names it.
This data describes where runs and lifts are mapped and the shape of the ground under them. It has no live lift, run or grooming status, and no avalanche danger, route advice or safety rating of any kind. A run being on the map does not mean it is open, groomed or within your ability. Check the ski area's own reports and, in avalanche terrain, the public avalanche bulletin before you go.
Access and pricing
Every /ski endpoint and the ski overlay sit behind the Ski add-on, SKU addon-ski, at
US$39 a month or US$390 a year. The add-on cannot be bought yet: its prices do not exist, and
the pricing page lists it as "Not available yet". Until then:
- A paid key without the add-on gets
403addon_required, namingaddon-ski. - A dev-plan key is free from a dev origin (
localhost,127.0.0.1,*.localhost,*.pages.devand*.workers.dev), so you can build the whole integration now. From a production origin it gets403dev_only, and past the month's premium calls (5,000, summed across every gated add-on) it getsusage_limit_reached.
A refusal is never billed. Neither is a 503, which is what you get, with retryable: true,
when no ski release is published or its file cannot be read: an empty country is never served
as an empty list. See Add-ons and metering for how
SKUs and capabilities relate, and Errors for the error bodies.
Areas
List areas
GET /ski/areas?province=BC&activity=downhillReturns every area in the release, ordered by id, or those matching all the filters you give:
provinceis a two-letter province or territory code:AB,BC,MB,NB,NL,NS,NT,NU,ON,PE,QC,SKorYT. Anything else is a400.activityisdownhillornordic. An area with both matches either.bboxiswest,south,east,northin degrees, and matches an area by itslocationpoint, not by its outline.qmatches names that contain it, ignoring case, accents and punctuation, somont sainte annefinds Mont-Sainte-Anne. Aqof only punctuation would match everything, so it is a400.
curl "https://api.unmap.dev/ski/areas?q=sainte-anne&key=um_live_..."
curl "https://api.unmap.dev/ski/areas?bbox=-123.5,49,-122,50.5&activity=downhill&key=um_live_..."One area
GET /ski/areas/{id}Pass an id from the list. An id that is not in the release is 404 not_found.
Lift geometry in 3D
GET /ski/areas/{id}/lifts-3dThe area's lifts as 3D geometry: stations, pylons and the cable between them, each with its
height in metres above sea level, baked at build time from the same terrain release as the
area's elevations. The payload's schema is unmap.ski-lifts-3d.v1. It is what the 3D lift
cables under Client code draw.
data is { schema, area_id, terrain_release, lifts }. Each lift has id, name, aerialway,
stations (the two ends, on the ground), pylons (each at the top of its tower) and cable, the
line between them with its sag, every point [lng, lat, z]. An area with no lifts in OSM answers
lifts: [].
The ski area record
Each response is { data, meta }: data is one area, or an array of them from the list.
{
"data": {
"id": "osm-way-123",
"name": "Example Ski Hill",
"province": "BC",
"locality": "Example",
"location": [-118.2, 50.9],
"base": [-118.19, 50.95],
"bbox": [-118.25, 50.88, -118.15, 50.97],
"activities": ["downhill", "nordic"],
"status": null,
"websites": ["https://example.com"],
"wikidata_id": null,
"elevation": { "top_m": 2225, "base_m": 512, "vertical_m": 1713 },
"runs": { "count": 75, "downhill_km": 68.4, "nordic_km": 12.1 },
"lifts": { "count": 5 },
"aspect": { "n": 0.31, "ne": 0.18, "e": 0.07, "se": 0.03, "s": 0.02, "sw": 0.05, "w": 0.14, "nw": 0.2 },
"drive": { "from": "Kelowna", "minutes": 200 },
"source": "OpenStreetMap",
"licence": "ODbL-1.0"
},
"meta": {
"attribution": "© OpenStreetMap contributors",
"data_version": "2026-09-17.c6cf3548a49a"
}
}The values above are illustrative, and meta is abridged.
| Field | What it is |
|---|---|
id | osm-way-<n>, osm-relation-<n> or osm-node-<n>, from the OpenStreetMap feature the area was built from. Stable across rebuilds while that feature exists. |
name | The name mapped in OpenStreetMap. |
province | Two-letter code, or "" when the area could not be placed. |
locality | The nearest town, from our reverse geocoder. |
location | A representative point, [lng, lat]. This is what bbox filters on. |
base | [lng, lat] of the lowest lift station, which is where the road and the car park usually are. null when the area has no lifts or runs. |
bbox | [west, south, east, north] of the area. |
activities | downhill, nordic, or both. |
status | The operating status mapped in OpenStreetMap, or null. A mapped tag, not today's conditions. |
websites | The area's websites, as mapped. |
wikidata_id | The Wikidata item, or null. |
elevation | { top_m, base_m, vertical_m } in metres, sampled from terrain release 6 along the area's lifts and runs. null when there is nothing to sample. |
runs | { count, downhill_km, nordic_km }. |
lifts | { count }. |
aspect | The share of downhill run length facing each way, n through nw, summing to about 1. |
drive | { from, minutes }: the fastest drive from the nearest census metropolitan area of 100,000 people or more, on Unmap's own routing. null when none is within reach of the routing matrix, which is the case for 27 remote areas. |
source | Where the area came from. |
licence | The licence the area is under. |
Attribution and licence
Everything here is derived from OpenStreetMap, including the computed statistics, so it is under
the Open Database License (ODbL). Show "© OpenStreetMap contributors" wherever you display the
data or the overlay; every response carries it in meta.attribution. meta also carries the
registry's source and freshness fields, plus data_version, the exact release that answered.
Cache against data_version: a release never changes once published.
The ski overlay
Add overlays=ski to a style URL. It draws on any style, and is made for winter, whose quiet
snow-white land leaves the run colours as the content. Add terrain=on to drape it over 3D
terrain. Both are documented on the Maps API page.
curl "https://api.unmap.dev/styles/winter.json?overlays=ski&terrain=on&key=um_live_..."import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
style: "winter",
overlays: ["ski"],
center: [-118.16, 50.96],
zoom: 12,
});The overlay's tiles carry four source-layers, so you can style or query them yourself:
| Source-layer | Geometry | Properties |
|---|---|---|
ski_areas | Area outlines | id, name |
ski_area_labels | Label points | id, name |
runs | Lines | piste_type (downhill or nordic), difficulty, name, pitch_avg_deg (average pitch in degrees) and aspect (the way it faces, in degrees), where the terrain could be sampled |
lifts | Lines | aerialway (the OpenStreetMap lift type), name |
Every feature also carries source and licence. The outlines draw from zoom 9, runs and lifts
from zoom 11, and run names from zoom 14.
Run colours
Downhill runs follow the North American convention:
difficulty | Drawn as |
|---|---|
novice, easy | Green circle: green |
intermediate | Blue square: blue |
advanced | Black diamond: black |
expert, freeride, extreme | Double black: black, drawn heavier |
park | Terrain park: orange |
A run mapped with no difficulty is drawn in the style's boundary colour rather than given a rating nobody mapped. Nordic trails are dashed. On a dark style the black and double black runs draw light, because black on dark land cannot be read.
These colours do not come from the style or a theme. They are a convention skiers read before the map, so a theme cannot turn a black diamond pink. They are tested to hold a 3:1 contrast against the land on every style.
Client code
SDK
@unmap/sdk exposes the three endpoints through unmap.ski:
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({ key: "um_live_..." });
const areas = await unmap.ski.areas({ province: "QC", activity: "downhill", q: "mont" });
const first = areas.data[0];
if (first) {
const area = await unmap.ski.area(first.id);
const lifts = await unmap.ski.lifts3d(first.id);
}unmap.ski.areas({ province, activity, bbox, q }) takes the same filters as the API. Each method
returns { data, meta }.
Ski resort explorer block
ski-resort-explorer is a whole page built from the registry: search and filter the areas, see
one on the map with the ski overlay, and read its facts. Install it like any other
block:
npx shadcn@latest registry add @unmap=https://registry.unmap.dev/r/{name}.json
npx shadcn@latest add @unmap/ski-resort-explorer
npx shadcn-vue@latest add https://registry.unmap.dev/r/vue/ski-resort-explorer.jsonThe first two lines are the React lane; the third is Vue and Nuxt, where shadcn-vue takes the
item URL directly. Set its lifts3d prop to draw the picked area's cables with @unmap/deck,
loaded only when an area is picked.
3D lift cables
@unmap/deck draws the lifts-3d geometry with deck.gl, interleaved in the map's own WebGL
context. It is a separate package, so a map that does not draw lifts never loads deck.gl.
import { createMap } from "@unmap/maps";
import { createDeckOverlay, deckMapOptions, fetchLifts3d, liftCablesLayer } from "@unmap/deck";
const key = "um_live_...";
const map = createMap({ key, container: "map", style: "winter", terrain: true, maplibre: deckMapOptions });
const lifts = await fetchLifts3d({ key, areaId: "osm-way-474288286" });
createDeckOverlay(map).setLayers([liftCablesLayer(lifts)]);deckMapOptions turns on antialiasing, which MapLibre leaves off and without which the cables
draw with hard edges. Heights are baked, never read from the map at render time, so if you change
the terrain exaggeration pass the same exaggeration to liftCablesLayer.
Coverage and limits
- The current release is
2026-09-17.c6cf3548a49a: OpenStreetMap as of 2026-09-17, read from the same Canada extract the routing graph is built from. It holds 226 named areas, 21,095 runs and 1,121 lifts. - It holds only what OpenStreetMap maps. In a reference sample of 112 Canadian ski areas, 94% had both runs and lifts mapped; small hills are the ones most often missing.
- Elevations come from terrain release 6. Verticals agree with the areas' published figures to within about 1%, except where the top lift station sits below the summit.
- No live lift, run or grooming status, and no avalanche danger, route advice or safety rating of any kind.
Next steps
- Maps API for the
winterstyle andterrain=on. - Layers for how overlays compose onto a style.
- Plans and limits for dev origins and metering.