{season}/schedule.json
/api/v1/{season}/schedule.json
All scheduled games for one season, including unplayed ones. Cancelled games are flagged to distinguish from null scores.
Seasons
Replace {season} with one of 2026, 2025, 2024, 2023, 2022. Not every season has every endpoint — check themanifest first. If it is not listed there, the URL 404s.
Response
Every endpoint returns the same envelope. Your data is indata — data is the array itself — there is no wrapper object inside it.
{
"schemaVersion": "1.0",
"generatedAt": "2026-06-23T14:03:13.152995-04:00",
"league": "PUL",
"data": [ ... ]
}generatedAt is a nullable ISO-8601 timestamp that may or may not carry a UTC offset — parse leniently. It is null for 2022 and 2023, which predate the pipeline that records it.
Fields
| Field | Type | Meaning |
|---|---|---|
| week | number|null | Week number. 98 = semifinals, 99 = finals. |
| weekLabel | string|null | Human label — "Week 3", "Semifinals", "Finals". |
| isPostseason | boolean | True for weeks 98 and 99. |
| date | string|null | ISO date (YYYY-MM-DD). |
| time | string|null | Local start time as published. |
| location | string|null | Venue name. |
| locationAddress | string|null | Venue street address. |
| division | string|null | Division this fixture belongs to, where applicable. |
| awayAbbrev | string|null | Canonical away-team abbreviation. |
| awayName | string|null | Full away-team name. |
| awayScore | number|null | Away goals, or null if the game has not been played. |
| homeAbbrev | string|null | Canonical home-team abbreviation. |
| homeName | string|null | Full home-team name. |
| homeScore | number|null | Home goals, or null if the game has not been played. |
| cancelled | boolean | Always present. True for games that will never be played. |
| youtubeUrl | string|null | Full-game video, where one exists. |
Dotted names are nested — colors.primary means{ colors: { primary } }. When the parent is a list of objects (like seasons on the manifest),seasons.season means each entry in that array has its ownseason field — access it as seasons[i].season, not seasons.season.
Examples
curl -s https://pul-stats-hub.pages.dev/api/v1/2025/schedule.json// Games with video, and games that were cancelled
const res = await fetch('https://pul-stats-hub.pages.dev/api/v1/2025/schedule.json');
const { data } = await res.json();
const watchable = data.filter((g) => g.youtubeUrl);
const cancelled = data.filter((g) => g.cancelled);
// A null score alone does NOT mean cancelled - check the flag.
const notYetPlayed = data.filter((g) => g.homeScore === null && !g.cancelled);
console.log(watchable.length, cancelled.length, notYetPlayed.length);# Venues used in 2025
import json, urllib.request
URL = "https://pul-stats-hub.pages.dev/api/v1/2025/schedule.json"
with urllib.request.urlopen(URL) as response:
games = json.load(response)["data"]
venues = {g["location"] for g in games if g["location"]}
for venue in sorted(venues):
print(venue)Worth knowing
- A null score means either "not played yet" or "cancelled" — `cancelled` is the flag that tells them apart. That is why it is always present rather than only on cancelled rows.
- Unlike games.json, this includes unplayed fixtures. That is the point of the schedule.
