GameOnSign API v1
Free, read-only JSON for scoreboards, signs and "game on" relays: NHL, NFL, NBA and MLB, all 124 teams. Built for microcontrollers (ESP32, ESP8266, Pico W) as much as for browsers.
Base URL: https://gameonsign.com/api/v1/ (plain http:// works too, so small boards can skip TLS)
| Endpoint | Use it for | Typical size | Suggested polling |
|---|---|---|---|
live.php |
relays, LED signs, score bugs | ~350 B | every poll seconds (15 s near/during games, 60–300 s otherwise) |
next.php |
clock / next-game card, countdown | ~750 B | every poll seconds |
schedule.php |
upcoming games list | 1–4 KB | hourly |
teams.php |
team pickers, names, colors | ~3 KB | once (cache it) |
multi.php |
several teams in one call (grid displays) | ~0.75 KB per team | every poll seconds |
Conventions (stable within v1)
- Times are UTC epoch seconds (
start,flash_at,on_at,est_off,updated). Format them in the device's local zone. Sync time with NTP, or read the HTTPDateheader. dayis the league's official game date (YYYY-MM-DD, US Eastern for NHL/NFL/NBA, MLBofficialDate). Use it whentbdis true.leagueis lower-case:nhl,nfl,nba,mlb.teamis the upper-case abbreviation fromteams.php(e.g.TBL,TB,ORL). Unknown values return HTTP 400.- Responses are compact UTF-8 JSON with
Content-Length(no chunked encoding), soHTTPClient.useHTTP10(true)works. - ETag / Last-Modified are sent. Send
If-None-Matchand you get304 Not Modified(empty body) when nothing changed. - CORS:
Access-Control-Allow-Origin: *. - New fields may be added in v1. Existing fields will not be renamed or change meaning. Breaking changes go to
/api/v2/. - Missing data: fields about a game are omitted when
stateisnone. With ArduinoJson, use defaults:doc["start"] | 0. stale: truemeans the upstream league feed failed and you are getting the last good copy. Keep polling.- Errors:
{"v":1,"error":"bad_team","message":"..."}with HTTP 400;429= rate limited (120/min per client, seeRetry-After).503means the upstream is down and nothing is cached yet. Retry in 30–60 s.
States
state |
meaning |
|---|---|
pre |
scheduled / pre-game |
live |
in progress (includes intermissions, halftime, rain delay, overtime, shootout) |
final |
finished. Shown for a few hours after the game ends, unless the next game starts within an hour. |
postponed |
postponed or suspended |
none |
no upcoming game known (season over, or the team was eliminated) |
game_on, the sign signal
game_on is true from the scheduled start time (or as soon as the league marks the game live) until the league confirms a final. It stays true through intermissions, halftime, delays and overtime. If the league feed still says "pre" after the scheduled start (late start), game_on is already true. It is never true for a tbd start time unless the game is actually live.
sign object
| field | meaning |
|---|---|
on |
same as game_on |
off_confirmed |
true once the league reports the game final |
flash_at |
[start-300, start-120]: flash the sign at T-5 min and T-2 min. Empty if the time is TBD. |
on_at |
scheduled start (epoch), 0 if TBD |
est_off |
on_at + typical game length (NHL 2h30, NFL 3h15, NBA 2h25, MLB 2h50). Offline fallback only. |
Recommended relay logic, used by the example sketch:
online : relay = game_on, or blinking during [flash_at[i], flash_at[i]+20s)
offline: relay = (on_at <= now < est_off) and not off_confirmed, plus the same flash bursts
poll, the server's polling hint (seconds)
15 while a game is on or starts within 15 min, 60 within 2 h, 300 otherwise, 3600 with no game. Please honor it, because it keeps the service free.
GET /api/v1/live.php?league={league}&team={abbr}
The smallest document: current game state for relays and signs.
{
"v": 1,
"league": "nhl",
"team": "TBL",
"state": "live",
"game_on": true,
"id": "2026020060",
"start": 1791500400,
"tbd": false,
"home": true,
"opp": "MIN",
"matchup": "MIN@TBL",
"score": {
"team": 3,
"opp": 2
},
"period": "3rd",
"clock": "09:51",
"inter": false,
"detail": "3rd 09:51",
"sign": {
"on": true,
"off_confirmed": false,
"flash_at": [
1791500100,
1791500280
],
"on_at": 1791500400,
"est_off": 1791509400
},
"poll": 15,
"updated": 1791508466,
"stale": false
}
| field | meaning |
|---|---|
id |
league game id (string) |
start, tbd |
start time (epoch). If tbd, the time is not set yet and start is 8 PM ET on day. |
home |
true if team is the home team |
matchup |
always AWAY@HOME (e.g. MIN@TBL), independent of which team you asked for. Displays should put the away team on the left and the home team on the right. |
opp |
opponent abbreviation, or TBD (e.g. playoff opponent not decided) |
score |
{"team":n,"opp":n} when live/final, else null |
period |
1st 2nd 3rd OT 2OT SO (NHL) · Q1–Q4 OT Half (NFL/NBA) · Top 7th Bottom 7th Mid 7th End 7th (MLB) |
clock |
game clock 12:34 (NHL/NFL/NBA). For MLB, outs: 2 out. |
inter |
true during an intermission, halftime, end of a quarter, or between half-innings |
detail |
human status: 2nd Intermission, Halftime, Final/OT, Rain Delay, Pre-Game … |
GET /api/v1/next.php?league={league}&team={abbr}
Everything in live.php, plus names, venue and neighbors: the current game if live (or just finished), otherwise the next one.
{
"v": 1,
"league": "nfl",
"team": "TB",
"state": "live",
"game_on": true,
"id": "401872980",
"start": 1791504900,
"tbd": false,
"day": "2026-10-08",
"home": false,
"opp": "DAL",
"matchup": "TB@DAL",
"opp_name": "Dallas Cowboys",
"score": {
"team": 7,
"opp": 7
},
"venue": "AT&T Stadium",
"city": "Arlington",
"season": "reg",
"period": "Q2",
"clock": "5:30",
"inter": false,
"detail": "5:30 - 2nd",
"sign": {
"on": true,
"off_confirmed": false,
"flash_at": [
1791504600,
1791504780
],
"on_at": 1791504900,
"est_off": 1791516600
},
"next": {
"id": "401873002",
"start": 1792342800,
"tbd": false,
"day": "2026-10-18",
"home": true,
"opp": "PIT",
"matchup": "PIT@TB",
"state": "pre"
},
"last": {
"id": "401872968",
"start": 1791133200,
"tbd": false,
"day": "2026-10-04",
"home": true,
"opp": "GB",
"matchup": "GB@TB",
"state": "final",
"score": {
"team": 14,
"opp": 17
},
"result": "L"
},
"poll": 15,
"updated": 1791508467,
"stale": false
}
Extra fields: opp_name, venue, city, day, season (pre, reg or post), note (playoff label, e.g. ALCS Game 1), if_nec (if-necessary playoff game), result (W, L or T when final), and next/last, compact summaries of the following game and the most recent final.
Countdown captions used by the web display: NHL "Countdown to puck drop", NFL "Countdown to kickoff", NBA "Countdown to tip-off", MLB "Countdown to first pitch".
GET /api/v1/schedule.php?league={league}&team={abbr}&n=16
Upcoming games, soonest first (including a game in progress).
| param | default | |
|---|---|---|
n |
16 | 1–40 games |
venue |
1 | 0 drops venue/city (smaller) |
past |
0 | also include the last 1–10 finals (with score, result) |
all |
0 | 1 returns the whole season (preseason, regular season, playoffs; past results and upcoming) plus record (regular season, NHL as W-L-OTL). Each game adds season (pre/reg/post), det, note, on for a game in progress. Not size-trimmed (MLB is ~45 KB), so it is meant for browsers; it powers schedule.html. Other params are ignored. |
Responses are kept under 4 KB: if a request would be larger, venue text is dropped first, then games, and "trimmed":true is set.
{
"v": 1,
"league": "nfl",
"team": "TB",
"count": 3,
"games": [
{
"id": "401872980",
"start": 1791504900,
"tbd": false,
"day": "2026-10-08",
"home": false,
"opp": "DAL",
"matchup": "TB@DAL",
"state": "live",
"score": {
"team": 7,
"opp": 7
},
"venue": "AT&T Stadium",
"city": "Arlington"
},
{
"id": "401873002",
"start": 1792342800,
"tbd": false,
"day": "2026-10-18",
"home": true,
"opp": "PIT",
"matchup": "PIT@TB",
"state": "pre",
"venue": "Raymond James Stadium",
"city": "Tampa"
},
{
"id": "401873018",
"start": 1792947600,
"tbd": false,
"day": "2026-10-25",
"home": false,
"opp": "CAR",
"matchup": "TB@CAR",
"state": "pre",
"venue": "Bank of America Stadium",
"city": "Charlotte"
}
],
"updated": 1791508467,
"stale": false
}
GET /api/v1/teams.php[?league={league}]
Without league: the list of leagues. With league: every team, with ESPN primary/alternate colors (c1, c2, hex without #).
{
"v": 1,
"league": "nba",
"count": 30,
"teams": [
{
"abbr": "ATL",
"name": "Atlanta Hawks",
"short": "Hawks",
"c1": "c8102e",
"c2": "fdb927"
},
{
"abbr": "BOS",
"name": "Boston Celtics",
"short": "Celtics",
"c1": "008348",
"c2": "ffffff"
}
],
"logo": "/assets/logos/nba/{abbr}.png"
}
Logos (PNG, 256×256, transparent): /assets/logos/{league}/{abbr-lowercase}.png, plus -d.png variants for dark backgrounds, e.g. /assets/logos/nhl/tbl-d.png.
GET /api/v1/multi.php?teams=nhl:TBL,nfl:TB,mlb:TB
Up to 12 league:ABBR pairs. Returns {"v":1,"count":n,"items":[ next.php documents ],"poll":min,"updated":...}. Unknown pairs are skipped.
GET /api/v1/scoreboard.php?league=all
Who is playing today, for every league in one request (or league=nhl|nfl|nba|mlb for one). "Today" is the US Eastern date; late games from yesterday's slate are included while they are still live. Cached about 20 s server-side, so poll it at most once a minute (poll is 60).
{"v":1,"day":"2026-10-08","leagues":{"nfl":{"live":1,"games":[{"id":"401872980","a":"TB","h":"DAL","state":"live","on":true,
"start":1791504900,"tbd":false,"as":7,"hs":7,"det":"2:00 - 2nd"}]}},"poll":60,"updated":1791509360,"stale":false}
| field | meaning |
|---|---|
leagues.{lg}.live |
number of games with on: true |
games[].a / h |
away / home team abbreviation (always AWAY @ HOME) |
games[].state |
pre, live, final, postponed, cancelled |
games[].on |
same rule as game_on: true from scheduled start until a confirmed final |
as / hs, det |
away / home score and a status line, when available |
Live games are listed first. A league whose upstream failed carries "error":true. The response for all four leagues is a few KB; ESP32 devices that follow a single team should keep using live.php (smaller).
ESP32 example (relay sign)
esp32_gameon_relay.ino uses WiFi, HTTPClient and ArduinoJson 7. It polls live.php, honors poll and ETags, flashes the sign at T-5 and T-2, holds it on until a confirmed final, and falls back to on_at/est_off when offline. Core of it:
http.useHTTP10(true);
if (etag.length()) http.addHeader("If-None-Match", etag);
int code = http.GET(); // 304 = unchanged
JsonDocument doc; deserializeJson(doc, http.getStream());
bool gameOn = doc["game_on"] | false;
uint32_t f5 = doc["sign"]["flash_at"][0] | 0, f2 = doc["sign"]["flash_at"][1] | 0;
uint32_t poll = doc["poll"] | 60;
Data sources
Data comes from the leagues' public feeds (NHL api-web, MLB Stats API) and ESPN (NFL, NBA). The server caches it (schedules 20–45 min, live scoreboards 15 s during games), so devices never hit the leagues directly. Logos and team names are trademarks of their owners and are shown for identification only.
API terms
- Free and provided as is. There's no warranty and no uptime guarantee or SLA. Endpoints may change or stop. Breaking changes go to
/api/v2/when possible. - Be reasonable with polling. Honor
polland useIf-None-Match. Rate limiting applies (currently 120 requests/min per client IP → HTTP 429 withRetry-After), and abusive clients may be blocked. - No affiliation. This is an independent fan project, not affiliated with, endorsed or sponsored by the NHL, NFL, NBA, MLB, ESPN or any team. Team names and logos are trademarks of their owners and are used for identification only.
- Data may be delayed, incomplete or wrong. Don't use it for betting or anything critical.
- Attribution ("data via gameonsign.com") is appreciated, not required.
- Full text: Terms, Disclaimer & Privacy.
GET /api/v1/also lists the terms URL. Every response carriesLink: </legal.html#api>; rel="terms-of-service".
Reserved for later
POST /api/v1/checkin.php: optional device check-in (device id, firmware, team), for fleet status. Not implemented yet. Devices may already send an optional&device=<id>query parameter; it is ignored today.