REST API reference
Every supported HTTP endpoint Goalfeed exposes with --web,
on --web-port (default 8080).
Minimal working example
curl -s http://localhost:8080/api/games | python3 -m json.tool
Every JSON endpoint wraps its payload in the same envelope:
type ApiResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Message string `json:"message,omitempty"`
}
There is no authentication on any endpoint below, and CORS allows every origin. This is by design for a home-network tool — see Security before putting this API anywhere reachable from the internet.
Games
GET /api/games
All currently active games across every monitored team and league.
| Param | Type | Default | Required | Notes |
|---|---|---|---|---|
| none | ||||
curl -s http://localhost:8080/api/games
Response: data is an array of
models.Game — { leagueId, gameCode, currentState: { home,
away, period, clock, status, venue, weather }, statistics, ... }. NFL games
get on-the-fly enrichment of missing down/distance/possession from a fresh ESPN call.
Errors: 500 on an internal failure.
GET /api/games/history
Games (any registered league, including IIHF) on a given date, filtered to your
monitored teams. Not listed in the generated Swagger spec
(docs/swagger.yaml) even though it's real and working — a
documentation gap in the generated spec, not a hint that the endpoint is
unsupported.
| Param | Type | Default | Required | Notes |
|---|---|---|---|---|
date | string | — | yes | YYYY-MM-DD. Missing or malformed → 400 |
curl -s "http://localhost:8080/api/games/history?date=2026-01-15"
Response: data is an array of models.Game,
restricted to leagues where watch.<league> has at least one team
configured — a league with no watched teams is skipped entirely, not returned
empty. Errors: 400 (missing/invalid date).
GET /api/upcoming
Upcoming games (next 7 days) for monitored teams across every registered league, including IIHF.
curl -s http://localhost:8080/api/upcoming
Response: data is an array of models.Game.
Errors: 500.
POST /api/clear
Clears every tracked game from the in-memory store. Safe to call any time — the store is ephemeral, and the next poll cycle rebuilds it.
curl -s -X POST http://localhost:8080/api/clear
Response: { "success": true }.
Errors: 500.
POST /api/refresh — documented, not functional
The handler is an explicit placeholder left over from a refactor. It logs
"Refresh active games - placeholder implementation" and returns
HTTP 200 unconditionally — it does not refresh anything. Games
refresh on their own every minute regardless of whether you call this.
Events & logs
GET /api/events
Recent goal/score events, most recent first.
| Param | Type | Default | Required | Notes |
|---|---|---|---|---|
leagueId | integer | — | no | see leagues reference for IDs |
team | string | — | no | team code |
since | string | — | no | RFC3339 timestamp |
limit | integer | 50 | no | max rows returned |
curl -s "http://localhost:8080/api/events?leagueId=1&team=WPG&limit=10"
Response: data is an array of models.Event
objects. Remember: type and description
are empty strings for NHL/MLB/NFL/CFL events — only Olympic hockey populates
them (see WebSocket reference). Errors:
400 (bad since/limit), 500.
GET /api/logs
The JSONL application log — every event delivery attempt (success or failure) and state change, queryable. This is where the errors on the troubleshooting page actually show up.
| Param | Type | Default | Required | Notes |
|---|---|---|---|---|
leagueId | integer | — | no | |
team | string | — | no | |
since | string | — | no | RFC3339 |
limit | integer | 0 (all) | no |
curl -s "http://localhost:8080/api/logs?limit=5"
Response (real, captured):
{
"id": "1787264741605724000-TEST-",
"type": "event",
"teamCode": "TEST",
"target": "ha:event:goal",
"success": false,
"error": "refusing to send Home Assistant request: home assistant url is empty",
"timestamp": "2026-08-20T17:25:41.605724-05:00"
}
Errors: 400, 500.
Teams & leagues
GET /api/teams
| Param | Type | Default | Required | Notes |
|---|---|---|---|---|
leagueId | integer | — | yes | 1=NHL, 2=MLB, 5=CFL, 6=NFL (full table: leagues reference) |
curl -s "http://localhost:8080/api/teams?leagueId=1"
Response: data is an array of team objects
(teamId, teamCode, teamName,
leagueId, logoUrl). Errors:
400 (missing/invalid leagueId), 500.
GET /api/leagues
Current per-league watch configuration.
curl -s http://localhost:8080/api/leagues
Errors: 500.
POST /api/leagues
Replaces the watched-team list for one league.
| Body field | Type | Required |
|---|---|---|
leagueId | integer | yes |
teams | array of string | yes |
curl -s -X POST http://localhost:8080/api/leagues \
-H "Content-Type: application/json" \
-d '{"leagueId": 1, "teams": ["WPG", "TOR"]}'
Errors: 400 (bad body), 500.
Home Assistant
GET /api/homeassistant/status
Whether Goalfeed can currently reach Home Assistant, and via which source.
curl -s http://localhost:8080/api/homeassistant/status
Response: data is
{ connected, source, message, url, tokenSet } — note
tokenSet is a boolean, the access token itself is never echoed here.
Errors: 500.
GET /api/homeassistant/config
Response: data.configured is
{ url, tokenSet } — again, boolean only; the token value is never
returned by this endpoint. Errors: 500.
POST /api/homeassistant/config
| Body field | Type | Default | Required | Notes |
|---|---|---|---|---|
url | string | "" | no | validated — a public/remote URL is rejected unless
home_assistant.allow_remote_url is true (see
Security) |
accessToken | string | "" | no | |
clearToken | bool | false | no | if true, clears the stored token regardless of accessToken |
curl -s -X POST http://localhost:8080/api/homeassistant/config \
-H "Content-Type: application/json" \
-d '{"url": "http://homeassistant.local:8123", "accessToken": "your-long-lived-token"}'
Response message differs depending on web.allow_config_writes:
by default, "Configuration updated for this session only (not persisted to
disk). Set web.allow_config_writes: true in config.yaml to allow this API to write
config.yaml." — the change applies in-memory immediately but reverts on
restart unless that key is set. Errors: 400 (invalid
JSON, or a URL rejected by the remote-address check).
Excluded from this reference
POST /api/debug/nfl/add exists in the code and is tagged
debug in its own Swagger annotation. It force-adds an NFL game by ESPN
event ID for testing and is intentionally left out of this reference — it is
not a supported part of the public API surface.
Next
Real-time push instead of polling this API — WebSocket reference. Threat model for running this unauthenticated API at all — Security.