Self-hosted · Open source
Your team scores.
Your house reacts.
Goalfeed watches live NHL and MLB games for the teams you tell it to follow, and fires a Home Assistant event within a second of the score changing upstream — goal horn, lights, sirens, whatever you wire to it. It polls the same public league data your browser already uses and talks straight to your own Home Assistant instance. No account, no cloud, no subscription.
The moment
This is what a goal looks like, arriving.
The instant a watched team's score changes, Goalfeed fires a goal
event to Home Assistant, logs it, and pushes it over its WebSocket API too.
Below is a simulated feed cycling through NHL and MLB — the two leagues where a
score is always worth one point, so one score change is one event — showing
team, score, timestamp. That's genuinely all that arrives today; see the note
under the payload for why.
- 20:41:03 GOAL — TOR 2–1 WPG
- 20:36:55 PERIOD START — P2
- 20:18:29 GOAL — WPG 1–1 TOR
Simulated for this page — not a live connection.
This is what actually reaches Home Assistant when that happens — the fields every league reliably sends:
goal event
{
"id": "b3f0c9de-4b7a-4c1a-9c2e-1e5b6a2f6a10",
"teamCode": "TOR",
"teamName": "Toronto Maple Leafs",
"opponentCode": "WPG",
"opponentName": "Winnipeg Jets",
"leagueId": 1,
"leagueName": "NHL",
"gameCode": "2026020123",
"period": 2,
"gameState": {
"home": { "team": "TOR", "score": 2 },
"away": { "team": "WPG", "score": 1 },
"status": "active"
}
}
Detection is a raw score-diff, not a play-by-play feed — Goalfeed knows a
team's score went up, not who scored or how. So there's no scorer name here.
The full event also carries an internal type field, but it's
empty for every league right now. Filter your automations on
teamCode and leagueId, not on type.
How it works
Four small steps, once a second.
No message queue, no database, no third-party goal API. Goalfeed is one Go binary that polls, diffs, and pushes.
Poll
Every second, Goalfeed re-checks each active game for the teams you're watching — straight from NHL's and MLB's own public APIs, the same ones their websites use.
Diff
It compares the new score to the last one it saw for that game. Any increase becomes an event — there's no play-by-play involved, just the number going up.
Fire
A goal event goes to Home Assistant, a matching message
broadcasts over the WebSocket API, and an entry lands in the local JSONL log
— all three, every time, nothing routed through a third party.
React
Your Home Assistant automation runs: goal horn, lights in team colours, a script, whatever you've built.
Every upstream source Goalfeed polls — NHL's and MLB's own public APIs — is unofficial and undocumented, the same one each league's site uses. Neither is versioned or contractually stable, so either can change shape without notice. And because detection is a raw score-diff rather than a real event feed, it counts points, not plays. For NHL and MLB that distinction doesn't bite: a goal is always worth one point, so one score change is exactly one event. It would bite for a sport where a single play is worth several points — which is part of why the football leagues aren't listed yet.
Leagues
Two leagues fire goal events today.
NHL and MLB are supported and live. CFL and NFL have client code in the repo but aren't listed below — see why underneath.
Client code for both leagues exists in this repo, but neither is listed as supported. Live testing on 2026-08-20 found both leagues' score tracking broken — CFL's client discards a valid payload on a type mismatch, and NFL parses a response shape ESPN doesn't send — so no goal event can fire for either today. Fixes are in progress; a league gets listed again only once it's been verified working against a live game.
Install
Three ways to run it.
Pick the one that matches how you already run Home Assistant. Full details, including every config option, are in the install docs and configuration reference.
The recommended path — no URL, no token, Home Assistant's Supervisor handles auth automatically.
- In Home Assistant: Settings → Add-ons → Add-on Store, open the ⋮ menu, choose Repositories, and add:
https://github.com/goalfeed/hassio-goalfeed-repository
- Install the Goalfeed add-on from the store.
- Open its Configuration tab and enter team codes:
nhl_teams: "TOR,WPG"
mlb_teams: "TOR,NYY"
test_goals: false
The add-on's Configuration tab currently exposes NHL and MLB only. To watch
CFL or NFL teams from the add-on, mount a config.yaml or set
GOALFEED_WATCH_* environment variables on the container — see
the Home Assistant docs.
- Start the add-on — it auto-detects Home Assistant through the Supervisor API, so there's nothing to authenticate.
- Open the web UI from the sidebar (ingress) or
http://homeassistant.local:8080.
There's no published Docker image yet — build the repo's
Dockerfile yourself.
# clone and build
git clone https://github.com/goalfeed/goalfeed.git
cd goalfeed
docker build -t goalfeed .
# run it — GOALFEED_WEB=true starts the API/UI without overriding the container's CMD
docker run -d --name goalfeed -p 8080:8080 \
-e GOALFEED_WEB=true \
-e GOALFEED_WATCH_NHL=WPG \
-e GOALFEED_HOME_ASSISTANT_URL=http://homeassistant.local:8123 \
-e GOALFEED_HOME_ASSISTANT_ACCESS_TOKEN=your-long-lived-token \
goalfeed
This Dockerfile builds the Go binary only — it doesn't bundle
the React web UI, so this image serves the REST and WebSocket API but not
the scoreboard page. For the full web UI in a container, build the frontend
in yourself, or use the release
tarball below, which ships both.
Every push to main publishes a
GitHub Release
for Linux, macOS and Windows (amd64/arm64/arm/386), each bundled with the built
web UI.
# swap linux_amd64 for darwin_arm64, windows_amd64, etc.
gh release download --repo goalfeed/goalfeed --pattern "*linux_amd64.tar.gz"
tar xzf goalfeed_*_linux_amd64.tar.gz
./goalfeed --nhl WPG --web
No gh? Grab the matching archive by hand from the
Releases page.
Home Assistant
The goal horn, wired up.
Goalfeed authenticates to Home Assistant with a long-lived access token (or,
on the add-on, the Supervisor handles it for you) and posts to
/api/events/goal. Every event Goalfeed sends — goal detections,
period-start notices, test goals — arrives under that same event type,
goal, regardless of sport. Filter on the event's
teamCode, not on its internal type field.
- 01Trigger on the
goalevent, filtered to your team's code. - 02Play the goal horn on whatever media player is in the room.
- 03Flash the lights in team colour, then bring them back.
Full walkthrough, including per-team sensors and binary sensors Goalfeed also publishes, is in the Home Assistant guide.
# Winnipeg Jets goal horn
automation:
- alias: "Goalfeed - Jets score"
trigger:
- platform: event
event_type: goal
event_data:
teamCode: WPG
action:
- service: media_player.play_media
target:
entity_id: media_player.living_room_speaker
data:
media_content_id: /local/sounds/goal_horn.mp3
media_content_type: music
- service: light.turn_on
target:
entity_id: light.living_room
data:
color_name: blue
flash: long
- delay: "00:00:03"
- service: light.turn_on
target:
entity_id: light.living_room
data:
color_name: white
For developers
Home Assistant isn't the only consumer.
Start Goalfeed with --web and you get a plain REST API, a
WebSocket feed, and the bundled scoreboard UI, all on one port. Build a
physical goal-light rig, a Discord bot, a browser overlay — anything that can
make an HTTP request or open a socket.
/api/games, /api/events, /api/teams…
Active games, historical/upcoming games by date, recent events, team lists,
and Home Assistant status/config — all under ApiResponse
{success, data, message}. Full reference: API
docs, or Swagger UI at /swagger/index.html on your own
instance.
ws://your-host:8080/ws
Connects immediately to a games_list snapshot, then streams
game_update, event, and log messages
as they happen. No auth, no ping/pong required. Full message shapes:
WebSocket docs.
No auth, open CORS, by design
Both APIs accept unauthenticated requests from any origin — fine behind Home Assistant ingress or on a home network, not something to expose directly to the internet. See troubleshooting if something isn't reaching you.