---
title: "Public Freshness"
method: GET
path: "/v1/freshness"
tags: ["Status"]
---

# Public Freshness

`GET /v1/freshness`

Per-bookmaker freshness — `last_update` summary across all markets.

Returns:
    bookmakers: [
      { key, market_count, latest_update, staleness_seconds, is_stale,
        worst_staleness_seconds, suspended_market_count,
        market_classes: {
          game_lines: { market_count, latest_update, staleness_seconds,
                        active_market_count, oldest_market_update,
                        worst_staleness_seconds, suspended_market_count },
          props:      { ...same shape },
        } }
    ]
    stale_threshold_seconds
    as_of (server time the rows were sampled)

`staleness_seconds` is the time since the most recent successful
market write for the book — a rough proxy for "how fresh is the
freshest line on this book." It overstates per-event freshness
because any single market touch resets it; for the marketing page
that's fine since the public claim we're backing is "we ingest
book data continuously and recently."

`market_classes` splits that per class (game lines vs props) so a class
going dark behind fresh game lines — the Pinnacle 2026-08-02 prop
incident — is visible here too. Single grouped query, no Event join, so
unlike the monitor's query this split does NOT exclude futures markets
(they land under `props` and can keep it looking fresher than the
per-game prop board really is) — acceptable for a trust surface.

A third `futures` class was considered (sportsapis.dev review follow-up,
2026-08-27) and deliberately NOT added, for two reasons that compound:
(1) `futures` is an `Event.is_future` property, not a `Market.key`
property, so `market_class_case()` — which buckets on `Market.key` alone
and is SHARED with the monitor — cannot emit it without an Event join in
every caller. And the monitor (`sport_book_class_freshness_rows`)
deliberately EXCLUDES futures so a year-round-fresh outright can't mask a
dark prop parse, so it can never carry a futures class: "keep the
vocabulary shared with the monitor" and "add a futures class" are
mutually exclusive on the same shared expression. (2) Splitting futures
out of props *correctly* (props excluding futures) forces an Event join
into THIS first query, which is deliberately join-free and spans every
market in the DB — not the bounded active-board window the ~167ms cost
was measured on. The worst-case fields (`worst_staleness_seconds` et al.)
ALREADY exclude futures via the active-board Event join, so the only
residual is a mildly inflated headline `props` count/latest on a trust
page — already noted as acceptable above. Net: high coupling + unbounded
cost to fix a cosmetic inflation. Skipped on purpose.

Each class (and the book) also carries the WORST case:
`worst_staleness_seconds` / `oldest_market_update` — the age of the
oldest NON-SUSPENDED market on the active board (events from -6h to
+48h) — plus `active_market_count` and `suspended_market_count`.
Headline staleness answers "is the book being ingested"; worst-case
answers "is any individual market being silently missed". A market
with `suspended_at` set is excluded from the worst-case number because
a flagged withdrawal is not a capture hole (the book pulled it; we
keep its last price and say so on /odds). Second grouped query with an
Event join, bounded by the active-board window.

## Response `200`

Successful Response

- unknown

---

[API](https://skmtc.dev/prop-line/apis/player-props-api.md) · [All operations](https://skmtc.dev/prop-line/apis/player-props-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/prop-line/player-props-api/revisions/b1e17e1bb9b5/schema)
