snowpack
Sign in

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.

62
mountains surveyed
398
instruments read
11,272
runs in the surveys
1,042
lifts you can ride
24,520
buildings placed

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.

Safety is never behind the paywall.

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.

loading
StationNetworkStateElevationReportsMeasuresLast 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.

loading
MountainidWhereBaseRunsLiftsBuildingsInstrumentOff 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.

Request
# 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:

Ask the price first
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:

A mountain, by id
curl 'https://snowpack.fyi/api/resort?id=solitude'
curl 'https://snowpack.fyi/api/atlas?lat=40.6199&lon=-111.5919'
Units are feet, inches and miles.

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.

  1. Request the endpoint. An unpaid request for a paid answer returns 402.
  2. Read accepts from the 402 body: the chain, the asset, and the exact amount in atomic units.
  3. Sign the authorization with your wallet.
  4. Request again with the X-PAYMENT header. The answer returns 200, and the settlement reference comes back in X-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.

Amounts are atomic in the challenge and decimal in the document.

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.

Buy a key with a wallet
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.

A key is a bearer credential.

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.

GET/api/fleet$0.25

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.

ParameterTypeMeaning
keystringYour API key, or send it as Authorization: Bearer. key or payment
paystringx402 to be quoted the price instead of charged.

The response

Shape
{
  "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

  • 402 no key and no payment. The body says what is needed and where to get it.
  • 503 the 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.

GET/api/weather$0.01

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.

ParameterTypeMeaning
placestringAny American city, county, state or postcode. one of place or lat+lon
latnumberDecimal degrees north.
lonnumberDecimal degrees east, negative in the Americas.
paystringx402 to be quoted the price instead of charged.

The response

Shape
{
  "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

  • 400 the place could not be resolved, or neither a place nor a coordinate pair was given.
  • 402 the 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.
  • 422 the coordinate is outside the United States. Use a mountain endpoint for Canadian ski areas.
  • 503 no analysis answered. The body says which paths were tried, never a fabricated forecast.
GET/api/station$0.01

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.

ParameterTypeMeaning
statestringTwo letter code. required
familystringOne of asos, rwis, dcp, coop, cocorahs, uscrn, scan. required
idstringThe 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

Shape
{
  "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

  • 400 a 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.
  • 402 payment is required. It comes after the instrument has been found, never before: nobody is charged for an instrument that is not there.
  • 404 no such instrument in that state and family, or no register for that state.
This is one instrument, and there is no call that returns them all.

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.

GET/api/atlas$0.02

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.

ParameterTypeMeaning
latnumberDecimal degrees north. with lon
lonnumberDecimal degrees east.
tilestringz/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.

GET/api/snow$0.01

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.

ParameterTypeMeaning
viewstringWhat 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.
resortstringOne 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.
statestringOne state or province, by name or abbreviation.
limitintegerHow many rows. The free tier returns the head of the list.
GET/api/conditionsfree

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.

GET/api/resort$0.01

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.

ParameterTypeMeaning
idstringA 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.

GET/api/featuresfree

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.

ParameterTypeMeaning
idstringA resort id. required
This is the large one.

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.

GET/api/demfree

Elevation for a tile, as a height grid. The resolution is the best generally available for the ground under the tile.

ParameterTypeMeaning
tilestringz/x/y. required
GET/api/hires$0.01

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.

ParameterTypeMeaning
latnumberCentre of the patch. with lon
lonnumberCentre of the patch.
spanFtnumberWidth 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.

GET/api/geocodefree

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.

ParameterTypeMeaning
qstringFree text to search for. one of q or lat+lon
latnumberReverse lookup, with lon.
lonnumberReverse lookup, with lat.
GET/api/boundariesfree

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.

ParameterTypeMeaning
setstringstates or countries. required
GET/api/licensefree

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.

ParameterTypeMeaning
keystringThe key to check, or send it as a bearer token.
Answer
{ "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:

FieldMeaning
miGround distance from the mountain's reference point to the instrument, in miles.
dzFtHow much higher the instrument stands than the mountain's base, in feet. Negative means lower.
missInThe expected disagreement in inches of depth, from the measured rule below.
standsForThat 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.analyses is how many independent paths answered. Two is the target, one is reported as one.
  • agree.spreadF is how far apart they were on temperature, in Fahrenheit.
  • agree.spreadSnowIn is 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/dem is the general grid, available everywhere.
  • /api/hires is 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

CollectionWhat each entry carries
runsName, difficulty, the line itself, and whether it is groomed, gladed or bumped where the survey says so.
liftsType, the rope line, its stations and towers. Stations, pylons, goods lines and disused lines are present but are not counted as lifts.
buildingsFootprint ring, height, levels, use, and where known a name, opening hours, telephone and website.
placesNamed points: lodges, food, patrol, lessons, rental, with their contact details where those are verified.
forest, water, roads, parking, peaks, boundariesThe 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

StatusWhat it meansWhat to do
400The request is missing a parameter or one of them will not parse.The body names the parameter. Fix and retry.
402Payment is required for this answer.Read accepts, authorize, retry with X-PAYMENT.
404The id or place is outside the catalogue.Check it against Mountains or Instruments. Do not retry unchanged.
422The request parses but the answer is outside the coverage area.The body says which boundary you crossed.
429Too many requests too quickly.Back off and retry. Cache anything whose ground truth changes in months, not minutes.
503No 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.

FieldWhat it is
avyLevelThe numeric rating of record, 1 to 5.
avyLabelThe same rating in the words the issuing zone used.
avyZoneThe zone the rating is for, named by where it is. Null where a zone arrives with no place name.
roseDanger by aspect and elevation band, where the zone publishes one. Absent where it does not, never filled in.
alertsActive severe weather alerts, in full text.
All of it is free, and every rose is the one its centre issued.

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 remains the call for an emergency, and this control is styled so the two stay distinct.

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.

CollectionAcross 62 surveysWhat each entry carries
peaks734 641 of them named, with elevation in feet. A summit you can label on a map.
roads14,484 5,394 of them named, with their surface and class.
buildings24,520 Footprint, height, levels and use. Where verified: name, opening hours, telephone and website.
places3,189 Named points a skier looks for: lodges, food, rental, lessons, patrol.
parking1,626 Car parks as areas, not as pins.
forest17,193 Stands of trees as surveyed areas, which is what makes a glade a glade.
water3,347 Creeks, ponds and reservoirs, which is where the melt goes.
street6,942 Village furniture: crossings, lighting, benches, signals.
boundaries118 Permit rings, which is how a mountain knows which runs are its own.
Contact detail is verified, not scraped and hoped over.

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.

CapabilityHow 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 mountainBuilt 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 glassesThe same. The Snowpack client walks the terrain these endpoints return; yours can too.
Position on the hillYour own device. A rider's raw coordinates stay on the handset they came from; the API has no route that accepts them.
What the API does not sell, said plainly.

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.

1. Ask, and be quoted
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"
  }]
}
2. Authorize, and ask again
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/boundaries or /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.

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

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

Node
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`);
Keep the key on your server.

The example reads it from the environment for a reason. A key in front-end code is a key anybody can read and spend.