A private HTTP API that returns normalized US avalanche forecasts from 30 avalanche centers. Single-purpose utility for the maintainer's own apps; not a public service.
Every request fetches live from the source (the center's own API, a parsed HTML page, or the NAC public API), validates against a canonical NAPADS schema, and caches at the edge. No database, no archive.
npm install
cp .env.example .env # set AVYAPI_KEY and CONTACT_EMAIL
npm start # vercel dev on http://localhost:4000Then:
curl -H "Authorization: Bearer $AVYAPI_KEY" \
"http://localhost:4000/api/v1/forecast/KPAC?tz=America/Phoenix"API routes namespaced under /api/v1/. Auth via Authorization: Bearer ${AVYAPI_KEY} or X-API-Key (/, /browse, and /api/v1/openapi.json excepted).
| Endpoint | Description |
|---|---|
GET / |
Scalar API reference UI |
GET /browse |
Click-through web UI for browsing centers, zones, forecasts |
GET /api/v1/forecast/{center}[/{zone}][/{date}] |
Normalized forecast; requires ?tz= or X-Timezone |
GET /api/v1/centers |
All known centers with strategy and capabilities |
GET /api/v1/zones/{center} |
Zones for a center |
GET /api/v1/openapi.json |
OpenAPI 3.1 spec |
The OpenAPI spec is canonical — consult / for full schemas.
30 US avalanche centers (Phase 2 complete, Phase 3 pending), by strategy:
| Strategy | Count | Notes |
|---|---|---|
api |
1 | CAIC (avid api-proxy) |
html |
13 | Per-center HTML parsers (Phase 3) |
nac-only |
14 | Centers delegating to NAC's widget |
nac-special |
1 | CBAC (dedicated NAC endpoint) |
unsupported |
1 | UAC — WAF-blocked, returns 501 |
The original HANDOFF estimated 3 api + 15 html centers based on May 2026 research. Phase 2 probes (PRs #4–#9) found that 5 of those candidates (SAC, MWAC, BTAC, WAC, FAC) actually publish their forecasts through the NAC widget rather than via a custom API or scraper-friendly HTML — they were downgraded to nac-only. CAIC stands as the only non-NAC center in v1. See each center's tests/fixtures/{centerId}/SOURCE.md for per-center investigation notes.
See centers.yaml for the full registry.
The pattern that emerged from Phase 2:
Before assuming a center has a scrapeable forecast page:
- Fetch the registry's
forecastPagePatternURL once with our identifying UA (avyapi/{version} (+mailto:{CONTACT_EMAIL})). - Grep for
nac-widget-loader.min.jsordu6amfiq9m9h7.cloudfront.net— both indicate the center delegates to the NAC widget. If found, downgrade tonac-only(see below). - For WordPress centers, also probe
/wp-json/wp/v2/typesto look for a custom post type. If none and no advisory category has posts, it's not API-friendly. - Check
tests/fixtures/nac/map-layer.jsonto confirm NAC already carries the center's zones with names matching the registry.
If the upstream is the NAC widget, edit centers.yaml:
strategy: nac-only
apiBaseUrl: null
capabilities: { danger: true, problems: false, rose: false, discussion: false }
napadsConformance: summary-only
supportsHistory: falseThe existing NAC adapter (src/adapters/nac.ts) handles it automatically — no new TypeScript. Save a tests/fixtures/{centerId}/SOURCE.md documenting the probe findings, the redirect chains, and the in-season re-verification task.
If the center has a genuine API or HTML forecast (so far: only CAIC):
- Create
src/adapters/{centerId}.tsimplementing theAdapterinterface (src/adapters/types.ts). - Read the URL from
center.apiBaseUrl— don't hardcode it in the adapter. fetchandparseare separate so a site redesign means editing onlyparse.- Throw the typed errors from
types.ts(UpstreamBlockedError,UpstreamUnavailableError,UpstreamChangedError,HistoryUnsupportedError,NotFoundError) — the route handler maps them to the error envelope. - Always call
isChallengeBody(res.text)on a 200 response beforeJSON.parseto distinguish a CDN block from a schema break. - Register in
src/adapters/index.ts. - For full-capability centers (
capabilities.problems: true), include an "is this a real forecast?" guard like CAIC'shasForecastContent(seesrc/adapters/caic.ts) — strict-mode validation will reject forecasts with all bands at-1and empty problems. - Save a real fixture under
tests/fixtures/{centerId}/. If only off-season data is available, also synthesize an in-season fixture for the strict-mode contract test. - Write a parse test plus a contract test that runs
validateForecastin strict mode for in-season fixtures (the route runs strict for current requests).
- CAIC: refresh
tests/fixtures/caic/products-all-offseason.jsonwith a real in-season capture; populatecenters.yamlCAIC.zones(one entry perareaId, name frompublicNameor the Drupal slug); verifyaspectElevationsshape and finishmapProblemincaic.ts; retiretests/fixtures/caic/products-all-in-season-synthetic.jsonand point the strict contract test at the real fixture. - SAC, MWAC, BTAC, WAC, FAC: re-probe each center's forecast page. If any of them have moved off the NAC widget to inline forecast rendering, flip the registry strategy back to
html(orapifor MWAC/BTAC) and write a parser. - WAC season window: NAC reported WAC zones as
off_season: Falsein June 2026; revisitseasonStart/seasonEndif WAC publishes year-round travel advisories. - NAC fixtures: refresh
tests/fixtures/nac/map-layer.jsonandtests/fixtures/nac/cbac.jsonwith in-season captures so the lenient contract tests can move to strict.
See each center's tests/fixtures/{centerId}/SOURCE.md for per-center details.
See HANDOFF.md for the build brief and architectural decisions, and CLAUDE.md for conventions and common commands.
MIT. Contact the maintainer via the CONTACT_EMAIL configured for the deployment (sent in the outbound User-Agent on every upstream fetch).