---
title: "Get Coverage"
method: GET
path: "/api/coverage"
tags: ["coverage"]
---

# Get Coverage

`GET /api/coverage`

## Request

- Base URL: `https://api.fairline.io` (the document declares no server; this is the origin it was published from)
- URL: `https://api.fairline.io/api/coverage`
- Auth: none declared

Public data-coverage report, per league, over trailing 7-day and 30-day
windows ending today, denominated by SCHEDULED games (never games with
odds, which would flatter coverage). Every registry league is included.
No auth: this feeds the public /accuracy page. Rate-limited (see below).

Per league, per window:

- two-plus-books share: fraction of scheduled games where the main markets
  (ML/Spread/Totals) drew >=2 distinct bettable bookmakers.
- median books per market (median_books.ml / .spread / .totals): for EACH
  main market separately, the median across the window's scheduled games
  of the count of distinct bettable bookmakers carrying a usable two-sided
  quote for THAT market. Each
  market gets its own median rather than one figure unioned across ML,
  Spread, and Totals, so a game with rich moneyline coverage can never
  mask a bare spread or totals market the way a single combined number
  would. A scheduled game with zero fetched rows for a market counts as 0
  books in that market's median, not as an excluded game.
- freshness (games_stale / stale_share): for a COMPLETED game (kickoff has
  passed), age is measured AT KICKOFF: the last pre-kickoff quote's
  source_fetched_at (T1.2), compared to the game's own kickoff time. This
  is deliberate. The kickoff-freeze gate means market_lines stops being
  touched once a game starts, so measuring a finished game's freshness
  against query time (now) would call every finished game stale purely
  because time has passed since kickoff. The "last pre-kickoff quote" is
  itself bounded to source_fetched_at <= kickoff in the SQL, so this stays
  correct even if the freeze gate's behavior ever changes; it can never
  read a post-kickoff write as if it were the pre-kickoff quote. For an
  UPCOMING game, age is query-relative: source_fetched_at compared to now.
  A game is "stale" if that age exceeds 30 minutes, or if it has no
  fetched bettable quote at all (missing data counts against coverage, it
  is not excluded).
- outage days: count of days in the window that had >=1 scheduled game for
  this league but zero games with a fresh (non-stale) quote that day. This
  intentionally reuses the same 30-minute freshness rule as games_stale,
  which is stricter than "the league had any fetch at all that day": a day
  with a fetch that was simply too old still counts as an outage day. That
  means this figure can only ever over-report outage days relative to a
  looser definition, never under-report one.

Every share is null, not zero, when its window has zero scheduled games
for that league (0/0 is undefined). Vendor last-update timestamps are not
available to us; see `freshness_note` on the response, which the /accuracy
page must surface verbatim ("as of our last fetch", not the book's own
time).

Cached in-process for 15 minutes behind one shared key (the endpoint takes
no parameters) with a singleflight lock, so concurrent cold requests share
one recomputation across all leagues instead of each re-running it.
Rate-limited to 30 requests/hour per IP (mirrors the public transparency
calibration route's posture), so even the cheap cache-hit path stays
bounded for an anonymous caller; the singleflight lock already bounds
concurrent recomputation, so this is the only gap the rate limit closes.
Pass/fail thresholds for billing purposes are not computed here; they are
preregistered separately in docs/product/product_thesis.md.

## Response `200`

Successful Response

- CoverageResponse
  - `generated_at` string, date-time, required
  - `freshness_note` string, required
  - `leagues` LeagueCoverage[], required
    - `league` string, required
    - `label` string, required
    - `window_7d` WindowCoverage, required: One league's coverage metrics over one trailing window. Every count is denominated by SCHEDULED games in the window, never games-with-odds (which would flatter coverage). A league with zero scheduled games in the window still appears, with games_scheduled=0 and every *_share field None (0/0 is undefined, not 0%).
      - `games_scheduled` integer, required
      - `games_with_2plus_books` integer, required
      - `two_plus_books_share` number, nullable, required
      - `median_books` MedianBooksByMarket, required: Median count of distinct bettable bookmakers per game, computed separately for each main market rather than one figure unioned across ML/Spread/Totals, so rich moneyline coverage can never mask a bare spread or totals market (see get_coverage's docstring). Each field is None, not 0, only when its window has zero scheduled games (0/0 is undefined); a scheduled game with zero fetched rows for a market still contributes 0 to that market's median.
        - `ml` number, nullable, required
        - `spread` number, nullable, required
        - `totals` number, nullable, required
      - `games_stale` integer, required
      - `stale_share` number, nullable, required
      - `outage_days` integer, required
    - `window_30d` WindowCoverage, required: One league's coverage metrics over one trailing window. Every count is denominated by SCHEDULED games in the window, never games-with-odds (which would flatter coverage). A league with zero scheduled games in the window still appears, with games_scheduled=0 and every *_share field None (0/0 is undefined, not 0%).
      - `games_scheduled` integer, required
      - `games_with_2plus_books` integer, required
      - `two_plus_books_share` number, nullable, required
      - `median_books` MedianBooksByMarket, required: Median count of distinct bettable bookmakers per game, computed separately for each main market rather than one figure unioned across ML/Spread/Totals, so rich moneyline coverage can never mask a bare spread or totals market (see get_coverage's docstring). Each field is None, not 0, only when its window has zero scheduled games (0/0 is undefined); a scheduled game with zero fetched rows for a market still contributes 0 to that market's median.
        - `ml` number, nullable, required
        - `spread` number, nullable, required
        - `totals` number, nullable, required
      - `games_stale` integer, required
      - `stale_share` number, nullable, required
      - `outage_days` integer, required

---

[API](https://skmtc.dev/fairline/apis/fairline-api.md) · [All operations](https://skmtc.dev/fairline/apis/fairline-api/llms.txt) · [OpenAPI document](https://skmtc.dev/fairline/apis/fairline-api/revisions/9f7ed2bfdf75?raw)
