Documentation
Snowpack API
Avalanche danger by aspect and elevation, the nearest reporting instrument and how far it stands from the snow you care about, the surveyed runs, lifts, buildings and forest of 62 ski areas, and a full weather report for any place in the United States. One request, one answer, paid for by the call.
These figures are counted from the baked surveys at build time, not typed in. The lift count excludes stations, pylons, goods lines and disused lines, because none of those is a machine anybody rides.
Avalanche danger by aspect and elevation, the issuing zone, active alerts in full, ski patrol lines and the distress line answer for everyone, paid or not. They are excluded from the free tier trim on the server, not hidden in the client.
Instruments
All 398 of them, by name, network, state, position, elevation, cadence, what each measures, and when it last reported. Search or filter, and the count follows.
| Station | Network | State | Elevation | Reports | Measures | Last heard |
|---|---|---|---|---|---|---|
| Loading the catalogue. | ||||||
A network code is the family of instrument, and it is part of the station id in every reading. Elevation is feet above sea level.
Mountains
All 62, with the survey's own counts and the instrument bound to each.
The id column is what you pass to /api/resort and /api/features.
| Mountain | id | Where | Base | Runs | Lifts | Buildings | Instrument | Off by |
|---|---|---|---|---|---|---|---|---|
| Loading the catalogue. | ||||||||
Runs counts every surveyed run line, named or not. Off by is the expected depth disagreement in inches between that instrument and the mountain's base, from the measured rule in The instrument network.
The first request
No account, no key, no signup. Ask for a place and read the answer.
# What is the weather doing at a named American place? curl 'https://snowpack.fyi/api/weather?place=Park%20City,%20UT'
That call answers without a key and without a payment. What comes back is the free tier: the
place, the conditions now, and any active alert in full. The week, the next forty-eight hours and
the rest of the point are the paid answer. Ask with ?pay=x402 and the price comes back
before anything is owed:
curl -i 'https://snowpack.fyi/api/weather?place=Park%20City,%20UT&pay=x402' HTTP/2 402 content-type: application/json { "accepts": [{ "scheme": "exact", "network": "eip155:8453", "maxAmountRequired": "10000", "asset": "USDC" }] }
The amount is in the token's own base units rather than dollars. USDC carries six decimals, so
10000 is one cent, which is what a weather call costs. Every endpoint in
Reference carries its own price in money on its badge.
The mountain endpoints take coordinates or a resort id. Every id in Mountains works:
curl 'https://snowpack.fyi/api/resort?id=solitude' curl 'https://snowpack.fyi/api/atlas?lat=40.6199&lon=-111.5919'
Every distance, depth and elevation in every response is in United States customary units.
There is no unit parameter and no metric mode, so a field named ft is feet everywhere
and you never have to check which system an answer came back in.
Paying per call
Payment rides on the request itself. There is nothing to sign up for and no invoice: you make the call, you are told the price, you authorize it, you make the call again with the authorization attached, and the answer comes back.
- Request the endpoint. An unpaid request for a paid answer returns
402. - Read
acceptsfrom the 402 body: the chain, the asset, and the exact amount in atomic units. - Sign the authorization with your wallet.
- Request again with the
X-PAYMENTheader. The answer returns200, and the settlement reference comes back inX-PAYMENT-RESPONSE.
Prices are published in two places that are built from the same table, so they cannot disagree:
the 402 challenge itself and the OpenAPI document, under
x-payment-info on each operation.
The runtime 402 carries atomic token units; the OpenAPI document carries decimal USD. Reading one as the other advertises a product at a millionth of its price, so check which you are holding.
API keys
Per-call payment suits an agent. A service making thousands of calls usually wants a key instead.
A key is a signed string you send as Authorization: Bearer sp1... or ?key=,
and it opens the paid tier on every endpoint without a payment step.
curl -X POST 'https://snowpack.fyi/api/x402' \ -H 'content-type: application/json' \ -d '{"buy":"api"}'
The key is shown once and is never stored by this service: what is kept is its public id, which names it and opens nothing. If you lose the key, issue another. Manage your keys, watch your usage and inspect your requests on the keys and usage page.
Anyone holding it can spend against it, so keep it server side. Its one power is spending: account sign-in takes your passcode, and a key opens the portal for nobody, including the person holding it. Usage stays with the account that issued it.
The state of the whole network in one request. Every mountain in the catalogue, named and placed, with what fell in the last twenty-four and seventy-two hours, the base depth, the temperature and the wind, both reporting instruments with their distance from the hill and their height above or below it, any reason a reading was set aside, the Snowpack lead in front of them, and the order they fall in.
It is priced under its parts on purpose. 62 resort cards bought one at a time is 62 cents; this is twenty-five, because the whole picture costs one cached sweep rather than 62, and a buyer who wants the network should not be charged as though they wanted it in pieces.
| Parameter | Type | Meaning |
|---|---|---|
| key | string | Your API key, or send it as Authorization: Bearer. key or payment |
| pay | string | x402 to be quoted the price instead of charged. |
The response
{
"units": { "snow": "inches", "elevation": "feet", "distance": "miles",
"temperature": "degrees Fahrenheit", "wind": "miles per hour" },
"coverage": { "mountains": 62, "ranked": 57, "stations": 88 },
"mountains": [ {
"id": "solitude", "name": "Solitude", "state": "Utah",
"lat": 40.6199, "lon": -111.5919, "baseFt": 7988, "summitFt": 10488,
"rank": 1, "new24hIn": 11, "new72hIn": 14, "depthIn": 41,
"lead": { "value": 11, "kind": "measured", "why": "..." },
"a": { "station": "...", "mi": 1.2, "dzFt": 800, "flags": [] },
"b": { "station": "...", "mi": 9.4, "dzFt": -300, "flags": ["31h old"] }
} ]
}
A mountain whose instruments have nothing clean to say carries "rank": null. It is
not sorted to the bottom as though it had measured zero, and no figure is invented to fill the
gap. coverage.ranked is how many of the 62 the ranking could place.
Error responses
402no key and no payment. The body says what is needed and where to get it.503the sweep underneath did not answer. The body says why, rather than returning 62 rows of nulls with a 200, which reads like a continent with no snow.
Reference
Every endpoint a buyer can call, with its parameters, what comes back, and what it does when it cannot answer. Anything not listed here is internal to the product and is not API surface.
A full weather report for any place in the United States: conditions now, forty-eight hours ahead, ten days, and any active severe weather alert in full.
| Parameter | Type | Meaning |
|---|---|---|
| place | string | Any American city, county, state or postcode. one of place or lat+lon |
| lat | number | Decimal degrees north. |
| lon | number | Decimal degrees east, negative in the Americas. |
| pay | string | x402 to be quoted the price instead of charged. |
The response
{
"place": { "name": "Park City, Utah", "lat": 40.6461, "lon": -111.498 },
"now": { "tempF": 28, "windMph": 9, "gustMph": 17, "condition": "Light snow",
"observedAt": "2026-01-15T19:00:00Z", "from": "measured" },
"hours": [ { "at": "...", "tempF": 27, "snowIn": 0.4 } ], // 48
"days": [ { "date": "...", "highF": 31, "lowF": 14, "snowIn": 6, "words": "..." } ], // 10
"alerts": [ { "event": "Winter Storm Warning", "severity": "Severe", "text": "..." } ],
"agree": { "analyses": 2, "spreadF": 1.8, "spreadSnowIn": 0.9 }
}
agree is the redundancy record: how many independent analyses answered and how far
apart they were. See Redundancy.
The free tier
A call with no key and no payment gets an answer. It comes back 200 carrying
"tier": "free", the place, and inside now:
the temperature, what it feels like, the wind chill, the heat index, the wind, the gust, the
bearing in degrees and the compass point it blows from, the condition in words and its code,
the observation time, and whether it is daylight. The units ride along with it: degrees
Fahrenheit, miles an hour, inches.
The week, the next forty-eight hours, the air, the pressure, the humidity and the reconciled
point are the paid answer. The body's more field names them, and hint
says where to get them. A full answer is marked "tier": "full", so the two are told
apart without counting fields. The free 200 also carries the offer in a
PAYMENT-REQUIRED header, so a client that did not know there was more can read the
price off the answer it already has.
Every active alert reaches a free caller in full, with its instruction word for word, and
alertsStatus rides with them: an empty list and an unreachable service look
identical to a caller and mean opposite things. Safety is never behind the paywall.
Error responses
400the place could not be resolved, or neither a place nor a coordinate pair was given.402the price, and only when it was asked for: with?pay=x402, or with no parameters at all, which is how a directory asks what this costs. A payment that was attached and could not be settled returns one too. A plain call with no key and no payment gets the free tier instead.422the coordinate is outside the United States. Use a mountain endpoint for Canadian ski areas.503no analysis answered. The body says which paths were tried, never a fabricated forecast.
One instrument, and everything it is publishing. There are 398 bound to a mountain and 73,114 reporting in the register, and every one of them is its own call at its own address.
| Parameter | Type | Meaning |
|---|---|---|
| state | string | Two letter code. required |
| family | string | One of asos, rwis, dcp, coop, cocorahs, uscrn, scan. required |
| id | string | The identifier the network publishes. required |
The three parameters are the three parts of that instrument's own page address, so
/weather/tx/station/asos/0f2 is bought at
/api/station?state=tx&family=asos&id=0F2 with nothing to translate.
The response
{
"id": "0F2", "name": "Bowie", "network": "TX_ASOS",
"at": { "lat": 33.6017, "lon": -97.7756, "elevationFt": 1101,
"county": "Montague", "standsAt": { "at": "At an airfield" } },
"rank": { "rank": 78, "of": 412, "day": "2026-09-26" },
"readings": [ { "as": "temperatureF", "what": "air temperature",
"value": 89, "unit": "F" } ],
"sky": { "code": "CLR", "says": "clear" },
"observation": "K0F2 221855Z AUTO 05006KT 10SM CLR 32/19 A3007",
"reporting": true
}
Every figure carries the words for what it is and the unit it is in. A figure the instrument did not publish is absent rather than null, so the list is what it said and not a column of dashes. Wind arrives in miles an hour: the networks publish knots and this converts once.
rank is the instrument's SP rank: its place among the stations ranked in its
own state by how reliably they report, the share of their scheduled reports received, then how
fast those reports arrive. It is the same figure printed on the instrument's page, read from
Storm Station 247's ranking, and it is
null where that ranking has not ranked the instrument.
observation is the raw report the figures were decoded from, where the network
publishes one. A caller who disagrees with a figure can read the observation behind it rather
than taking this service's word for it.
reporting is false when the network did not answer. An empty reading list and an
unreachable network look identical otherwise, and they mean opposite things.
Error responses
400a parameter is missing or malformed. A call with no parameters at all is answered with the price instead, because that is a directory asking what this costs.402payment is required. It comes after the instrument has been found, never before: nobody is charged for an instrument that is not there.404no such instrument in that state and family, or no register for that state.
Each address answers for the instrument it names. The roll of a whole state is a different
thing and is not sold here at any price; a state's list is open to read at
/weather/<state>/instruments and every instrument has its own free page.
The mountain answer for a point: avalanche danger by aspect and elevation with the zone that issued it, active alerts, the land beneath the point, and the nearest reporting instrument with the distance and elevation offset between it and you.
| Parameter | Type | Meaning |
|---|---|---|
| lat | number | Decimal degrees north. with lon |
| lon | number | Decimal degrees east. |
| tile | string | z/x/y in place of a coordinate pair. |
The avalanche rose and the alerts are in the free tier: an unpaid caller gets those in full and is trimmed on the analysis, the band work and the live station readings.
Where the snow is falling, ranked across every mountain in the catalogue and grouped by state or province. New snow over twenty-four hours, three days and seven days, base depth, water content, and the instrument each figure came from.
| Parameter | Type | Meaning |
|---|---|---|
| view | string | What the ranking is on: today, week, base, tomorrow, season or tonight (6 pm to 6 am at each mountain, with the least and most of the analyses and the next five days). Defaults to today. |
| resort | string | One mountain by its id. Its row comes back on its own, with a thirty-day history and the hour-by-hour trace of every observation the record holds for it. |
| state | string | One state or province, by name or abbreviation. |
| limit | integer | How many rows. The free tier returns the head of the list. |
One compact reading for every mountain in the catalogue, in a single request. This exists so that sweeping a region does not mean 62 round trips from a phone on a lift.
No parameters. The answer is small by design: enough to colour a map and label a marker, not enough to replace a per-mountain call.
Everything known about one ski area: its identity and coordinates, base and summit elevation, the survey's counts, its avalanche zone, its patrol line, and the instruments bound to it in order of how well each represents the snow on the hill.
| Parameter | Type | Meaning |
|---|---|---|
| id | string | A resort id from the catalogue. required |
An unknown id returns 404 with the count of ids the catalogue holds, never an empty
resort that looks like a mountain with nothing on it.
The surveyed geometry of one mountain: named runs with their difficulty, lifts with their type and stations, buildings with their height and use, forest, roads, parking, water, peaks and the permit boundary. This is what the 3D mountain is built from.
| Parameter | Type | Meaning |
|---|---|---|
| id | string | A resort id. required |
A survey runs to hundreds of kilobytes. Cache it: the geometry changes when a mountain is
re-surveyed, which is measured in months, not minutes. The response carries the survey's
builtAt and schema so you can tell when yours went stale.
Elevation for a tile, as a height grid. The resolution is the best generally available for the ground under the tile.
| Parameter | Type | Meaning |
|---|---|---|
| tile | string | z/x/y. required |
Elevation at lidar resolution where lidar has been flown, down to posts about three feet apart. Not every mountain has been flown, and the answer says which it is rather than quietly returning the coarse grid dressed as fine.
| Parameter | Type | Meaning |
|---|---|---|
| lat | number | Centre of the patch. with lon |
| lon | number | Centre of the patch. |
| spanFt | number | Width of the patch in feet. |
The response carries the source resolution and the coverage it actually had. A patch with no
lidar answers with mode saying so, and you fall back to /api/dem.
Turn a typed place into a coordinate, or a coordinate into a place name. The same lookup the product's own search uses, so a name that works here opens the same place in the app.
| Parameter | Type | Meaning |
|---|---|---|
| q | string | Free text to search for. one of q or lat+lon |
| lat | number | Reverse lookup, with lon. |
| lon | number | Reverse lookup, with lat. |
Political boundaries as GeoJSON, for drawing a map that can be read. Without them a relief map with place dots on it is a texture with confetti on it.
| Parameter | Type | Meaning |
|---|---|---|
| set | string | states or countries. required |
Ask whether a key is good, and what tier it opens. Verification is a signature check at the edge, so it costs nothing and adds no latency to your own boot.
| Parameter | Type | Meaning |
|---|---|---|
| key | string | The key to check, or send it as a bearer token. |
{ "ok": true, "tier": "api", "configured": true }
configured says whether this deployment can verify anything at all. It matters:
a key that fails on a configured service is a bad key, and a key that fails on an unconfigured one
is a service that has not been switched on, and those need completely different things from you.
A revoked key reads as ok: false here, the same as at every gate.
The instrument network
Every published snow figure traces to a physical instrument standing somewhere, and the honest question is never "what is the depth" but "what is the depth where the instrument is, and how far is that from where you are asking".
The registry holds 4,395 active sites and probes 1,263 of them for the mountains in the catalogue. 394 answered on the last sweep, and 398 are bound to a mountain and listed below with what they measure and how often.
Distance from the station to the mountain
An instrument two hundred feet below the base area and half a mile down valley is not measuring the snow on the run, and pretending otherwise is the single most common way a snow figure misleads. So every binding carries three numbers and a word, and every response that quotes a station carries them too:
| Field | Meaning |
|---|---|
| mi | Ground distance from the mountain's reference point to the instrument, in miles. |
| dzFt | How much higher the instrument stands than the mountain's base, in feet. Negative means lower. |
| missIn | The expected disagreement in inches of depth, from the measured rule below. |
| standsFor | That same expectation in words: on the mountain, close, indicative or weak. |
The rule is measured, not assumed: 0.145 inches of expected disagreement
per mile of separation and 6.416 inches per thousand feet of
elevation difference. missIn is what those two produce for a given binding. It is the
number that stops a reading being presented as though the instrument stood on the run.
standsFor is that number in words, on the thresholds the product speaks in: under two
inches of expected disagreement reads on the mountain, under six close,
under twelve indicative, and past twelve weak. It pairs with
standsAt, because a station stands at a distance and stands for a place. An
instrument's rank is a different quantity, answered by /api/network
and /api/station: its SP rank in its state by how reliably it reports. One word for
both is how a caller ends up comparing two things that were never comparable, so each has its own.
How often stations report
An hourly instrument and a monthly one are both useful and they are not interchangeable. Every station in the catalogue states which it is, and a station that has fallen quiet says how many days ago it last reported rather than being dropped from the list. A binding that has been quiet past the staleness line is not used for a live figure.
Redundancy
A weather service that publishes one analysis and calls it the answer has no way to know when it is wrong. Every weather answer asks two independent paths and publishes what they said, not just the conclusion:
agree.analysesis how many independent paths answered. Two is the target, one is reported as one.agree.spreadFis how far apart they were on temperature, in Fahrenheit.agree.spreadSnowInis the same for new snow, in inches.
A wide spread is information in its own right: it is the honest signal that the atmosphere is doing something the models disagree about, and it is exactly when a forecast should be treated with care. The spread is published as its own figure, beside the average rather than inside it.
Where a measured reading and a modelled one both exist for the same place, they are never mixed under one label. A figure marked as measured came from an instrument and carries that instrument's name and distance; a modelled figure says so.
Terrain and lidar
Mountain geometry comes from survey. Every run, lift, building, car park,
stand of forest and stretch of road in /api/features was surveyed and baked, and every
one carries what the survey knew about it, with the fields the survey left empty left empty.
Two elevation resolutions
/api/demis the general grid, available everywhere./api/hiresis lidar, down to posts about three feet apart, where it has been flown. Coverage is not universal and the answer says which you got.
The distinction matters at close range. A lift tower standing in a gully, the fall of a road across a slope, and the lip of a terrain feature are all things the coarse grid smooths away and lidar keeps.
Contents of a survey
| Collection | What each entry carries |
|---|---|
| runs | Name, difficulty, the line itself, and whether it is groomed, gladed or bumped where the survey says so. |
| lifts | Type, the rope line, its stations and towers. Stations, pylons, goods lines and disused lines are present but are not counted as lifts. |
| buildings | Footprint ring, height, levels, use, and where known a name, opening hours, telephone and website. |
| places | Named points: lodges, food, patrol, lessons, rental, with their contact details where those are verified. |
| forest, water, roads, parking, peaks, boundaries | The rest of the ground, each as surveyed geometry. |
Free safety data
This is a product decision and it is not negotiable, so it is worth stating plainly on the page a developer reads before integrating.
- The avalanche danger by aspect and elevation, and the zone that issued it, answer for everyone.
- Active alerts are returned in full, not truncated to a headline.
- Ski patrol lines and the distress line are free and are never rate limited below the point of usefulness.
The trim that separates the free tier from the paid one is applied on the server, so a free caller is never handed a response with safety fields deleted and never has to ask politely for data that was already removed. What the paid tier buys is analysis, elevation band work, live instrument readings and the surveyed geometry.
Errors and limits
| Status | What it means | What to do |
|---|---|---|
| 400 | The request is missing a parameter or one of them will not parse. | The body names the parameter. Fix and retry. |
| 402 | Payment is required for this answer. | Read accepts, authorize, retry with X-PAYMENT. |
| 404 | The id or place is outside the catalogue. | Check it against Mountains or Instruments. Do not retry unchanged. |
| 422 | The request parses but the answer is outside the coverage area. | The body says which boundary you crossed. |
| 429 | Too many requests too quickly. | Back off and retry. Cache anything whose ground truth changes in months, not minutes. |
| 503 | No upstream analysis answered. | Retry later. The route answers this rather than fabricating a figure. |
Rules the API follows
- A figure the API does not have is absent or null. It is never filled in with a plausible number.
- A modelled figure is never returned under a measured label.
- A station reading always carries the distance and the elevation difference between that station and the place in the request.
Avalanche danger and warnings
For a point on a mountain, /api/atlas answers with the rating of record for the
avalanche zone that point sits in, the zone's own name, and where a real rose is published, the
danger by aspect and by elevation band, each one as the zone issued it.
| Field | What it is |
|---|---|
| avyLevel | The numeric rating of record, 1 to 5. |
| avyLabel | The same rating in the words the issuing zone used. |
| avyZone | The zone the rating is for, named by where it is. Null where a zone arrives with no place name. |
| rose | Danger by aspect and elevation band, where the zone publishes one. Absent where it does not, never filled in. |
| alerts | Active severe weather alerts, in full text. |
A zone that publishes a single rating gives you that one rating, on every aspect it covers. Each aspect carries the rating its centre issued for it, because a rose that was computed rather than issued is a rose that says avalanche danger is low on a slope nobody assessed.
Every one of the 62 mountains is bound to its own forecast zone, office and grid point, checked when the network was last baked, so a point on the hill resolves to the right issuing office rather than to the nearest one by straight-line distance.
Ski patrol and SOS
/api/patrol answers which mountains are connected and, behind the patrol key, the open
alerts on one of them. /api/sos is the other end: a hurt person raises a position, and
the patrol room reads and acknowledges it.
The emergency number reaches a dispatcher who can send an ambulance. This reaches the patrol who can reach a person on a run. If you are building on it, say that distinction where your user can read it.
Patrol coverage: 54 of 62 mountains
The patrol register covers 54 of the 62 mountains. The other eight have no
verified line, and /api/patrol says so for them rather than returning an empty list that
reads like a quiet day. A screen that promises help nobody is watching for is worse than a screen
that says this mountain is not connected.
Both endpoints are free, and are never rate limited below the point of usefulness.
Peaks, roads, villages and businesses
Everything below comes back inside /api/features for one mountain. The counts are
across all 62 surveys, and they are what the bake actually holds, counted.
| Collection | Across 62 surveys | What each entry carries |
|---|---|---|
| peaks | 734 | 641 of them named, with elevation in feet. A summit you can label on a map. |
| roads | 14,484 | 5,394 of them named, with their surface and class. |
| buildings | 24,520 | Footprint, height, levels and use. Where verified: name, opening hours, telephone and website. |
| places | 3,189 | Named points a skier looks for: lodges, food, rental, lessons, patrol. |
| parking | 1,626 | Car parks as areas, not as pins. |
| forest | 17,193 | Stands of trees as surveyed areas, which is what makes a glade a glade. |
| water | 3,347 | Creeks, ponds and reservoirs, which is where the melt goes. |
| street | 6,942 | Village furniture: crossings, lighting, benches, signals. |
| boundaries | 118 | Permit rings, which is how a mountain knows which runs are its own. |
A telephone number, an opening time or a website appears on a place only when a check has confirmed it against the business's own page. Everything unverified is absent. The check runs monthly, and what it could not confirm stays missing rather than going stale in public.
Maps, 3D and the visor
Worth being exact about what is an endpoint and what is a thing you build with endpoints, because the difference decides what you can buy today.
| Capability | How you get it |
|---|---|
| Political boundaries for a map | /api/boundaries |
| Terrain height | /api/dem, /api/hires |
| The mountain's geometry | /api/features |
| A rendered 3D mountain | Built from the three above. The renderer is the product it serves: what the API sells is the surveyed ground and the height under it, which is everything the render is made of. |
| First person, headset and glasses | The same. The Snowpack client walks the terrain these endpoints return; yours can too. |
| Position on the hill | Your own device. A rider's raw coordinates stay on the handset they came from; the API has no route that accepts them. |
There is no predictive model endpoint, because there is no predictive model in the product. What
a forecast answer carries is two independent analyses and the spread between them, which is
published as agree and described under Redundancy. If that
changes it will appear here with the same detail as everything else; it will not be implied before
it exists.
Payment protocol
The short version is under Paying per call. This is the whole exchange, for an agent implementing it without a library.
curl -i 'https://snowpack.fyi/api/weather?place=Alta,%20UT&pay=x402' HTTP/2 402 { "x402Version": 1, "accepts": [{ "scheme": "exact", "network": "eip155:8453", "asset": "USDC", "maxAmountRequired": "10000", // ATOMIC units, not dollars "payTo": "0x...", "resource": "https://snowpack.fyi/api/weather?place=Alta,%20UT" }] }
curl -i 'https://snowpack.fyi/api/weather?place=Alta,%20UT' \ -H 'X-PAYMENT: <the signed authorization>' HTTP/2 200 x-payment-response: <settlement reference> { "place": { ... }, "now": { ... }, "days": [ ... ] }
Three details to check
- Atomic against decimal. The 402 quotes atomic token units; the OpenAPI document quotes decimal USD. Reading one as the other is a factor of a million, in one direction or the other.
- The resource is the exact URL. An authorization signed for one query string does not pay for another. Quote and pay for the same request, character for character.
- A free endpoint never answers 402. If you get one from
/api/conditions,/api/geocode,/api/boundariesor/api/license, that is a fault in this service and worth reporting.
Every individually priced answer is walkable: GET /api/x402?offset=&limit= pages
the catalogue, ?resource=<id> fetches one, ?resort=<id> filters
to a mountain.
Examples
Three whole tasks, start to finish. Each one runs as written.
A map of every mountain, coloured by new snow
One free call, no key. This is the pattern behind a regional overview: ask once, draw 62 markers, and only ask a mountain for detail when somebody presses it.
const r = await fetch('https://snowpack.fyi/api/conditions'); const { resorts } = await r.json(); // Inches of new snow decide the colour. Sort so the leader is first. const ranked = resorts .filter(m => m.newIn != null) .sort((a, b) => b.newIn - a.newIn); for (const m of ranked.slice(0, 5)) console.log(`${m.name.padEnd(22)} ${String(m.newIn).padStart(4)}" new`);
Avalanche danger, without paying
The avalanche rating, its zone and any active alert are free. Note what the code does when the rose is absent: it says so, rather than assuming the danger is low.
const at = async (lat, lon) => { const r = await fetch(`https://snowpack.fyi/api/atlas?lat=${lat}&lon=${lon}`); if (!r.ok) throw new Error(`atlas answered ${r.status}`); return r.json(); }; const a = await at(40.6197, -111.5919); // Solitude console.log(a.avyZone ?? 'no zone covers this point'); console.log(a.avyLevel ? `${a.avyLevel} (${a.avyLabel})` : 'no rating of record'); // A missing rose is a real answer. Do not fill it in. if (a.rose) for (const [aspect, band] of Object.entries(a.rose)) console.log(` ${aspect}: ${band.join(' / ')}`); else console.log(' this zone publishes a rating alone'); for (const alert of a.alerts ?? []) console.log(`ALERT ${alert.event}: ${alert.severity}`);
The runs of a mountain, cached
Geometry changes when a mountain is re-surveyed, which is months apart. Asking for it on every page load is the most common way to make an integration slow and expensive at once.
import { readFile, writeFile } from 'node:fs/promises'; async function survey(id, key){ const cache = `./cache-${id}.json`; try{ const held = JSON.parse(await readFile(cache, 'utf8')); // builtAt is the survey's own stamp. Re-fetch monthly, not hourly. if (Date.now() - new Date(held.builtAt) < 30 * 864e5) return held; }catch{ /* no cache yet, fall through and fetch */ } const r = await fetch(`https://snowpack.fyi/api/features?id=${id}`, { headers: { authorization: `Bearer ${key}` } }); if (!r.ok) throw new Error(`features answered ${r.status}`); const doc = await r.json(); await writeFile(cache, JSON.stringify(doc)); return doc; } const doc = await survey('alta', process.env.SNOWPACK_KEY); const named = doc.runs.filter(r => r.name); console.log(`${named.length} named runs, ${doc.lifts.length} lift structures`);
The example reads it from the environment for a reason. A key in front-end code is a key anybody can read and spend.