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).
GroomingAreas change (lift wait/status churn is ignored here).data/{resort}/lifts/YYYY-MM-DD.ndjson with per-resort lift indexes.data/{resort}/terrain/index.json exists for every resort with terrain data (generated automatically).FORCE_SCRAPE=true to bypass window checks for Inspector runs.View interactive reports with date navigation, historical comparisons, and weather forecasts:
Grooming Report
Grooming Report
Grooming Report
Grooming Report
Grooming Report
Grooming Report
Grooming Report
Grooming Report
Grooming Report
Grooming Report
Grooming Report
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
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
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
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
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
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
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
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
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
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
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
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
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
View formatted reports with date navigation and historical comparisons:
/data/{resort}/grooming.html - Grooming and lift status report/data/{resort}/snow.html - Snow conditions and weather forecast/data/{resort}/trails.html - Trail listing with grooming history/data/{resort}/lifts.html - Real-time lift status dashboard with search/data/{resort}/lift.html - Individual lift report with wait-time chartsEach 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
}
]
}
]
}
| 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 |
| 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 |
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
}
]
}
]
}
}
| 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") |
| 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 |
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"
}
| 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) |
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": []
}
| 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) |
computedInsights make it easy to filter for specific conditions, while morningBrief provides ready-to-use text.
// 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`);
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']}\"")
// 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 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)`);
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)
// 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 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`);
});
}
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")
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).
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.
Use these keys in API URLs. Resorts are grouped by data provider:
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
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
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.
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.
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.
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
}
The API enables powerful applications across multiple data types:
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.