Ski Run Scraper API

Daily grooming, lift status, snow report data, and real-time lift wait times for 60+ ski resorts across North America and Australia. Grooming/snow is fetched once each morning in the resort's local window; lift wait times run separately every few minutes. Data comes from two providers: Vail Resorts (Puppeteer) and Inspector/Ikon Pass resorts (HTTP API, normalized to the same schema).

Freshness & cadence:

Browse Resorts

View interactive reports with date navigation, historical comparisons, and weather forecasts:

Colorado (Mountain Time)

Vail

Grooming Report

Beaver Creek

Grooming Report

Breckenridge

Grooming Report

Keystone

Grooming Report

Crested Butte

Grooming Report

Copper Mountain

Grooming Report

Steamboat

Grooming Report

Arapahoe Basin

Grooming Report

Aspen Highlands

Grooming Report

Aspen Mountain

Grooming Report

Buttermilk

Grooming Report

Utah (Mountain Time)

Park City

Grooming Report

Alta

Grooming Report

Snowbird

Grooming Report

Deer Valley

Grooming Report

Solitude

Grooming Report

Wyoming / Montana / New Mexico (Mountain Time)

Jackson Hole

Grooming Report

Big Sky

Grooming Report

Taos

Grooming Report

California

Heavenly

Grooming Report

Northstar

Grooming Report

Kirkwood

Grooming Report

Palisades Tahoe

Grooming Report

Mammoth Mountain

Grooming Report

June Mountain

Grooming Report

Pacific Northwest

Stevens Pass

Grooming Report

Crystal Mountain

Grooming Report

Vermont

Stowe

Grooming Report

Okemo

Grooming Report

Mount Snow

Grooming Report

Stratton

Grooming Report

Killington

Grooming Report

Sugarbush

Grooming Report

New Hampshire / Maine

Attitash

Grooming Report

Wildcat

Grooming Report

Mount Sunapee

Grooming Report

Crotched

Grooming Report

Loon Mountain

Grooming Report

Sugarloaf

Grooming Report

Sunday River

Grooming Report

New York

Hunter Mountain

Grooming Report

Mid-Atlantic

Liberty

Grooming Report

Roundtop

Grooming Report

Whitetail

Grooming Report

Jack Frost

Grooming Report

Big Boulder

Grooming Report

Hidden Valley PA

Grooming Report

Laurel Mountain

Grooming Report

Seven Springs

Grooming Report

Midwest

Paoli Peaks

Grooming Report

Boston Mills

Grooming Report

Brandywine

Grooming Report

Mad River

Grooming Report

Alpine Valley

Grooming Report

Wilmot Mountain

Grooming Report

Afton Alps

Grooming Report

Hidden Valley MO

Grooming Report

Mt Brighton

Grooming Report

Canada - British Columbia

Whistler Blackcomb

Grooming Report

Revelstoke

Grooming Report

Cypress Mountain

Grooming Report

Canada - Alberta

Banff Sunshine

Grooming Report

Lake Louise

Grooming Report

Canada - Quebec / Ontario

Tremblant

Grooming Report

Blue Mountain

Grooming Report

Australia

Perisher

Grooming Report

Falls Creek

Grooming Report

Hotham

Grooming Report

API Endpoints

Data Index (File Manifest)

GET /data/index.json

Returns a manifest of all available data files for all resorts, including file counts and latest dates.

https://jacobschulman.github.io/ski-run-scraper/data/index.json

Latest Snow Reports (All Resorts)

GET /data/latest-snow.json

Returns the most recent snow report data for all resorts with snow reporting enabled.

https://jacobschulman.github.io/ski-run-scraper/data/latest-snow.json

Latest Lift Status (All Resorts)

GET /data/latest-lifts.json

Returns the most recent lift status, wait times, and counts for all resorts with lift data. Updated every 5 minutes during operating hours.

https://jacobschulman.github.io/ski-run-scraper/data/latest-lifts.json
Available for: All Vail resorts plus Inspector/Ikon resorts with lift data (see config.json for the active list). More resorts are added as lift feeds come online.

Resort Terrain/Grooming Data (by date)

GET /data/{resort}/terrain/{YYYY-MM-DD}.json

Returns terrain, grooming, and lift data for a specific resort on a specific date.

https://jacobschulman.github.io/ski-run-scraper/data/keystone/terrain/2025-11-18.json
https://jacobschulman.github.io/ski-run-scraper/data/vail/terrain/2025-11-18.json

Resort Terrain Index

GET /data/{resort}/terrain/index.json

Per-resort manifest of available terrain files (dates, latest, counts). Generated automatically after each scrape.

https://jacobschulman.github.io/ski-run-scraper/data/stratton/terrain/index.json
https://jacobschulman.github.io/ski-run-scraper/data/breckenridge/terrain/index.json

Resort Snow Data (by date)

GET /data/{resort}/snow/{YYYY-MM-DD}.json

Returns snow report data for a specific resort on a specific date.

https://jacobschulman.github.io/ski-run-scraper/data/vail/snow/2025-11-18.json
https://jacobschulman.github.io/ski-run-scraper/data/beavercreek/snow/2025-11-18.json

Latest Resort Snow Data

GET /data/{resort}/snow/latest.json

Returns the most recent snow data for a specific resort.

https://jacobschulman.github.io/ski-run-scraper/data/vail/snow/latest.json

Morning Brief (by date)

GET /data/{resort}/brief/{YYYY-MM-DD}.json

Returns a comprehensive morning brief combining snow conditions, terrain changes, lift wait time analysis, weather forecast, and AI-generated insights for a specific date. Perfect for daily ski planning and conditions overview.

https://jacobschulman.github.io/ski-run-scraper/data/vail/brief/2025-12-07.json
https://jacobschulman.github.io/ski-run-scraper/data/breckenridge/brief/2025-12-07.json

Latest Morning Brief

GET /data/{resort}/brief/latest.json

Returns the most recent morning brief for a specific resort.

https://jacobschulman.github.io/ski-run-scraper/data/vail/brief/latest.json

Morning Brief Index

GET /data/{resort}/brief/index.json

Per-resort manifest of available brief files (dates, latest, counts).

https://jacobschulman.github.io/ski-run-scraper/data/vail/brief/index.json

Real-Time Lift Wait Times (NDJSON)

GET /data/{resort}/lifts/{YYYY-MM-DD}.ndjson

Returns real-time lift wait-time data captured every few minutes during operating hours. Data is stored in NDJSON format (newline-delimited JSON), with one lift snapshot per line. Each line represents the status of one lift at a specific moment in time. Inspector/Ikon resorts include extra metadata (elevation, length, first tracks, etc.).

https://jacobschulman.github.io/ski-run-scraper/data/vail/lifts/2025-11-21.ndjson
https://jacobschulman.github.io/ski-run-scraper/data/keystone/lifts/2025-11-21.ndjson
Note: Lift data is only recorded during lift operating hours (automatically detected from lift schedules). Outside of operating hours, no data is collected. Files may be large for busy days with many lifts.

Lift Status Overview (Index)

GET /data/{resort}/lifts/index.json

Returns a summary of all lifts for a resort with their latest status, average wait times, and historical statistics. Generated from real-time lift data.

https://jacobschulman.github.io/ski-run-scraper/data/vail/lifts/index.json
https://jacobschulman.github.io/ski-run-scraper/data/breckenridge/lifts/index.json
Available for: Vail, Breckenridge, Keystone, Heavenly, Northstar, Stowe, Mount Snow, Hunter Mountain, Whistler Blackcomb (more resorts added as lift data becomes available)

Individual Lift Details

GET /data/{resort}/lifts/data/{lift-slug}.json

Returns detailed history, statistics, and wait-time patterns for a specific lift. Includes current status, daily averages, peak times, and complete history.

https://jacobschulman.github.io/ski-run-scraper/data/vail/lifts/data/gondola-one.json
https://jacobschulman.github.io/ski-run-scraper/data/keystone/lifts/data/river-run-gondola.json

Interactive Web Pages

View formatted reports with date navigation and historical comparisons:

Data Structure

Terrain/Grooming Data

Each daily terrain file contains complete resort status:

{
  "Date": "2025-11-19T08:20:52.616Z",
  "ResortId": 15,
  "GroomingAreas": [
    {
      "Id": 336,
      "Name": "Blue Sky Basin",
      "order": 0,
      "Trails": [
        {
          "Id": 4900,
          "Name": "Big Rock Park - East",
          "Difficulty": "Blue",
          "IsOpen": false,
          "IsGroomed": false,
          "TrailInfo": "",
          "TrailLength": "0 (ft)",
          "TrailType": "Skiing",
          "IsTrailWork": false
        }
      ],
      "Lifts": [
        {
          "Id": 1,
          "Name": "Skyline Express",
          "Status": "Closed",
          "IsOpen": false
        }
      ]
    }
  ]
}

Trail Object Fields

Field Type Description
Id number Unique trail identifier
Name string Trail name
Difficulty string "Green", "Blue", "Black", or "Double Black"
IsOpen boolean Whether the trail is currently open
IsGroomed boolean Whether the trail was groomed today
TrailInfo string Additional trail information (often empty)
TrailLength string Trail length (e.g., "3.5 miles" or "0 (ft)")
TrailType string "Skiing" or "Snowboarding"
IsTrailWork boolean Whether trail work is in progress

Lift Object Fields

Field Type Description
Id number Unique lift identifier
Name string Lift name
Status string "Open", "Closed", or "Scheduled"
IsOpen boolean Whether the lift is currently operating

Snow Report Data

Each daily snow file contains conditions and forecasts:

{
  "resort": "vail",
  "resortName": "Vail",
  "date": "2025-11-19",
  "timestamp": "2025-11-19T15:23:40.113Z",
  "lastUpdated": "Updated November 19, 2025 at 5:02 AM MST",
  "conditions": "Snow Groomed",
  "snowfall": {
    "overnight_inches": 0,
    "overnight_cm": 0,
    "24hour_inches": 0,
    "24hour_cm": 0,
    "48hour_inches": 2,
    "48hour_cm": 5,
    "7day_inches": 5,
    "7day_cm": 12,
    "season_total_inches": 8,
    "season_total_cm": 20
  },
  "baseDepth": {
    "inches": 18,
    "cm": 45
  },
  "forecast": {
    "locations": [
      {
        "name": "Unknown",
        "elevation": null,
        "today": {
          "high_f": 43,
          "high_c": 6,
          "low_f": 26,
          "low_c": -4,
          "description": "Partly Cloudy",
          "wind": "SSW",
          "wind_speed": 3.69,
          "snowfall_day_inches": 0,
          "snowfall_night_inches": 0
        },
        "forecast_days": [
          {
            "date": "2025-11-20T07:00:00Z",
            "high_f": 44,
            "high_c": 7,
            "low_f": 29,
            "low_c": -2,
            "description": "Cloudy",
            "snowfall_day_inches": 0,
            "snowfall_night_inches": 1
          }
        ]
      }
    ]
  }
}

Snow Report Fields

Field Type Description
resort string Resort key (e.g., "vail")
resortName string Display name (e.g., "Vail")
provider string Data provider: "vail" (Vail Resorts, scraped via Puppeteer) or "ikon" (Ikon Pass/Inspector API resorts)
date string Date in YYYY-MM-DD format
timestamp string ISO 8601 timestamp of when data was scraped
lastUpdated string Human-readable update time from resort
conditions string Overall conditions (e.g., "Snow Groomed", "Powder")

Forecast Location Fields

Field Type Description
high_f / high_c number High temperature in Fahrenheit/Celsius
low_f / low_c number|null Low temperature in Fahrenheit/Celsius
description string Weather description (e.g., "Partly Cloudy")
wind string Wind direction (e.g., "SSW")
wind_speed number Wind speed in mph
snowfall_day_inches number Expected daytime snowfall in inches
snowfall_night_inches number Expected nighttime snowfall in inches
date string ISO 8601 date for forecast day

Real-Time Lift Wait-Time Data (NDJSON)

Lift wait times are captured every 5 minutes during operating hours and stored in NDJSON (newline-delimited JSON) format. Each line is a complete JSON object representing one lift's status at a specific moment:

{
  "timestamp": "2025-11-21T18:03:41.360Z",
  "localTime": "11:03:41",
  "resort": "vail",
  "liftId": "3",
  "name": "Gondola One",
  "status": "Open",
  "type": "gondola",
  "waitMinutes": 2,
  "capacity": 10,
  "mountain": "Vail Village",
  "openTime": "09:00",
  "closeTime": "15:15"
}

Lift Record Fields

Field Type Description
timestamp string ISO 8601 timestamp (UTC) when data was captured
localTime string Time in resort's local timezone (HH:mm:ss format)
resort string Resort key (e.g., "vail", "keystone")
liftId string|null Unique identifier for the lift
name string Lift name
status string "Open", "Closed", "Scheduled", or "Hold"
type string Lift type (e.g., "gondola", "quad", "six-pack", "surface")
waitMinutes number|null Current wait time in minutes (null if not available)
capacity number Number of people per chair/cabin
mountain string Mountain area or base location
openTime string Scheduled opening time (HH:mm format, local time)
closeTime string Scheduled closing time (HH:mm format, local time)
NDJSON Format: Files contain one JSON object per line, making them efficient for streaming and time-series analysis. To parse, read the file line by line and parse each line as separate JSON. Perfect for building heatmaps, analyzing wait-time patterns, and tracking historical lift performance.

Morning Brief Data

Morning briefs provide a comprehensive daily summary combining all data sources with AI-generated insights. Perfect for daily planning notifications, chatbots, or dashboards:

{
  "resort": "vail",
  "resortName": "Vail",
  "date": "2025-12-07",
  "generated": "2025-12-07T12:34:16.626Z",
  "rawData": {
    "snow": {
      "overnight_inches": 0,
      "overnight_cm": 0,
      "24hour_inches": 7,
      "24hour_cm": 17,
      "48hour_inches": 9,
      "48hour_cm": 22,
      "7day_inches": 27,
      "7day_cm": 68,
      "season_total_inches": 38,
      "season_total_cm": 96,
      "conditions": {
        "today": "Powder",
        "yesterday": "Powder"
      }
    },
    "terrain": {
      "stats": {
        "openTrails": { "today": 34, "yesterday": 16, "delta": 18 },
        "groomedTrails": { "today": 23, "yesterday": 15, "delta": 8 },
        "totalTrails": 277
      },
      "newlyOpened": [
        { "name": "Columbine - Upper", "difficulty": "Blue", "area": "Lionshead" }
      ],
      "newlyClosed": [],
      "newlyGroomed": [
        { "name": "Columbine - Upper", "difficulty": "Blue", "area": "Lionshead" }
      ],
      "ungroomed": []
    },
    "lifts": {
      "available": true,
      "yesterday": {
        "date": "2025-12-06",
        "avgWaitTime": 3.7,
        "maxWaitTime": 11,
        "totalLiftsTracked": 7,
        "busiest": [
          { "name": "Mountaintop Express #4", "avgWait": 5.5, "peakWait": 11, "type": "six", "mountain": "Mid-Vail" }
        ]
      }
    },
    "forecast": {
      "today": {
        "date": "2025-12-07",
        "high_f": 26, "low_f": 17,
        "high_c": -3, "low_c": -8,
        "description": "Cloudy",
        "snowfall_day_inches": 0,
        "snowfall_night_inches": 0
      },
      "tomorrow": { ... },
      "outlook": [ ... ]
    }
  },
  "computedInsights": {
    "flags": {
      "hasFreshSnow": false,
      "isPowderDay": true,
      "hasNewTrails": true,
      "significantGroomingIncrease": true,
      "highLiftDemand": false
    },
    "alerts": [
      "7\" in last 24 hours - powder day!",
      "18 new trails opened today",
      "8 trails freshly groomed"
    ],
    "trends": [
      "Great week for snow - 27\" in last 7 days",
      "Grooming increased 53% from yesterday",
      "18 more trails open than yesterday"
    ],
    "recommendations": [
      "Fresh corduroy on Columbine - Upper - perfect for intermediate skiers",
      "Fresh corduroy on Coyote Crossing - great for beginners"
    ]
  },
  "morningBrief": {
    "headline": "Powder Day Alert!",
    "body": "Powder alert with 7\" of fresh overnight and soft conditions across the hill. Terrain is expanding with 18 new trails opening up more room to roam. 8 runs were freshly groomed overnight, with smooth corduroy calling in Lionshead and Mid-Vail."
  },
  "errors": []
}

Morning Brief Fields

Field Type Description
resort string Resort key (e.g., "vail")
resortName string Display name (e.g., "Vail")
date string Date in YYYY-MM-DD format
generated string ISO 8601 timestamp when brief was generated
rawData.snow object Snow totals (overnight, 24hr, 48hr, 7day, season) and conditions comparison
rawData.terrain object Trail statistics with today/yesterday deltas, newly opened/closed/groomed trails
rawData.lifts object Yesterday's lift wait time summary including busiest lifts
rawData.forecast object Weather forecast for today, tomorrow, and 3-day outlook
computedInsights.flags object Boolean flags: hasFreshSnow, isPowderDay, hasNewTrails, significantGroomingIncrease, highLiftDemand
computedInsights.alerts string[] High-priority notifications (powder days, new terrain, etc.)
computedInsights.trends string[] Trend observations (weekly snow totals, grooming changes, etc.)
computedInsights.recommendations string[] Actionable suggestions based on conditions
morningBrief.headline string Short headline summarizing conditions (e.g., "Powder Day Alert!")
morningBrief.body string Natural language summary paragraph for the day
errors string[] Any errors encountered during brief generation (usually empty)
Use Cases: Morning briefs are ideal for push notifications ("Powder Day at Vail!"), chatbot integrations, daily email digests, voice assistants, and dashboard widgets. The structured computedInsights make it easy to filter for specific conditions, while morningBrief provides ready-to-use text.

Example Usage

JavaScript / Node.js

// Fetch terrain data for a specific resort and date
const response = await fetch(
  'https://jacobschulman.github.io/ski-run-scraper/data/keystone/terrain/2025-11-18.json'
);
const data = await response.json();

// Count groomed trails
const groomedTrails = data.GroomingAreas.flatMap(area =>
  area.Trails.filter(trail => trail.IsGroomed)
);
console.log(`${groomedTrails.length} trails groomed today`);

// Get open lifts
const openLifts = data.GroomingAreas.flatMap(area =>
  area.Lifts.filter(lift => lift.IsOpen)
);
console.log(`${openLifts.length} lifts operating`);

Python

import requests

# Fetch snow report data
url = 'https://jacobschulman.github.io/ski-run-scraper/data/vail/snow/latest.json'
response = requests.get(url)
data = response.json()

print(f"Conditions: {data['conditions']}")
print(f"24hr snowfall: {data['snowfall']['24hour_inches']}\"")
print(f"Base depth: {data['baseDepth']['inches']}\"")
print(f"Season total: {data['snowfall']['season_total_inches']}\"")

Get All Available Files

// Fetch the index to see all available data
const indexResponse = await fetch(
  'https://jacobschulman.github.io/ski-run-scraper/data/index.json'
);
const index = await indexResponse.json();

// List all resorts and their file counts
for (const [resort, info] of Object.entries(index.resorts)) {
  console.log(`${info.name}: ${info.count} days of data (latest: ${info.latest})`);
}

Fetch and Parse Lift Wait Times (JavaScript)

// Fetch real-time lift wait-time data (NDJSON format)
const response = await fetch(
  'https://jacobschulman.github.io/ski-run-scraper/data/vail/lifts/2025-11-21.ndjson'
);
const text = await response.text();

// Parse NDJSON (one JSON object per line)
const liftRecords = text
  .trim()
  .split('\n')
  .map(line => JSON.parse(line));

// Find lifts with wait times
const liftsWithWaits = liftRecords.filter(record =>
  record.waitMinutes && record.waitMinutes > 0
);

console.log(`Found ${liftsWithWaits.length} lifts with wait times`);

// Group by lift name to see wait times over time
const liftsByName = {};
for (const record of liftRecords) {
  if (!liftsByName[record.name]) {
    liftsByName[record.name] = [];
  }
  liftsByName[record.name].push({
    time: record.localTime,
    wait: record.waitMinutes,
    status: record.status
  });
}

// Find the busiest lift
let busiestLift = null;
let maxWait = 0;
for (const [name, records] of Object.entries(liftsByName)) {
  const avgWait = records
    .filter(r => r.wait)
    .reduce((sum, r) => sum + r.wait, 0) / records.length;
  if (avgWait > maxWait) {
    maxWait = avgWait;
    busiestLift = name;
  }
}

console.log(`Busiest lift: ${busiestLift} (avg ${maxWait.toFixed(1)} min wait)`);

Analyze Lift Wait Times (Python)

import requests
import json
from collections import defaultdict

# Fetch lift wait-time data
url = 'https://jacobschulman.github.io/ski-run-scraper/data/vail/lifts/2025-11-21.ndjson'
response = requests.get(url)

# Parse NDJSON format
lift_records = [json.loads(line) for line in response.text.strip().split('\n')]

# Group by lift and calculate average wait times
lift_waits = defaultdict(list)
for record in lift_records:
    if record['waitMinutes'] is not None:
        lift_waits[record['name']].append(record['waitMinutes'])

# Calculate average wait time per lift
for lift_name, waits in lift_waits.items():
    avg_wait = sum(waits) / len(waits)
    max_wait = max(waits)
    print(f"{lift_name}: avg {avg_wait:.1f} min, max {max_wait} min")

# Build a heatmap of wait times by hour
import pandas as pd
from datetime import datetime

df = pd.DataFrame(lift_records)
df['hour'] = df['localTime'].str[:2].astype(int)
hourly_avg = df.groupby('hour')['waitMinutes'].mean()
print("\nAverage wait time by hour:")
print(hourly_avg)

Real-Time Wait-Time Dashboard Example

// Fetch current lift status and display on a dashboard
async function getLiftStatus(resort, date) {
  const url = `https://jacobschulman.github.io/ski-run-scraper/data/${resort}/lifts/${date}.ndjson`;
  const response = await fetch(url);
  const text = await response.text();
  const records = text.trim().split('\n').map(line => JSON.parse(line));

  // Get the latest snapshot for each lift
  const latestByLift = {};
  for (const record of records) {
    const key = record.liftId || record.name;
    if (!latestByLift[key] || record.timestamp > latestByLift[key].timestamp) {
      latestByLift[key] = record;
    }
  }

  // Display on dashboard
  return Object.values(latestByLift)
    .filter(lift => lift.status === 'Open')
    .sort((a, b) => (b.waitMinutes || 0) - (a.waitMinutes || 0))
    .map(lift => ({
      name: lift.name,
      wait: lift.waitMinutes || 0,
      capacity: lift.capacity,
      type: lift.type,
      mountain: lift.mountain
    }));
}

// Usage
const openLifts = await getLiftStatus('vail', '2025-11-21');
console.log('Open lifts with wait times:');
openLifts.forEach(lift => {
  console.log(`${lift.name} (${lift.type}): ${lift.wait} min wait`);
});

Fetch Morning Brief (JavaScript)

// Fetch the latest morning brief for daily planning
const response = await fetch(
  'https://jacobschulman.github.io/ski-run-scraper/data/vail/brief/latest.json'
);
const brief = await response.json();

// Check for powder day alerts
if (brief.computedInsights.flags.isPowderDay) {
  console.log(`🎿 ${brief.morningBrief.headline}`);
  console.log(brief.morningBrief.body);
}

// Display alerts
brief.computedInsights.alerts.forEach(alert => {
  console.log(`⚠️ ${alert}`);
});

// Get trail recommendations
brief.computedInsights.recommendations.forEach(rec => {
  console.log(`💡 ${rec}`);
});

// Check yesterday's lift wait times
if (brief.rawData.lifts.available) {
  const { avgWaitTime, busiest } = brief.rawData.lifts.yesterday;
  console.log(`Yesterday's average wait: ${avgWaitTime} min`);
  busiest.forEach(lift => {
    console.log(`  ${lift.name}: ${lift.avgWait} min avg, ${lift.peakWait} min peak`);
  });
}

Morning Brief for Push Notifications (Python)

import requests

# Fetch morning brief
url = 'https://jacobschulman.github.io/ski-run-scraper/data/vail/brief/latest.json'
brief = requests.get(url).json()

# Build notification based on conditions
flags = brief['computedInsights']['flags']

if flags['isPowderDay']:
    title = brief['morningBrief']['headline']
    body = brief['morningBrief']['body']
    send_push_notification(title, body, priority='high')

elif flags['hasNewTrails']:
    new_trails = len(brief['rawData']['terrain']['newlyOpened'])
    send_push_notification(
        f"{new_trails} new trails at {brief['resortName']}!",
        brief['morningBrief']['body']
    )

# Filter resorts by provider
if brief.get('provider') == 'ikon':
    print("This is an Ikon Pass resort")

Update Schedule

Daily Terrain & Grooming Data

Terrain, grooming, and snow report data is automatically scraped via GitHub Actions with timezone-aware scheduling:

Each resort is scraped in its local timezone (e.g., America/Denver for Colorado resorts, America/Los_Angeles for California resorts, Australia/Sydney for Australian resorts).

Real-Time Lift Wait Times

Lift wait-time data is captured much more frequently for real-time tracking:

Smart Recording: The lift scraper only records data when lifts are actually operating. It automatically detects the earliest lift opening time and latest closing time for each resort, capturing snapshots every 5 minutes during this window. Outside operating hours, no data is collected to save resources and keep file sizes manageable.

Historical data is retained indefinitely. The North American season typically runs November through May; Australian season runs June through October.

Resort Keys

Use these keys in API URLs. Resorts are grouped by data provider:

Vail Resorts (provider: "vail")

vail, beavercreek, breckenridge, keystone, crestedbutte, parkcity,
heavenly, northstar, kirkwood, stevenspass,
stowe, okemo, mountsnow, hunter, attitash, wildcat, mountsunapee, crotched,
liberty, roundtop, whitetail, jackfrost, bigboulder, hiddenvalleypa, laurelmountain, sevensprings,
paolipeaks, bostonmills, brandywine, madrivermountain, alpinevalley, wilmot, aftonalps, hiddenvalley, mtbrighton,
whistlerblackcomb, snowcreek,
perisher, fallscreek, hotham

Ikon Pass / Inspector Resorts (provider: "ikon")

stratton, palisades, jacksonhole, bigsky, deervalley, alta, snowbird, solitude,
aspenhighlands, aspenmountain, buttermilk, copper, steamboat, abasin, taos,
mammoth, junemountain, crystal,
killington, sugarbush, sugarloaf, sundayriver, loon,
tremblant, blue,
revelstoke, cypressmountain, banff, lakelouise

Technical Details

CORS

All endpoints are served via GitHub Pages, which includes CORS headers allowing access from any origin. You can fetch data directly from browser JavaScript or mobile apps without proxy servers.

Error Handling

The API returns standard HTTP status codes:

When a file doesn't exist (e.g., requesting data for a future date or a date before scraping started), you'll receive a 404. Always check response status before parsing JSON.

Rate Limiting

GitHub Pages has no explicit rate limiting for static file serving, but excessive requests may be throttled. For bulk data access, clone the repository directly.

Data Availability

TypeScript Types

Here are TypeScript interfaces for the main data structures:

interface Trail {
  Id: number;
  Name: string;
  Difficulty: "Green" | "Blue" | "Black" | "Double Black";
  IsOpen: boolean;
  IsGroomed: boolean;
  TrailInfo: string;
  TrailLength: string;
  TrailType: "Skiing" | "Snowboarding";
  IsTrailWork: boolean;
}

interface Lift {
  Id: number;
  Name: string;
  Status: "Open" | "Closed" | "Scheduled";
  IsOpen: boolean;
}

interface GroomingArea {
  Id: number;
  Name: string;
  order: number;
  Trails: Trail[];
  Lifts: Lift[];
}

interface TerrainData {
  Date: string;
  ResortId: number;
  GroomingAreas: GroomingArea[];
}

interface SnowData {
  resort: string;
  resortName: string;
  provider: "vail" | "ikon";  // Data provider (Vail Resorts or Ikon Pass)
  date: string;
  timestamp: string;
  lastUpdated: string;
  conditions: string;
  snowfall: {
    overnight_inches: number;
    overnight_cm: number;
    "24hour_inches": number;
    "24hour_cm": number;
    "48hour_inches": number;
    "48hour_cm": number;
    "7day_inches": number;
    "7day_cm": number;
    season_total_inches: number;
    season_total_cm: number;
  };
  baseDepth: {
    inches: number;
    cm: number;
  };
  forecast: {
    locations: ForecastLocation[];
  };
}

interface ForecastLocation {
  name: string;
  elevation: number | null;
  today: ForecastDay;
  forecast_days: ForecastDay[];
}

interface ForecastDay {
  date?: string;
  high_f: number;
  high_c: number | null;
  low_f: number;
  low_c: number;
  description: string;
  wind?: string;
  wind_speed?: number;
  snowfall_day_inches: number;
  snowfall_night_inches: number;
}

interface ResortIndex {
  resorts: {
    [key: string]: {
      name: string;
      files: string[];
      latest: string;
      count: number;
    };
  };
}

// Lift wait-time record (NDJSON format)
interface LiftRecord {
  timestamp: string;           // ISO 8601 timestamp (UTC)
  localTime: string;            // HH:mm:ss in resort's timezone
  resort: string;               // Resort key (e.g., "vail")
  liftId: string | null;        // Unique lift identifier
  name: string;                 // Lift name
  status: "Open" | "Closed" | "Scheduled" | "Hold";
  type: string;                 // Lift type (e.g., "gondola", "quad")
  waitMinutes: number | null;   // Wait time in minutes (null if unavailable)
  capacity: number;             // People per chair/cabin
  mountain: string;             // Mountain area or base location
  openTime: string;             // Scheduled opening (HH:mm format)
  closeTime: string;            // Scheduled closing (HH:mm format)
}

// Lift Index (overview of all lifts at a resort)
interface LiftIndex {
  resort: string;               // Resort key (e.g., "vail")
  resortName: string;           // Display name (e.g., "Vail")
  liftCount: number;            // Total number of lifts
  lifts: LiftSummary[];         // Summary of each lift
  generated: string;            // ISO 8601 timestamp when generated
}

interface LiftSummary {
  liftId: string | null;        // Unique lift identifier
  name: string;                 // Lift name
  slug: string;                 // URL-safe slug for detail page
  type: string;                 // Lift type (e.g., "gondola", "quad")
  capacity: number;             // People per chair/cabin
  mountain: string;             // Mountain area or base location
  status: string;               // Latest status
  openTime: string;             // Scheduled opening (HH:mm format)
  closeTime: string;            // Scheduled closing (HH:mm format)
  waitMinutes: number | null;   // Latest wait time in minutes
  lastUpdated: string;          // ISO 8601 timestamp of latest data
}

// Individual Lift Detail
interface LiftDetail {
  resort: string;               // Resort key
  resortName: string;           // Display name
  liftId: string | null;        // Unique lift identifier
  liftName: string;             // Lift name
  slug: string;                 // URL-safe slug
  currentStatus: {
    status: string;
    waitMinutes: number | null;
    lastUpdated: string;
  };
  stats: {
    totalSnapshots: number;     // Total data points collected
    averageWait: number;        // Average wait time (minutes)
    maxWait: number;            // Peak wait time (minutes)
    daysTracked: number;        // Number of days with data
  };
  history: LiftRecord[];        // Complete history of snapshots
  generated: string;            // ISO 8601 timestamp when generated
}

// Morning Brief - comprehensive daily summary
interface MorningBrief {
  resort: string;               // Resort key (e.g., "vail")
  resortName: string;           // Display name (e.g., "Vail")
  date: string;                 // Date in YYYY-MM-DD format
  generated: string;            // ISO 8601 timestamp when brief was generated
  rawData: {
    snow: BriefSnowData;
    terrain: BriefTerrainData;
    lifts: BriefLiftData;
    forecast: BriefForecastData;
  };
  computedInsights: BriefInsights;
  morningBrief: {
    headline: string;           // Short headline (e.g., "Powder Day Alert!")
    body: string;               // Natural language summary paragraph
  };
  errors: string[];             // Any errors during generation (usually empty)
}

interface BriefSnowData {
  overnight_inches: number;
  overnight_cm: number;
  "24hour_inches": number;
  "24hour_cm": number;
  "48hour_inches": number;
  "48hour_cm": number;
  "7day_inches": number;
  "7day_cm": number;
  season_total_inches: number;
  season_total_cm: number;
  conditions: {
    today: string;              // Today's conditions (e.g., "Powder")
    yesterday: string;          // Yesterday's conditions
  };
}

interface BriefTerrainData {
  stats: {
    openTrails: { today: number; yesterday: number; delta: number };
    groomedTrails: { today: number; yesterday: number; delta: number };
    totalTrails: number;
  };
  newlyOpened: BriefTrail[];    // Trails that opened today
  newlyClosed: BriefTrail[];    // Trails that closed today
  newlyGroomed: BriefTrail[];   // Trails freshly groomed
  ungroomed: BriefTrail[];      // Trails no longer groomed
}

interface BriefTrail {
  name: string;                 // Trail name
  difficulty: "Green" | "Blue" | "Black" | "Double Black";
  area: string;                 // Mountain area (e.g., "Mid-Vail")
}

interface BriefLiftData {
  available: boolean;           // Whether lift data exists
  yesterday?: {
    date: string;               // Date of lift data
    avgWaitTime: number;        // Average wait across all lifts (minutes)
    maxWaitTime: number;        // Peak wait time (minutes)
    totalLiftsTracked: number;  // Number of lifts with data
    busiest: BriefBusiestLift[];
  };
}

interface BriefBusiestLift {
  name: string;                 // Lift name
  avgWait: number;              // Average wait time (minutes)
  peakWait: number;             // Peak wait time (minutes)
  type: string;                 // Lift type (e.g., "gondola", "six")
  mountain: string;             // Mountain area
}

interface BriefForecastData {
  today: BriefForecastDay;
  tomorrow: BriefForecastDay;
  outlook: BriefForecastDay[];  // 3-day outlook
}

interface BriefForecastDay {
  date: string;                 // Date or ISO 8601 timestamp
  high_f: number;
  low_f: number;
  high_c: number;
  low_c: number;
  description: string;          // Weather description
  snowfall_day_inches: number;
  snowfall_night_inches: number;
  snowfall_expected?: number;   // Total expected (outlook only)
}

interface BriefInsights {
  flags: {
    hasFreshSnow: boolean;              // Overnight snow > 0
    isPowderDay: boolean;               // Significant fresh snow
    hasNewTrails: boolean;              // New trails opened today
    significantGroomingIncrease: boolean;
    highLiftDemand: boolean;            // Yesterday had high wait times
  };
  alerts: string[];             // High-priority notifications
  trends: string[];             // Trend observations
  recommendations: string[];    // Actionable suggestions
}

// Morning Brief Index
interface BriefIndex {
  resort: string;               // Resort key
  resortName: string;           // Display name
  provider: "vail" | "ikon";    // Data provider
  files: string[];              // Available dates (YYYY-MM-DD)
  latest: string;               // Most recent date
  count: number;                // Total number of briefs
  generated: string;            // ISO 8601 timestamp when index was generated
}

Use Cases

The API enables powerful applications across multiple data types:

Morning Briefs

Lift Wait Times

Terrain & Grooming

Source Code

View the scraper source code and configuration on GitHub.

The project also includes a SQLite database for historical analysis. See DATABASE.md for schema and query examples.