---
title: "List today's games"
method: GET
path: "/api/v1/games"
tags: ["Props"]
---

# List today's games

`GET /api/v1/games`

## Request

- Base URL: `https://api.flashodds.live`
- URL: `https://api.flashodds.live/api/v1/games`
- Auth: HTTP bearer

Games with player props posted for the given sport, merged across free books. Live games first. This is a schedule/event-discovery feed in its own right: use it to learn what is on the board before asking for props. An empty list is classified (`emptyReason`) so an off day, a season break and a source outage are told apart, and `pollAdvice` says what a poller should do next. `data.refreshCadenceSeconds` is how often the board behind it actually rebuilds; polling faster than that cannot return anything new. When the board identity is pinned the response carries a strong `ETag`: send it back as `If-None-Match` and an unchanged board answers `304` with no body. A 304 still authenticates and still counts against your rate limit; what it saves is the transfer.

Poll it like this: keep the ETag, treat 304 as "same board", and wait as long as `pollAdvice` says. `stop_until_coverage_returns` (offseason) and `wait_for_refresh` on `no_games_scheduled` are expected schedules, not errors; `backoff_source` means the upstream source is down or stale.

```js
let etag = null;
async function poll() {
  const res = await fetch('https://api.flashodds.live/api/v1/games?sport=cs2', {
    headers: { Authorization: `Bearer ${KEY}`, ...(etag ? { 'If-None-Match': etag } : {}) }
  });
  let wait = 300;
  if (res.status !== 304) {
    etag = res.headers.get('ETag') ?? etag;
    const body = await res.json();
    wait = body.pollAdvice.suggestedSeconds ?? body.data.refreshCadenceSeconds;
    if (body.empty) console.log(body.emptyReason, body.pollAdvice.action);
    else render(body.games);
  }
  setTimeout(poll, wait * 1000);
}
```

## Query parameters

- `sport` 'nba' | 'basketball' | 'mlb' | 'nfl' | 'nhl' | 'ncaab' | 'ncaaf' | 'soccer' | 'tennis' | 'ufc' | 'cs2' | 'valorant' | 'dota2' | 'esports' | 'cod': Sport id. One of: nba, basketball, mlb, nfl, nhl, ncaab, ncaaf, soccer, tennis, ufc, cs2, valorant, dota2, esports, cod. See GET /sports.

## Headers

- `if-none-match` string: An ETag from a previous /games response. Unchanged board answers 304.

## Response `200`

Games list

- GamesResponse
  - `sport` string, required
  - `count` integer, required
  - `games` Game[], required
    - `id` string, required: Event id. Prefix ud- (Underdog) or sc- (soccer odds board).
    - `sport` string, required
    - `homeTeam` string, required
    - `awayTeam` string, required
    - `startTime` string, required: ISO 8601 start time.
    - `live` boolean, required: Game is currently in progress.
    - `source` 'underdog' | 'bovada' | 'theoddsapi', required
  - `snapshotId` string, nullable, required: Logical board identity; matches /sports snapshotId for the same board. Null when consistency is not single.
  - `snapshotConsistency` 'single' | 'mixed' | 'none', required: single = every row came from one upstream snapshot. mixed = rows span snapshots (treat as partial). none = no source snapshot.
  - `data` object, required: Board identity and cadence. The top-level snapshot fields are kept for compatibility.
    - `snapshotId` string, nullable, required: Logical board identity; matches /sports snapshotId for the same board. Null when consistency is not single.
    - `snapshotConsistency` 'single' | 'mixed' | 'none', required: single = every row came from one upstream snapshot. mixed = rows span snapshots (treat as partial). none = no source snapshot.
    - `refreshCadenceSeconds` integer, required: How often the board behind this feed actually rebuilds. Polling faster than this cannot return anything new; pair it with If-None-Match to skip the transfer entirely.
  - `empty` boolean, required: True when no games are on the board.
  - `emptyReason` 'offseason' | 'no_games_scheduled' | 'no_props_posted' | 'no_filter_matches' | 'source_unavailable' | 'source_stale' | 'outside_supported_window' | 'tier_limit' | 'unknown' | 'null', nullable, required: Why the result is empty. Null when it is not. A healthy empty slate is distinguishable from a source failure.
  - `message` string, nullable, required: Human-readable explanation of an empty result.
  - `coverage` object: Present on an empty board: archive-observed coverage, so a season break is distinguishable from an outage.
    - `state` 'active' | 'offseason' | 'never', required
    - `lastLineAt` string, nullable, required
    - `daysDark` integer, nullable, required
  - `pollAdvice` PollAdvice, required
    - `action` 'wait_for_refresh' | 'stop_until_coverage_returns' | 'change_filters' | 'backoff_source' | 'reduce_or_upgrade_request', required: What an unattended client should do next. Derived from emptyReason, which stays the authority on WHY. stop_until_coverage_returns means the sport is dark for weeks: re-asking every few seconds burns your quota and cannot return anything. change_filters means the board has rows but your filters select none of them, so repeating the identical query is the one thing that cannot change the answer.
    - `suggestedSeconds` integer, nullable, required: How long to wait before the next call, derived from the server board cadence. Null means waiting is not what will fix this. This is advice on a healthy 200 and is deliberately NOT Retry-After, which this API reserves for rate limiting.

## Other responses

- `304`: The board has not changed since the ETag you sent. No body.
- `400`: Invalid request
- `401`: Missing or invalid API key
- `403`: Not permitted on your tier
- `429`: Rate limit or daily quota exceeded

---

[API](https://skmtc.dev/flashodds/apis/flash-props-api.md) · [All operations](https://skmtc.dev/flashodds/apis/flash-props-api/llms.txt) · [OpenAPI document](https://skmtc.dev/flashodds/apis/flash-props-api/revisions/044161ffbfd8?raw)
