Skip to content

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.

Terrain facts, not safety advice

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 403 addon_required, naming addon-ski.
  • A dev-plan key is free from a dev origin (localhost, 127.0.0.1, *.localhost, *.pages.dev and *.workers.dev), so you can build the whole integration now. From a production origin it gets 403 dev_only, and past the month's premium calls (5,000, summed across every gated add-on) it gets usage_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=downhill

Returns every area in the release, ordered by id, or those matching all the filters you give:

  • province is a two-letter province or territory code: AB, BC, MB, NB, NL, NS, NT, NU, ON, PE, QC, SK or YT. Anything else is a 400.
  • activity is downhill or nordic. An area with both matches either.
  • bbox is west,south,east,north in degrees, and matches an area by its location point, not by its outline.
  • q matches names that contain it, ignoring case, accents and punctuation, so mont sainte anne finds Mont-Sainte-Anne. A q of only punctuation would match everything, so it is a 400.
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-3d

The 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.

FieldWhat it is
idosm-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.
nameThe name mapped in OpenStreetMap.
provinceTwo-letter code, or "" when the area could not be placed.
localityThe nearest town, from our reverse geocoder.
locationA 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.
activitiesdownhill, nordic, or both.
statusThe operating status mapped in OpenStreetMap, or null. A mapped tag, not today's conditions.
websitesThe area's websites, as mapped.
wikidata_idThe 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 }.
aspectThe 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.
sourceWhere the area came from.
licenceThe 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-layerGeometryProperties
ski_areasArea outlinesid, name
ski_area_labelsLabel pointsid, name
runsLinespiste_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
liftsLinesaerialway (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:

difficultyDrawn as
novice, easyGreen circle: green
intermediateBlue square: blue
advancedBlack diamond: black
expert, freeride, extremeDouble black: black, drawn heavier
parkTerrain 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.json

The 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