---
title: "Market-wide props scan"
method: GET
path: "/api/v1/props"
tags: ["Props"]
---

# Market-wide props scan

`GET /api/v1/props`

## Request

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

All player props across today's games for a sport, flattened into rows: the "flow" feed. Row count is capped by your tier.

## Query parameters

- `sport` string
- `stat` string: Filter to a single stat.
- `player` string: Filter to one player.
- `eventId` string: Filter to one event.
- `limit` integer: Max rows (capped by your tier).
- `scopeKind` 'full_game' | 'map' | 'maps' | 'match' | 'season': Keep only markets at this scope. `mainLinesOnly` is about the LINE (standard vs goblin/demon alternate) and is orthogonal to scope, so a first-half market has its own standard line and survives that filter. Use `scopeKind=full_game` for whole-game markets only, which is what most callers building a board actually want. Values mirror `market.scopeKind` on every row.
- `mainLinesOnly` 'true' | 'false': Return only the standard two-way line per market, hiding goblin/demon/alternate variants. Use this for the pre-2026-07-27 row set.
- `cursor` string: Opaque cursor from a previous response’s pagination.nextCursor. Keyset-based, so paging stays correct when the board changes mid-walk. A cursor is bound to the filter set that produced it and returns 400 invalid_cursor if replayed against different filters.

## Response `200`

Flattened prop rows

- ScanResponse
  - `sport` string, required
  - `stat` string, nullable, required
  - `filters` object, required: Echo of the filters applied.
    - `stat` string, nullable, required
    - `player` string, nullable, required
    - `eventId` string, nullable, required
  - `count` integer, required: Rows returned. Same meaning as before; unchanged for compatibility.
  - `rows` ScanRow[], required
    - `player` string, required
    - `stat` string, required: Stat key. NBA: points | rebounds | assists | threes | pra. Other sports use descriptive keys (strikeouts, total_bases, passing_yards, goals, ...).
    - `line` number, required: The posted over/under line.
    - `overOdds` number: American odds for the over / higher.
    - `underOdds` number: American odds for the under / lower.
    - `bookCount` integer, required: Number of sources contributing this line (currently 1).
    - `playerId` string: The source's own player id (Underdog today). Not canonical: it changes if the source renumbers.
    - `flashPlayerId` string: Canonical Flash player id: deterministic from sport + normalized name, so spelling variants of one player share it and the same name in two sports does not. Pass it to GET /changes?playerId=.
    - `teamSide` 'home' | 'away': Which side of the event this player is on, as the source assigns it (its team id for this appearance matched to the event's home/away team id). Pair with homeTeam/awayTeam on scan rows. Absent when the source does not say, e.g. head-to-head events. Never inferred.
    - `liveValue` number: Current in-game stat count (live games only).
    - `gameClock` string
    - `gameState` 'pre' | 'live' | 'final'
    - `variant` 'main' | 'alternate' | 'goblin' | 'demon' | 'milestone' | 'other': Which line within the market. main = the standard two-way line. goblin = easier line at a reduced payout. demon = harder line at a boosted payout. Classified from the provider’s own payout multiplier, never guessed from the number.
    - `variantLabel` string
    - `isMainLine` boolean: True for the standard line. Filter with ?mainLinesOnly=true.
    - `alternateLine` boolean
    - `payoutMultiplier` number: Provider payout multiplier. 1.0 is standard.
    - `sourceLineId` string: The provider’s id for this specific line.
    - `market` object: Self-describing market metadata (label, family, scope, unit, projectability) for this row. Always present on API responses; optional only on the internal pre-enrichment shape.
      - `statKey` string, required: Raw normalized stat key on the prop row.
      - `label` string, required: Human display label.
      - `family` string, required: Market family: kills | fantasy | points | total_bases | ...
      - `scopeKind` 'map' | 'maps' | 'full_game' | 'match' | 'season', required: What the scope spans.
      - `scope` string, required: Stable scope filter key: map1 | maps13 | full_game.
      - `scopeLabel` string, required
      - `unit` string, nullable, required: The noun the number counts, or null when ambiguous.
      - `displayOrder` integer, required: Reading order within the sport (lower first).
      - `uiGroup` string, required: Recommended grouping bucket (= family).
      - `projection` MarketProjectionSupport, required
        - `supported` boolean, required: Whether a Flash projection is available for this market.
        - `unsupported` 'not_modeled' | 'fantasy_scoring' | 'scoped_market' | 'no_provider' | 'null', nullable, required: Machine-readable reason a market is not projectable. not_modeled = a real stat the provider does not model yet (makes a board partial); fantasy_scoring, scoped_market and no_provider are structural. Null when supported.
        - `reason` string, nullable, required: Why a projection is unavailable. Never null when supported=false (no silent omission).
      - `contextSupported` boolean, required: /context/player carries recent-form for this market.
      - `lineOnly` boolean, required: No Flash projection for this market today.
      - `alternateLine` boolean, required: Reserved; false until an alternate-line feed exists.
      - `rawStat` string, required: The stat key exactly as the source posted it, before alias resolution. The board posts several spellings for one market (receiving_yds vs receiving_yards), so statKey is normalized for grouping while this preserves the original.
      - `aliased` boolean, required: True when rawStat resolved to a different canonical statKey.
    - `flashProjection` FlashProjection: Flash's own baseline projection, present when the sport's registered provider models this market and its pack covers the player. Absent = no model, never fabricated; see market.projection.reason. Check `basis`: prior_season is not current form.
      - `value` number, required: Flash season-baseline projection, rounded to 0.5.
      - `sampleN` integer, required: Baseline sample size (maps/games), never the smaller recent-form n.
      - `method` string, required: How the baseline was built. Carries the season in parentheses when basis is prior_season.
      - `marketKey` string, required: Key into /context/player's `markets` map for this same prop.
      - `basis` 'current_season' | 'prior_season' | 'mixed_history', required: Which season's play this projection is built from. current_season = the season the prop is for. prior_season = an earlier season, because the prop season has no usable data in the pack. mixed_history = a few current-season games topped up with the most recent prior-season games. A prior_season projection is NOT current form and must never be presented as such; a mixed_history one is only partly current form.
      - `seasonId` string, nullable, required: The season the underlying play came from, when the pack records it. Null when unknown.
      - `basisNote` string, nullable, required: Plain-English provenance caveat. Always set when basis is prior_season or mixed_history; null otherwise.
    - `projectionAvailability` ProjectionAvailability: Whether this row is projected and, when it is not, a stable reasonCode (identity_ambiguous, insufficient_history, unsupported_market, ...). Absent when there is no row-level explanation to give; never guessed.
      - `status` 'projected' | 'line_only', required
      - `reasonCode` 'no_provider' | 'unsupported_market' | 'insufficient_history' | 'identity_unresolved' | 'identity_ambiguous' | 'source_stale' | 'source_unavailable' | 'rights_blocked' | 'model_shadow_only' | 'market_shadow_only' | 'missing_opponent' | 'missing_event' | 'invalid_line' | 'offseason' | 'quality_gate_failed' | 'null', nullable, required: Stable machine-readable reason a row is line-only. Null when status is projected.
      - `reason` string, nullable, required: Plain-English explanation. Null when status is projected.
    - `eventId` string, required
    - `sport` string, required
    - `homeTeam` string, required
    - `awayTeam` string, required
    - `startTime` string, required
    - `source` string, required
    - `fetchedAt` number, required: Epoch ms when the upstream snapshot this row came from was taken.
    - `sourceType` 'dfs' | 'sportsbook' | 'exchange' | 'model' | 'unknown', required: What kind of operator produced this price. Underdog is a DFS pick’em, NOT a sportsbook: its odds come from a fixed-line payout multiplier and should not be treated as an independently wagerable two-way book price.
    - `pricingModel` 'posted' | 'implied' | 'effective' | 'consensus' | 'unknown', required: How the number relates to a real price: posted by the operator, derived, adjusted, or aggregated across books.
    - `pricingNote` string, nullable, required: Caveat when the price is not a plain single-book posted price.
  - `pagination` Pagination, required
    - `returned` integer, required: Rows in THIS response.
    - `totalAvailable` integer, required: Rows matching the filters before paging.
    - `limit` integer, required: Effective page size actually applied.
    - `truncated` boolean, required: True when rows were withheld. Always explicit.
    - `hasMore` boolean, required
    - `nextCursor` string, nullable, required: Pass as ?cursor= for the next page. Null on the last page.
    - `requestedLimit` integer: Set when your tier capped the limit you asked for.
    - `tierLimit` integer: The tier ceiling that was applied.
  - `data` DataMeta, required
    - `status` 'fresh' | 'stale' | 'partial' | 'unavailable', required
    - `fetchedAt` number, nullable, required
    - `oldestFetchedAt` number, nullable, required
    - `newestFetchedAt` number, nullable, 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.
  - `sources` SourceHealth[], required
    - `id` string, required
    - `status` 'healthy' | 'degraded' | 'unavailable', required
    - `fetchedAt` number, nullable, required
    - `rows` integer, required
    - `warning` string, nullable, required
  - `empty` boolean, required: True when zero rows were returned.
  - `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 or degraded result.
  - `coverage` object: Present on an empty result: 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

- `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)
