{
  "openapi": "3.0.3",
  "info": {
    "title": "PropLine \u2014 Player Props & Sports Betting Odds API",
    "version": "1.2.0",
    "description": "Real-time player prop betting lines and game odds across 56 sports from 27 books \u2014 17 sportsbooks (BetMGM, BetRivers, Bovada, DraftKings, Fanatics, FanDuel, Fliff, Hard Rock Bet, Marathon Bet, Pinnacle, Unibet, 1xBet, TAB AU, ReBet, BetUS, BetOnline.ag, LowVig.ag), 4 DFS providers (PrizePicks, Underdog, Sleeper, Dabble), 2 prediction-market exchanges (Kalshi, Polymarket), and 4 betting exchanges (Matchbook, Smarkets, Novig, ProphetX). Includes prop resolution against actual box-score stats, historical line movement with book-published timestamps, cross-book +EV with no-vig fair lines, an EV calculator for prices at books we don't carry, period-market filtering (?period=q1|h1|p1|f5|map1|s1|g1) on every odds endpoint, futures markets, and bulk CSV exports. Responses are the-odds-api compatible for drop-in migration. Every authenticated response carries live quota headers (X-Daily-Limit, X-Daily-Used, X-Daily-Remaining, X-Daily-Reset \u2014 unix seconds at the 00:00 UTC reset) so usage can be tracked programmatically without a dashboard. Standard rate-limit headers are sent alongside them (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset \u2014 seconds until reset \u2014 and RateLimit-Policy, plus the X-RateLimit-* spelling), so a generic client can self-throttle; both 429 types carry Retry-After. Versioning is in the URL path and anything deprecated keeps serving for at least 12 months with RFC 8594 Deprecation and Sunset headers (https://prop-line.com/api-versioning).",
    "contact": {
      "name": "PropLine",
      "url": "https://prop-line.com",
      "email": "support@prop-line.com"
    }
  },
  "paths": {
    "/v1/sports": {
      "get": {
        "tags": [
          "Sports"
        ],
        "summary": "List Sports",
        "description": "List all available sports.",
        "operationId": "list_sports_v1_sports_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/SportOut"
                  },
                  "type": "array",
                  "title": "Response List Sports V1 Sports Get"
                }
              }
            }
          }
        },
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ]
      }
    },
    "/v1/sports/{sport_key}/events": {
      "get": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "List Events",
        "description": "List upcoming events for a sport (no odds \u2014 free endpoint).",
        "operationId": "list_events_v1_sports__sport_key__events_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EventOut"
                  },
                  "title": "Response List Events V1 Sports  Sport Key  Events Get"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sports/{sport_key}/odds": {
      "get": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "List Odds",
        "description": "Bulk odds for a sport. Filter by market keys (comma-separated).",
        "operationId": "list_odds_v1_sports__sport_key__odds_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "h2h",
              "title": "Markets"
            }
          },
          {
            "name": "bookmakers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all books. Matches the-odds-api's bookmakers param.",
            "example": "draftkings,fanduel"
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "Game-period filter. Omitted = full-game markets only (backwards-compat default). Pass canonical codes like 'q1', 'h1', 'p1', 'i6', 'f5', 'map1', 's1' (sets \u2014 tennis / volleyball), 'g1' (games \u2014 table tennis / badminton), comma-separated for multiple, or 'all' to include every period.",
            "schema": {
              "type": "string",
              "example": "q1"
            }
          },
          {
            "name": "includeLinks",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "When true, adds a `link` field \u2014 the book's public event-page URL (plain navigation, no affiliate tagging) \u2014 per bookmaker block (odds endpoints) or per price row (/best-line). Links ship for Bovada, DraftKings, FanDuel, BetMGM, Kalshi, Polymarket and Smarkets; other books return null. Matches the-odds-api's includeLinks param. It also adds an `app_link` field (mobile app-open deep link; ProphetX only today, null elsewhere)."
          },
          {
            "name": "includeBookIds",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "When true, adds `book_event_id` per bookmaker block and `book_outcome_id` + `outcome_id` per outcome \u2014 each book's OWN identifiers for the event and the priced selection, plus PropLine's canonical outcome id (the value `line_movement` / `resolution` webhooks carry, so a delivery joins onto one REST row). Use these to join PropLine rows onto a book's native feed by id instead of matching on team names, players and lines. Kalshi ships both (event ticker + per-contract market ticker, e.g. KXMLBGAME-26AUG08NYYBOS-NYY); Bovada, DraftKings, FanDuel, BetMGM, Pinnacle, Polymarket, Smarkets, Novig, Matchbook, Sleeper, Dabble, ReBet, 1xBet, TAB AU and the Kambi books (BetRivers/Unibet) ship event ids. Books without a stable id return null. NB a two-sided market can share one book_outcome_id across both legs (a Kalshi contract is binary \u2014 Over/Under are its YES/NO sides); the outcome's `name` says which side. PropLine-specific, not a the-odds-api param."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EventOddsOut"
                  },
                  "title": "Response List Odds V1 Sports  Sport Key  Odds Get"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/odds": {
      "get": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "Get Event Odds",
        "description": "Get odds for a single event \u2014 supports player prop markets.\nThis is where the real value is.",
        "operationId": "get_event_odds_v1_sports__sport_key__events__event_id__odds_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "pitcher_strikeouts,batter_hits,batter_home_runs",
              "title": "Markets"
            }
          },
          {
            "name": "bookmakers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all books. Matches the-odds-api's bookmakers param.",
            "example": "draftkings,fanduel"
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "Game-period filter. Omitted = full-game markets only. Canonical codes ('q1', 'h1', 'p1', 'i6', 'f5', 'map1', 's1', 'g1'), comma-separated, or 'all'.",
            "schema": {
              "type": "string",
              "example": "q1"
            }
          },
          {
            "name": "includeLinks",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "When true, adds a `link` field \u2014 the book's public event-page URL (plain navigation, no affiliate tagging) \u2014 per bookmaker block (odds endpoints) or per price row (/best-line). Links ship for Bovada, DraftKings, FanDuel, BetMGM, Kalshi, Polymarket and Smarkets; other books return null. Matches the-odds-api's includeLinks param. It also adds an `app_link` field (mobile app-open deep link; ProphetX only today, null elsewhere)."
          },
          {
            "name": "includeBookIds",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "When true, adds `book_event_id` per bookmaker block and `book_outcome_id` + `outcome_id` per outcome \u2014 each book's OWN identifiers for the event and the priced selection, plus PropLine's canonical outcome id (the value `line_movement` / `resolution` webhooks carry, so a delivery joins onto one REST row). Use these to join PropLine rows onto a book's native feed by id instead of matching on team names, players and lines. Kalshi ships both (event ticker + per-contract market ticker, e.g. KXMLBGAME-26AUG08NYYBOS-NYY); Bovada, DraftKings, FanDuel, BetMGM, Pinnacle, Polymarket, Smarkets, Novig, Matchbook, Sleeper, Dabble, ReBet, 1xBet, TAB AU and the Kambi books (BetRivers/Unibet) ship event ids. Books without a stable id return null. NB a two-sided market can share one book_outcome_id across both legs (a Kalshi contract is binary \u2014 Over/Under are its YES/NO sides); the outcome's `name` says which side. PropLine-specific, not a the-odds-api param."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventOddsOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/markets": {
      "get": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "List Event Markets",
        "description": "List available market types for a specific event.\nUseful for discovering what props are available before requesting odds.",
        "operationId": "list_event_markets_v1_sports__sport_key__events__event_id__markets_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/odds/history": {
      "get": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "Get Event Odds History",
        "description": "Historical odds movement for a single event.\nPro / Streaming / Enterprise: full snapshot history.\nFree tier: redacted (shows structure and count).\n\nDepth is tier-gated by event age: Pro = events from the last 90 days, Streaming = 365 days, Enterprise = unlimited. Events older than the tier window return the redacted shape (empty snapshots + `upgrade_url`)\u00a0\u2014 same envelope as the free-tier redaction, so no schema fork.",
        "operationId": "get_event_odds_history_v1_sports__sport_key__events__event_id__odds_history_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "pitcher_strikeouts,batter_hits",
              "title": "Markets"
            }
          },
          {
            "name": "bookmakers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all books. Matches the-odds-api's bookmakers param.",
            "example": "draftkings,fanduel"
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "ISO timestamp; only include snapshots at or after this time. Mutually exclusive with relative_from.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "ISO timestamp; only include snapshots at or before this time. Mutually exclusive with relative_to.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "relative_from",
            "in": "query",
            "required": false,
            "description": "Offset relative to commence_time, e.g. '-3h', '-30m', '-90s'. Mutually exclusive with `from`.",
            "schema": {
              "type": "string",
              "example": "-3h"
            }
          },
          {
            "name": "relative_to",
            "in": "query",
            "required": false,
            "description": "Offset relative to commence_time, e.g. '-1m' or '0' for commence_time itself. Mutually exclusive with `to`.",
            "schema": {
              "type": "string",
              "example": "0"
            }
          },
          {
            "name": "interval",
            "in": "query",
            "required": false,
            "description": "Downsample to one snapshot per bucket; latest snapshot in each bucket wins.",
            "schema": {
              "type": "string",
              "enum": [
                "30s",
                "1m",
                "5m",
                "15m",
                "30m",
                "1h"
              ]
            }
          },
          {
            "name": "changes_only",
            "in": "query",
            "required": false,
            "description": "When true, drop snapshots whose (price, point) match the previous one. The opening line is always kept.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "Game-period filter. Omitted = full-game markets only. Canonical codes ('q1', 'h1', 'p1', 'i6', 'f5', 'map1', 's1', 'g1'), comma-separated, or 'all'.",
            "schema": {
              "type": "string",
              "example": "q1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventHistoryOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/odds/closing": {
      "get": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "Opening & closing lines (CLV)",
        "description": "Opening and closing line per (book, market, outcome).\n\nClosing = the last snapshot at or before commence_time \u2014 the canonical line CLV-tracking tools measure against. Opening = the first snapshot in the same 14-day pre-kickoff window. Returning both ends in one call is what makes this a complete CLV helper: on spreads and totals the point moves as much as the price (-3 -110 -> -3.5 -105), so `opening_point`/`point` matter as much as the prices.\n\nHobby+ gets full data; free tier is redacted (structure only). Same per-tier event-age cap as /odds/history.",
        "operationId": "get_event_odds_closing_v1_sports__sport_key__events__event_id__odds_closing_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "h2h,spreads,totals",
              "title": "Markets"
            }
          },
          {
            "name": "bookmakers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all books. Matches the-odds-api's bookmakers param.",
            "example": "draftkings,fanduel"
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "description": "Game-period filter. Omitted = full-game markets only. Canonical codes ('q1', 'h1', 'p1', 'i6', 'f5', 'map1', 's1', 'g1'), comma-separated, or 'all'.",
            "schema": {
              "type": "string",
              "example": "q1"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventClosingOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/futures": {
      "get": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "List Futures",
        "description": "List futures markets for a sport \u2014 championship winner, MVP,\ndivision winner, etc. One row per (futures event, book, market) with\nevery team or player priced. Free tier; pulled from each book's\nfutures feed (Bovada today). Order is by event commence_time\n(typically season-end) ascending.\n\nOptional `bookmakers` (comma-separated book keys) narrows the per-book market rows \u2014 same the-odds-api-compatible contract as every /odds endpoint: omitted = all books, unknown keys match nothing. A futures event with no market left after filtering is dropped.",
        "operationId": "list_futures_v1_sports__sport_key__futures_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "bookmakers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "title": "Bookmakers",
              "nullable": true
            },
            "description": "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all books. Matches the-odds-api's bookmakers param."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/FuturesEventOut"
                  },
                  "title": "Response List Futures V1 Sports  Sport Key  Futures Get"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/ev": {
      "get": {
        "tags": [
          "+EV"
        ],
        "summary": "Get Event Ev",
        "description": "Compute cross-book +EV for a single event.\n\n`markets` is an optional comma-separated filter. If omitted, every\nmarket on the event is evaluated. Each market line that has Pinnacle\n(preferred) or Bovada (fallback) coverage gets a no-vig fair price\nand an EV% per book.\n\n`bookmakers` is an optional comma-separated book-key filter \u2014 shop only the books you hold accounts at. It narrows the returned prices, never the anchor.\n\nThe anchor is chosen per line and always named: every line carries `fair_source`, and the response carries `fair_source_default` with the fallback order (pinnacle \u2192 polymarket \u2192 kalshi \u2192 bovada). One response routinely mixes several anchors, so read `fair_source` per line.",
        "operationId": "get_event_ev_v1_sports__sport_key__events__event_id__ev_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "title": "Markets",
              "nullable": true
            }
          },
          {
            "name": "bookmakers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "title": "Bookmakers",
              "nullable": true
            },
            "description": "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all books. Matches the-odds-api's bookmakers param. NB it narrows the PRICES only, never the fair-line anchor: bookmakers=draftkings still returns DraftKings EV% measured against Pinnacle. Lines where none of the requested books quote a price are omitted."
          },
          {
            "name": "devig",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "multiplicative",
                "shin"
              ],
              "default": "multiplicative",
              "title": "Devig"
            },
            "description": "How the anchor's vig is removed before the fair line is derived. 'multiplicative' (default) divides each implied probability by the booksum, spreading the overround evenly. 'shin' solves Shin's insider-trading model, which loads the overround onto the longshot side and corrects the favourite-longshot bias \u2014 negligible on a -110/-110 total, material on a +600 anytime scorer. The default is the only method the +EV backtest is measured on. Unknown values return 400.",
            "example": "shin"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventEvOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/projections": {
      "get": {
        "summary": "Market-implied consensus projections per (market, player)",
        "description": "One row per (market, player): the statistical value the betting market collectively implies \u2014 the line where the no-vig P(over) crosses 50%, median across contributing sportsbooks. Market-implied arithmetic over sportsbook prices, not a forecast. DFS pick'em pricing excluded; only genuine two-way Over/Under pairs contribute. player_id is the stable cross-book id also served on /odds (null until the player has graded). Hobby+ (all paid tiers) full; free tier gets a redacted teaser.",
        "operationId": "getEventProjections",
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "football_nfl"
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "example": 25070
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional comma-separated market-key filter, e.g. player_pass_yds,player_receptions"
          }
        ],
        "responses": {
          "200": {
            "description": "Market-implied projections for the event",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "sport_key": {
                      "type": "string"
                    },
                    "home_team": {
                      "type": "string"
                    },
                    "away_team": {
                      "type": "string"
                    },
                    "commence_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "method": {
                      "type": "string"
                    },
                    "projections": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "market_key": {
                            "type": "string"
                          },
                          "player": {
                            "type": "string"
                          },
                          "player_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "projected_value": {
                            "type": "number",
                            "nullable": true
                          },
                          "consensus_over_prob": {
                            "type": "number",
                            "nullable": true
                          },
                          "books_contributing": {
                            "type": "integer"
                          },
                          "last_update": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "redacted": {
                      "type": "boolean"
                    },
                    "upgrade_url": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Sport or event not found"
          }
        },
        "tags": [
          "+EV"
        ],
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ]
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/ev/calc": {
      "get": {
        "tags": [
          "+EV"
        ],
        "summary": "Calc Event Ev",
        "description": "Calculate EV% for a user-supplied price against the event's no-vig\nfair anchor.\n\nUseful when you bet at a book PropLine doesn't carry directly\n(Caesars, BetMGM, Fanatics, BetUS, Hard Rock, etc.). The fair line\nis derived from the same Pinnacle-preferred anchor as the bulk\n`/ev` endpoint, so the math is identical \u2014 you're just plugging in\na price we don't ingest. Returns 404 with the available outcome\nnames when the (market, point, name) tuple doesn't match any\nfair-anchored line on the event.\n\nPro tier required.",
        "operationId": "calc_event_ev_v1_sports__sport_key__events__event_id__ev_calc_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "market",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Market key, e.g. 'h2h', 'spreads', 'totals', 'pitcher_strikeouts', 'player_pass_yds', 'player_anytime_td'. Full per-sport list: https://prop-line.com/docs \u2014 or GET /v1/sports/{sport_key}/events/{event_id}/markets for the keys a specific event carries.",
              "title": "Market"
            },
            "description": "Market key, e.g. 'h2h', 'spreads', 'totals', 'pitcher_strikeouts', 'player_pass_yds', 'player_anytime_td'. Full per-sport list: https://prop-line.com/docs \u2014 or GET /v1/sports/{sport_key}/events/{event_id}/markets for the keys a specific event carries."
          },
          {
            "name": "name",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Outcome name. For h2h/spreads: team name. For totals/props: 'Over' or 'Under'.",
              "title": "Name"
            },
            "description": "Outcome name. For h2h/spreads: team name. For totals/props: 'Over' or 'Under'."
          },
          {
            "name": "price",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "description": "American odds at your book, e.g. -118 or +145.",
              "title": "Price"
            },
            "description": "American odds at your book, e.g. -118 or +145."
          },
          {
            "name": "point",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "description": "Line/point for spreads, totals, and player props. Omit for h2h. For spreads, sign matters: e.g. -1.5 for the favorite.",
              "title": "Point",
              "nullable": true
            },
            "description": "Line/point for spreads, totals, and player props. Omit for h2h. For spreads, sign matters: e.g. -1.5 for the favorite."
          },
          {
            "name": "description",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Player name for player-prop markets; empty for game-line markets.",
              "default": "",
              "title": "Description"
            },
            "description": "Player name for player-prop markets; empty for game-line markets."
          },
          {
            "name": "devig",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "multiplicative",
                "shin"
              ],
              "default": "multiplicative",
              "title": "Devig"
            },
            "description": "How the anchor's vig is removed before the fair line is derived. 'multiplicative' (default) divides each implied probability by the booksum, spreading the overround evenly. 'shin' solves Shin's insider-trading model, which loads the overround onto the longshot side and corrects the favourite-longshot bias \u2014 negligible on a -110/-110 total, material on a +600 anytime scorer. The default is the only method the +EV backtest is measured on. Unknown values return 400.",
            "example": "shin"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvCalcOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/best-line": {
      "get": {
        "tags": [
          "+EV"
        ],
        "summary": "Get Event Best Line",
        "description": "Cross-book best-line lookup for a single event.\n\nFor each (market, player, line) tuple on the event, returns the\nsingle best American price across every comparable book PropLine\ningests, with the book name attached and an `all_prices` array\nsorted best-first (one row per book, each with `last_update`) so\nconsumers can render runner-ups without a second request.\n\n`markets` is an optional comma-separated filter (e.g.\n`markets=h2h,spreads,pitcher_strikeouts`). When omitted, every\nmarket on the event is included. `bookmakers` is an optional\ncomma-separated book-key filter (the-odds-api-compatible) \u2014 shop\nonly the books you hold accounts at.\n\nDFS pick'em books (PrizePicks, Sleeper, Dabble) are excluded \u2014\ntheir quotes aren't independently bettable payouts; Underdog is\nincluded only at its clean two-way lines (payout_multiplier ==\n1.0). Companion to `/ev`: best-line tells you which book has the\nhighest payout right now; +EV tells you whether that price beats a\nsharp no-vig fair line.\n\nHobby tier or higher sees prices. Free tier gets a redacted\nteaser: the full structure \u2014 every line, side, book identity, and\nthe best-first ranking \u2014 with every price nulled, plus\n`redacted: true` and an `upgrade_url`.",
        "operationId": "get_event_best_line_v1_sports__sport_key__events__event_id__best_line_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "title": "Markets",
              "nullable": true
            }
          },
          {
            "name": "bookmakers",
            "in": "query",
            "required": false,
            "description": "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all comparable books. Matches the-odds-api's `bookmakers` param.",
            "schema": {
              "type": "string",
              "title": "Bookmakers",
              "nullable": true
            }
          },
          {
            "name": "includeLinks",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "When true, adds a `link` field \u2014 the book's public event-page URL (plain navigation, no affiliate tagging) \u2014 per bookmaker block (odds endpoints) or per price row (/best-line). Links ship for Bovada, DraftKings, FanDuel, BetMGM, Kalshi, Polymarket and Smarkets; other books return null. Matches the-odds-api's includeLinks param. It also adds an `app_link` field (mobile app-open deep link; ProphetX only today, null elsewhere)."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventBestLineOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/scores": {
      "get": {
        "tags": [
          "Scores & Results"
        ],
        "summary": "List Scores",
        "description": "Game scores and status for recent events (last N days).\nFree tier \u2014 no Pro required.",
        "operationId": "list_scores_v1_sports__sport_key__scores_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "days_from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 3,
              "title": "Days From"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ScoreOut"
                  },
                  "title": "Response List Scores V1 Sports  Sport Key  Scores Get"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sports/baseball_mlb/grand-salami": {
      "get": {
        "tags": [
          "Scores & Results"
        ],
        "summary": "MLB Grand Salami",
        "description": "Synthetic daily Grand Salami \u2014 total runs scored across all MLB games on a given UTC date, plus each book's implied Grand Salami line (sum of per-game primary totals across our MLB books). No retail book quotes this as a single market. Free tier. Default date is today (UTC).",
        "operationId": "mlb_grand_salami_v1_sports_baseball_mlb_grand_salami_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "",
              "title": "Date (YYYY-MM-DD UTC)"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GrandSalamiOut"
                }
              }
            }
          },
          "400": {
            "description": "Invalid date format"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sports/hockey_nhl/daily-goals-total": {
      "get": {
        "tags": [
          "Scores & Results"
        ],
        "summary": "NHL Daily Goals Total",
        "description": "Hockey's equivalent of the MLB Grand Salami \u2014 total goals scored across all NHL games on a given UTC date (incl. OT/SO), plus each book's implied Daily Goals Total line (sum of per-game primary totals across our NHL books). No retail book quotes this as a single market. Free tier. Default date is today (UTC).",
        "operationId": "nhl_daily_goals_total_v1_sports_hockey_nhl_daily_goals_total_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "",
              "title": "Date (YYYY-MM-DD UTC)"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DailyGoalsTotalOut"
                }
              }
            }
          },
          "400": {
            "description": "Invalid date format"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/stats": {
      "get": {
        "tags": [
          "Scores & Results"
        ],
        "summary": "Get Event Stats",
        "description": "Actual player/team stats from box scores (book-agnostic).\nUse these to resolve props against any sportsbook's lines.\nLive during games for major US sports (MLB + WNBA now; NFL, NCAAF, NBA, NHL at season start): while status is in_progress, stats refresh ~every 90 seconds with cumulative in-game values; at final they are the official box score. Other sports populate at game completion.\nFree tier \u2014 no Pro required.",
        "operationId": "get_event_stats_v1_sports__sport_key__events__event_id__stats_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "stat_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "",
              "title": "Stat Type"
            },
            "description": "Comma-separated stat names to return; omit for all. The stored vocabulary is wider than the market list \u2014 a stat is recorded whenever the box score carries it, not only when a book prices it. MLB: hits, total_bases, singles, doubles, triples, home_runs, runs, rbis, walks, stolen_bases, at_bats, hits_runs_rbis, batter_strikeouts, strikeouts, earned_runs, hits_allowed, outs, pitcher_started. NBA/WNBA/NCAAB: points, rebounds, assists, threes, steals, blocks, turnovers, points_rebounds, points_assists, rebounds_assists, points_rebounds_assists, minutes. NHL: goals, points_nhl, shots_on_goal, blocked_shots, power_play_points, saves. NFL/NCAAF: passing_yards, passing_tds, passing_attempts, passing_completions, interceptions, longest_completion, rushing_yards, rushing_tds, rushing_attempts, longest_rush, receiving_yards, receiving_tds, receptions, longest_reception, pass_rush_yds, rush_reception_yds, anytime_td, first_td, sacks, fumbles, fumbles_lost, fumbles_recovered, field_goals_made, field_goals_attempted, longest_field_goal, extra_points_made, extra_points_attempted, kicking_points. Soccer: goals, assists, shots, shots_on_target, yellow_cards, red_cards, cards, first_goal, fouls_committed, fouls_suffered, offsides, saves, shots_faced, goals_conceded, own_goals, corners. Tennis: total_games, sets_won, set_1_games..set_5_games, aces, dblfaults, breakpts, breakpts_w, tiebreaks, tiebreaks_w, games_w, sets_w, matches_w and opp_* mirrors. Golf: strokes, score, total_score, birdies, eagles, pars, bogeys_ow, holes, rank, golf_position, golf_made_cut, tournament_winner. UFC: significant_strikes, takedowns, fight_winner, fight_method, fight_round, completed_rounds, went_distance. Team period scores are period_{code}_home / period_{code}_away."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventStatsOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/movement": {
      "get": {
        "tags": [
          "Odds"
        ],
        "summary": "Get Line Movement & Steam",
        "description": "Line movement derived from the snapshot tick history. Per (book, market, outcome): opening line, latest line, and the implied-probability / point shift between them. The steam[] array flags outcomes multiple books moved in the same direction \u2014 the sharp-money signal, computed across all 28 books. Hobby+ full; free tier redacted.",
        "operationId": "get_event_movement_v1_sports__sport_key__events__event_id__movement_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "h2h,spreads,totals",
              "title": "Markets"
            }
          },
          {
            "name": "bookmakers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all books. Matches the-odds-api's bookmakers param.",
            "example": "draftkings,fanduel"
          },
          {
            "name": "period",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "title": "Period",
              "nullable": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventMovementOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/context": {
      "get": {
        "tags": [
          "Scores & Results"
        ],
        "summary": "Get Game Context",
        "description": "Conditions a game is played under: probable starting pitchers, confirmed-lineup flag, the home-plate umpire, and first-pitch weather (outdoor / open-roof venues).\nThe same context is embedded in the /results response so every graded prop carries the conditions it settled against.\nFree tier. MLB today; weather extends to other outdoor sports next. 404 when no context is on file yet.",
        "operationId": "get_event_context_v1_sports__sport_key__events__event_id__context_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GameContextOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/results": {
      "get": {
        "tags": [
          "Scores & Results"
        ],
        "summary": "Get Event Results",
        "description": "Resolved prop outcomes with actual player stats.\nPro / Streaming / Enterprise: full resolution data.\nFree tier: redacted (shows structure but hides results).",
        "operationId": "get_event_results_v1_sports__sport_key__events__event_id__results_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "title": "Event Id"
            }
          },
          {
            "name": "markets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "",
              "title": "Markets"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventResultOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "404": {
            "description": "Not found. On an event id that previously worked, the event was likely merged into a canonical duplicate (the same fixture posted by multiple books under different team-name spellings). Merged ids resolve automatically and return the canonical event with a 200 \u2014 its `id` field is the id to store. A persistent 404 means re-fetch the event from /v1/sports/{sport}/events and use the current id."
          }
        }
      }
    },
    "/v1/sports/{sport_key}/players/{player_name}/history": {
      "get": {
        "tags": [
          "Players"
        ],
        "summary": "Get Player History",
        "description": "Resolved prop history for a player in a given market.\n\nReturns one entry per (event, bookmaker) pair with line + prices +\nresolution + actual value.\n\nPro / Streaming / Enterprise: full data.\nFree tier: structure only \u2014 resolution + actual_value redacted.",
        "operationId": "get_player_history_v1_sports__sport_key__players__player_name__history_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "player_name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Player Name"
            }
          },
          {
            "name": "market",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Market key, e.g. pitcher_strikeouts",
              "title": "Market"
            },
            "description": "Market key, e.g. pitcher_strikeouts"
          },
          {
            "name": "bookmaker",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Optional bookmaker filter",
              "default": "",
              "title": "Bookmaker"
            },
            "description": "Optional bookmaker filter"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 1,
              "default": 20,
              "title": "Limit"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerHistoryOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sports/{sport_key}/players/{player_name}/trends": {
      "get": {
        "tags": [
          "Players"
        ],
        "summary": "Get Player Trends",
        "description": "Aggregated hit-rate trends for a player \u2014 \"did X go over in N of his last M games?\" \u2014 across every market they have graded history in.\n\nFor each market: over/under/push splits across the last 5 / 10 / 20 / 50 graded games, the current over/under streak, average actual stat, and the most recent line. The verdict each game compares the player's real (book-agnostic) stat against one reference book's posted line; pushes are excluded from the over_pct denominator.\n\nDerived entirely from PropLine's prop resolution \u2014 the-odds-api has no equivalent. Paid tiers (Hobby and above): full data. Free tier: market list + graded-game counts, with the rates redacted.",
        "operationId": "get_player_trends_v1_sports__sport_key__players__player_name__trends_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Sport Key"
            },
            "description": "Sport key. the-odds-api names are accepted as aliases (americanfootball_nfl -> football_nfl, icehockey_nhl -> hockey_nhl, soccer_spain_la_liga -> soccer_la_liga, ...), but only where the competition is identical; otherwise the request returns 404 with a structured body carrying `reason` and `did_you_mean`. Canonical list: GET /v1/sports."
          },
          {
            "name": "player_name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Player Name"
            }
          },
          {
            "name": "market",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Optional market key to filter to one market",
              "default": "",
              "title": "Market"
            },
            "description": "Optional market key to filter to one market"
          },
          {
            "name": "dfs_odds_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "standard",
                "goblin",
                "demon"
              ],
              "default": "",
              "title": "DFS Odds Type"
            },
            "description": "Optional PrizePicks pick-em flavor filter: 'standard', 'goblin', or 'demon'. When set, trends are computed against that flavor's PrizePicks line only (e.g. compare a player's goblin-line hit-rate vs his standard-line trend). Omitted = cross-book behavior (reference line from the preferred-book list, flavor ignored). PrizePicks flavor tagging began 2026-06-16, so per-flavor trends only have depth from that date forward."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerTrendsOut"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/exports/resolved-props": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export Resolved Props",
        "description": "Stream resolved prop outcomes as CSV. Pro+ tier required.\n\nOne row per (event, market, bookmaker, outcome). Includes the line,\nprice, resolution (won/lost/push/void), and actual stat value \u2014 the\nexact data model builders need for CLV and backtesting. Every row also carries both ends of the line move: the closing line\n(`closing_price` / `closing_point` / `closing_at` = the last snapshot at\nor before commence_time) and the opening line (`opening_price` /\n`opening_point` / `opening_at` = the first snapshot in the 14 days\nbefore kickoff). That makes a full CLV study one download rather than\none history call per event, and it is something the-odds-api cannot\noffer since they don't resolve props. Note `closing_point` is distinct\nfrom `line`, which is the outcome's own current point.\n\nNew columns are always appended immediately before `customer_token`, so\npositional parsers written against an earlier column set keep working.\n\nHistorical depth is tier-gated: Pro = last 90 days, Streaming = last\n365 days, Enterprise = unbounded. The effective floor is reflected in\nthe `X-PropLine-Export-Window-Start` response header.\n\nDaily call cap is also tier-gated: Pro = 50/day, Streaming Lite =\n100/day, Streaming = 200/day, Enterprise = uncapped. The cap counts\nwhole calls, not rows, and `sport` is required, so a multi-sport pull\nis one call per sport \u2014 the efficient pattern is one call per sport\nover the widest date range you need. Remaining budget is surfaced on\nevery successful response in the `X-PropLine-Export-Daily-Remaining`\nheader; over-cap calls return 429 with a `Retry-After` set to the\nseconds until 00:00 UTC.\n\nEvery row carries a `dfs_odds_type` column \u2014 the PrizePicks projection\ntier (`standard` / `goblin` / `demon`); blank for traditional\nsportsbooks.\n\nRows also carry `outcome_id` (our stable id for the priced selection \u2014 the same value `/odds?includeBookIds=true`, `/odds/history`, `/odds/closing` and webhook payloads carry) and `player_id` (matches `/odds` and `/results`).\n\nArchive coverage is stated in-band on every response: the `X-PropLine-Archive-Starts` header carries the sport's earliest held date (`none` if the sport has no data yet), and `X-PropLine-Archive-Notice` is set when the requested window ends before the archive begins, meaning the export can only be empty.",
        "operationId": "export_resolved_props_v1_exports_resolved_props_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Sport key (e.g. baseball_mlb). Required.",
              "title": "Sport"
            },
            "description": "Sport key (e.g. baseball_mlb). Required."
          },
          {
            "name": "market",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Optional market filter, e.g. pitcher_strikeouts",
              "default": "",
              "title": "Market"
            },
            "description": "Optional market filter, e.g. pitcher_strikeouts"
          },
          {
            "name": "bookmaker",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Optional bookmaker filter",
              "default": "",
              "title": "Bookmaker"
            },
            "description": "Optional bookmaker filter"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "description": "ISO datetime lower bound on resolved_at",
              "title": "Since",
              "nullable": true
            },
            "description": "ISO datetime lower bound on resolved_at"
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "description": "ISO datetime upper bound on resolved_at",
              "title": "Until",
              "nullable": true
            },
            "description": "ISO datetime upper bound on resolved_at"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/exports/odds-history": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export Odds History",
        "description": "Stream the full line-movement time-series as CSV \u2014 every recorded odds snapshot (price + line, per book, including period markets) for every outcome, not just the closing line. One row per (outcome, snapshot).\n\nThis is the raw tick history that no subscription tier can pull in bulk: Pro/Streaming get per-event `/odds/history` and the resolved-props export (closing line only), but the bulk snapshot firehose is exclusive to the one-time Historical Backfill pass and Enterprise. Backfill pass = trailing 2-year lookback (Enterprise unbounded); uncapped calls.\n\nA full all-sport, all-history pull is many GB \u2014 page month-by-month with `since`/`until` to keep any single request bounded.\n\nColumns: event_id, sport_key, commence_time, home_team, away_team, market, period, bookmaker, player_name, outcome_name, recorded_at, price_american, price_decimal, point, book_updated_at, dfs_odds_type, customer_token (dfs_odds_type = PrizePicks tier: standard / goblin / demon; blank for sportsbooks).\n\nArchive coverage is stated in-band on every response: the `X-PropLine-Archive-Starts` header carries the sport's earliest held date (`none` if the sport has no data yet), and `X-PropLine-Archive-Notice` is set when the requested window ends before the archive begins, meaning the export can only be empty.",
        "operationId": "export_odds_history_v1_exports_odds_history_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "description": "Sport key (e.g. baseball_mlb). Required.",
              "title": "Sport"
            },
            "description": "Sport key (e.g. baseball_mlb). Required."
          },
          {
            "name": "market",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Optional market filter, e.g. pitcher_strikeouts",
              "default": "",
              "title": "Market"
            },
            "description": "Optional market filter, e.g. pitcher_strikeouts"
          },
          {
            "name": "bookmaker",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Optional bookmaker filter",
              "default": "",
              "title": "Bookmaker"
            },
            "description": "Optional bookmaker filter"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "description": "ISO datetime lower bound on recorded_at",
              "title": "Since",
              "nullable": true
            },
            "description": "ISO datetime lower bound on recorded_at"
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "description": "ISO datetime upper bound on recorded_at",
              "title": "Until",
              "nullable": true
            },
            "description": "ISO datetime upper bound on recorded_at"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/exports/sample": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export Sample",
        "description": "Public sample: last 7 days of MLB pitcher_strikeouts as CSV.\n\nNo auth required \u2014 this is a SEO/marketing lead magnet. Capped to\nkeep it a taste, not a full dataset. For full access, Pro tier\nusers hit /v1/exports/resolved-props.",
        "operationId": "export_sample_v1_exports_sample_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/markets/hit-rates": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Get Market Hit Rates",
        "description": "Per-market daily Over hit rates over the last N days.\n\nResponse shape:\n    {\n      \"days\": 28,\n      \"bookmaker\": \"bovada\",\n      \"markets\": {\n        \"pitcher_strikeouts\": [\n          {\"date\": \"2026-04-01\", \"total\": 14, \"won\": 6},\n          ...\n        ],\n        \"batter_total_bases\": [...],\n        ...\n      }\n    }\n\n`total` and `won` are scoped to graded **Over** outcomes only \u2014 the\nsame denominator the cross-sport accuracy panel uses, so the numbers\nline up. `total` excludes voids; `won` is the subset where the actual\nstat exceeded the line.",
        "operationId": "get_market_hit_rates_v1_markets_hit_rates_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 60,
              "minimum": 1,
              "description": "Look-back window in days",
              "default": 28,
              "title": "Days"
            },
            "description": "Look-back window in days"
          },
          {
            "name": "bookmaker",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "description": "Bookmaker to aggregate. Defaults to bovada \u2014 the canonical book for /today.",
              "default": "bovada",
              "title": "Bookmaker"
            },
            "description": "Bookmaker to aggregate. Defaults to bovada \u2014 the canonical book for /today."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dfs/payouts": {
      "get": {
        "tags": [
          "DFS"
        ],
        "summary": "DFS Payout Schedule + Breakeven",
        "description": "PrizePicks Power Play (all legs must hit) and Flex Play (partial payouts) entry payout schedule for 2-6 legs, plus the per-leg breakeven win probability for each play type and leg count. Pass `?leg_win_prob=0.58` to also get `expected_return` (per $1 staked) and `is_plus_ev` at that assumed per-leg hit rate. Pure reference math \u2014 free for any authenticated key. NOTE (see the `disclaimer` field): these are standard published payouts that PrizePicks can change; demon/goblin per-pick modifiers are NOT in PrizePicks's feed so they are not reflected; breakeven assumes independent legs (real legs are correlated, so treat it as a floor).",
        "operationId": "dfs_payouts_v1_dfs_payouts_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "prizepicks",
              "title": "Platform",
              "description": "DFS platform. Only 'prizepicks' today."
            },
            "description": "DFS platform. Only 'prizepicks' today."
          },
          {
            "name": "leg_win_prob",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "maximum": 1.0,
              "minimum": 0.0,
              "title": "Leg Win Prob",
              "description": "Assumed per-leg win probability. When supplied, each play also returns expected_return and is_plus_ev."
            },
            "description": "Assumed per-leg win probability. When supplied, each play also returns expected_return and is_plus_ev."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "404": {
            "description": "Unknown platform"
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/markets/resolution-summary": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Get Resolution Summary",
        "description": "Factual volume of graded player props over the last N days.\n\nAggregated counts only (no individual prop data). Every outcome counted here was graded against the real box score \u2014 a moat statement nobody else (the-odds-api, OddsJam) can make at any tier. This is a volume statement, never a profitability one.\n\nResponse shape:\n    {\n      \"days\": 30,\n      \"total_graded\": 812334,\n      \"total_settled\": 798901,\n      \"events_graded\": 4120,\n      \"sports_covered\": 33,\n      \"by_sport\": [{\"sport_key\",\"title\",\"graded\",\"events\"}...],\n      \"top_markets\": [{\"market_key\",\"graded\"}...]\n    }",
        "operationId": "get_resolution_summary_v1_markets_resolution_summary_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 90,
              "minimum": 1,
              "description": "Look-back window in days",
              "default": 30,
              "title": "Days"
            },
            "description": "Look-back window in days"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/clv/grade": {
      "post": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "Grade placed bets against the closing line (CLV)",
        "description": "POST the bets you actually placed; each comes back with its closing line, the de-vigged closing fair probability, CLV, and the graded result once the game settles. Closing line value is the only durable proxy for whether a bettor has edge.\n\nStateless \u2014 nothing is stored.\n\nTwo CLV numbers are returned deliberately. `clv_pct` is price-vs-price: familiar and quotable, but vig-blind. `ev_vs_close_pct` scores your price against the DE-VIGGED close and is the honest one \u2014 a -110 taken into a -105/-115 close beat the price but not the fair line.\n\nThe de-vig uses the sharpest book quoting that line at close (Pinnacle first, then Polymarket / Kalshi / Bovada / Smarkets, falling back to your own book), reported as `fair_source`. De-vigging the book you bet at always returns a negative number because you paid its hold.\n\nMatching is fail-closed: a bet that cannot be pinned to exactly one stored outcome returns `matched: false` with an `unmatched_reason` rather than a confident wrong match. Lines match by equality, never nearest-value.\n\nBets on events that have not started carry `closing_is_final: false` and are excluded from the summary averages. Max 500 bets per request.\n\nHobby+ gets full data; free tier is redacted (structure and match verdicts visible, every number nulled).",
        "operationId": "gradeClv",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "maxItems": 500,
                "items": {
                  "type": "object",
                  "required": [
                    "sport_key",
                    "event_id",
                    "market",
                    "bookmaker",
                    "selection",
                    "price"
                  ],
                  "properties": {
                    "ref": {
                      "type": "string",
                      "nullable": true,
                      "description": "Caller-supplied id echoed back, so rows can be aligned without relying on list order."
                    },
                    "sport_key": {
                      "type": "string",
                      "example": "baseball_mlb"
                    },
                    "event_id": {
                      "type": "integer",
                      "example": 150791
                    },
                    "market": {
                      "type": "string",
                      "example": "batter_hits_runs_rbis"
                    },
                    "bookmaker": {
                      "type": "string",
                      "example": "lowvig"
                    },
                    "selection": {
                      "type": "string",
                      "description": "Player name for a prop, team name for a game line.",
                      "example": "Drake Baldwin"
                    },
                    "side": {
                      "type": "string",
                      "nullable": true,
                      "description": "Over/Under for a two-way market; omit for YES-only props.",
                      "example": "Under"
                    },
                    "point": {
                      "type": "number",
                      "nullable": true,
                      "example": 0.5
                    },
                    "period": {
                      "type": "string",
                      "nullable": true,
                      "description": "Canonical period code (q1, h1, p1, f5). Omit for full game."
                    },
                    "price": {
                      "type": "integer",
                      "description": "American odds you took.",
                      "example": 145
                    },
                    "stake": {
                      "type": "number",
                      "nullable": true,
                      "description": "Defaults to 1 unit when computing profit_units."
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Graded bets plus a portfolio summary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "summary": {
                      "type": "object",
                      "properties": {
                        "bets": {
                          "type": "integer"
                        },
                        "matched": {
                          "type": "integer"
                        },
                        "unmatched": {
                          "type": "integer"
                        },
                        "graded": {
                          "type": "integer"
                        },
                        "pending": {
                          "type": "integer",
                          "description": "Matched bets whose event has not started; excluded from the averages."
                        },
                        "avg_clv_pct": {
                          "type": "number",
                          "nullable": true
                        },
                        "avg_ev_vs_close_pct": {
                          "type": "number",
                          "nullable": true
                        },
                        "beat_close_pct": {
                          "type": "number",
                          "nullable": true
                        },
                        "profit_units": {
                          "type": "number",
                          "nullable": true
                        }
                      }
                    },
                    "bets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "ref": {
                            "type": "string",
                            "nullable": true
                          },
                          "matched": {
                            "type": "boolean"
                          },
                          "unmatched_reason": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "event_not_found",
                              "no_market_for_key",
                              "no_outcome_for_selection",
                              "ambiguous_selection",
                              "no_closing_snapshot",
                              null
                            ]
                          },
                          "closing_price": {
                            "type": "integer",
                            "nullable": true
                          },
                          "closing_point": {
                            "type": "number",
                            "nullable": true
                          },
                          "closing_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "closing_is_stale": {
                            "type": "boolean"
                          },
                          "closing_is_final": {
                            "type": "boolean"
                          },
                          "fair_source": {
                            "type": "string",
                            "nullable": true
                          },
                          "closing_fair_prob": {
                            "type": "number",
                            "nullable": true
                          },
                          "clv_pct": {
                            "type": "number",
                            "nullable": true
                          },
                          "ev_vs_close_pct": {
                            "type": "number",
                            "nullable": true
                          },
                          "beat_close": {
                            "type": "boolean",
                            "nullable": true
                          },
                          "resolution": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "won",
                              "lost",
                              "push",
                              "void",
                              null
                            ]
                          },
                          "actual_value": {
                            "type": "number",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "redacted": {
                      "type": "boolean"
                    },
                    "upgrade_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "devig_method": {
                      "type": "string",
                      "description": "Echo of the devig query param."
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "More than 500 bets in one request."
          }
        },
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "devig",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "multiplicative",
                "shin"
              ],
              "default": "multiplicative"
            },
            "description": "How the closing anchor's vig is removed before closing_fair_prob and ev_vs_close_pct are computed. 'multiplicative' (default) divides by the booksum; 'shin' loads the overround onto the longshot (corrects the favourite-longshot bias). Same vocabulary as /ev; echoed as devig_method.",
            "example": "shin"
          }
        ]
      }
    },
    "/v1/sports/{sport_key}/events/{event_id}/sgp": {
      "post": {
        "tags": [
          "Events & Odds"
        ],
        "summary": "Price a same-game parlay at the book's own correlated odds",
        "description": "POST two to ten legs from ONE event and get back the book's own price for that exact slip \u2014 what a FanDuel customer would be offered for it at that moment, not a model of it. Beside it: `independent_price` (the product of the live single-leg prices) and `correlation_factor` (their ratio \u2014 the correlation the book is charging, below 1, or paying, above 1, for).\n\nBook-native. `bookmaker` is `fanduel` (its own betslip pricer), `draftkings` (its SGP widget's pricer; legs are named by the nash selection id `?includeBookIds=true` serves, and `independent_price` is the product of our stored DK single prices) or `betonlineag` / `lowvig` (the Sportcast engine both Chico brands embed, same builder price); an unsupported value is a 422 `unsupported_bookmaker`. For the Chico books legs are matched against the book's own SGP board, wider than the game feed we store, so `independent_price` is null when a leg has no stored single price.\n\nLegs are named exactly as `/odds` names them (market, name, description, point, period), so a leg is a copy-paste from an odds response; `book_outcome_id` from `?includeBookIds=true` is accepted as a shortcut.\n\nMatching is fail-closed: a leg that does not pin to exactly one stored outcome is a 422 `leg_unmatched` naming the leg \u2014 an Over with no point on an event carrying two total lines is refused, not guessed. `quoted: false` means the book will not offer that combination as a same-game parlay; refused legs carry the book's own `failure_code`. 503 `book_unavailable` when the book's pricer does not answer.\n\n`bookmaker=all` quotes every supported book on the same legs and returns `quotes` (one full single-book response per book that answered), `errors` (each book that could not price the slip, with the status + detail its own call would have returned) and `best_bookmaker` (the quoted book paying the most \u2014 the smallest correlation reduction on identical legs).\n\nEvery priced call is a live request to the book; quotes for an identical slip are shared for 15 seconds. Hobby+ gets the price; free tier gets the matched legs with every price nulled and never triggers a book call.",
        "operationId": "priceSameGameParlay",
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "baseball_mlb"
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "example": 150791
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "legs"
                ],
                "properties": {
                  "bookmaker": {
                    "type": "string",
                    "default": "fanduel",
                    "description": "Book to price at: fanduel, draftkings, betonlineag or lowvig \u2014 or `all` to quote every supported book on the same legs in one call. With `all` the response is `{quotes: [<one single-book response per book that answered>], errors: [{bookmaker, status, error, detail}], best_bookmaker}`; `best_bookmaker` is the quoted book paying the most, i.e. the one charging the smallest correlation reduction on these legs.",
                    "enum": [
                      "fanduel",
                      "draftkings",
                      "betonlineag",
                      "lowvig",
                      "all"
                    ]
                  },
                  "legs": {
                    "type": "array",
                    "minItems": 2,
                    "maxItems": 10,
                    "items": {
                      "type": "object",
                      "properties": {
                        "market": {
                          "type": "string",
                          "nullable": true,
                          "description": "Market key as served by /odds.",
                          "example": "h2h"
                        },
                        "name": {
                          "type": "string",
                          "nullable": true,
                          "description": "Outcome name as served by /odds: team name, Over/Under, or the player for YES-only props.",
                          "example": "St. Louis Cardinals"
                        },
                        "description": {
                          "type": "string",
                          "description": "Outcome description as served by /odds (the player on a two-way prop); empty for game lines.",
                          "example": ""
                        },
                        "point": {
                          "type": "number",
                          "nullable": true,
                          "description": "Line exactly as served by /odds. Omit for h2h and YES-only props."
                        },
                        "period": {
                          "type": "string",
                          "nullable": true,
                          "description": "Canonical period code (q1, h1, f5). Omit for full game."
                        },
                        "team": {
                          "type": "string",
                          "nullable": true,
                          "description": "For a TEAM total: the team, as /odds serves it in the market's `team` field. Omit for the game total \u2014 a totals leg with no team matches only the team-less market."
                        },
                        "book_outcome_id": {
                          "type": "string",
                          "nullable": true,
                          "description": "The book's own id from ?includeBookIds=true. When given, the other fields are ignored. On betonlineag / lowvig this is Sportcast's settlement id (e.g. MatchWinner_Home)."
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "bookmaker": "fanduel",
                "legs": [
                  {
                    "market": "h2h",
                    "name": "St. Louis Cardinals"
                  },
                  {
                    "market": "batter_1plus_hits",
                    "name": "Freddie Freeman",
                    "description": "Freddie Freeman"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The book's SGP quote for the slip.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "sport_key": {
                      "type": "string"
                    },
                    "home_team": {
                      "type": "string"
                    },
                    "away_team": {
                      "type": "string"
                    },
                    "commence_time": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "bookmaker": {
                      "type": "string"
                    },
                    "bookmaker_title": {
                      "type": "string"
                    },
                    "legs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer"
                          },
                          "market": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "point": {
                            "type": "number",
                            "nullable": true
                          },
                          "period": {
                            "type": "string",
                            "nullable": true
                          },
                          "team": {
                            "type": "string",
                            "nullable": true,
                            "description": "The team a team total is scoped to, as /odds serves it; null for the game total."
                          },
                          "book_outcome_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "price": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The last price PropLine stored for this leg (American)."
                          },
                          "book_price": {
                            "type": "integer",
                            "nullable": true,
                            "description": "The single-leg price the book quoted in the same call as the parlay \u2014 the live number."
                          },
                          "accepted": {
                            "type": "boolean",
                            "nullable": true
                          },
                          "failure_code": {
                            "type": "string",
                            "nullable": true,
                            "description": "The book's own refusal code when it would not take this leg in the slip (INVALID_COMBINATION, BET_TYPE_NOT_SUPPORTED, ...)."
                          }
                        }
                      }
                    },
                    "quoted": {
                      "type": "boolean",
                      "nullable": true,
                      "description": "True when the book priced the FULL combination; false when a leg was refused or the combination is not offered as an SGP; null on the free tier."
                    },
                    "sgp_price": {
                      "type": "integer",
                      "nullable": true,
                      "description": "The book's correlated parlay price (American)."
                    },
                    "sgp_price_decimal": {
                      "type": "number",
                      "nullable": true
                    },
                    "independent_price": {
                      "type": "integer",
                      "nullable": true,
                      "description": "Product of the live single-leg prices, as American odds."
                    },
                    "independent_price_decimal": {
                      "type": "number",
                      "nullable": true
                    },
                    "correlation_factor": {
                      "type": "number",
                      "nullable": true,
                      "description": "sgp_price_decimal / independent_price_decimal."
                    },
                    "priced_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "redacted": {
                      "type": "boolean"
                    },
                    "upgrade_url": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                },
                "example": {
                  "id": "150791",
                  "sport_key": "baseball_mlb",
                  "home_team": "Los Angeles Dodgers",
                  "away_team": "St. Louis Cardinals",
                  "commence_time": "2026-09-03T02:11:00Z",
                  "bookmaker": "fanduel",
                  "bookmaker_title": "FanDuel",
                  "quoted": true,
                  "sgp_price": 592,
                  "sgp_price_decimal": 6.9205,
                  "independent_price": 322,
                  "independent_price_decimal": 4.2231,
                  "correlation_factor": 1.6387,
                  "priced_at": "2026-09-02T18:41:07Z",
                  "legs": [
                    {
                      "index": 0,
                      "market": "h2h",
                      "name": "St. Louis Cardinals",
                      "description": "",
                      "point": null,
                      "period": null,
                      "book_outcome_id": "717.184106545:50291",
                      "price": 205,
                      "book_price": 205,
                      "accepted": true,
                      "failure_code": null
                    },
                    {
                      "index": 1,
                      "market": "batter_1plus_hits",
                      "name": "Freddie Freeman",
                      "description": "Freddie Freeman",
                      "point": null,
                      "period": null,
                      "book_outcome_id": "717.184289530:10885578",
                      "price": -260,
                      "book_price": -260,
                      "accepted": true,
                      "failure_code": null
                    }
                  ],
                  "redacted": false,
                  "upgrade_url": null
                }
              }
            }
          },
          "404": {
            "description": "Unknown sport or event, or (`event_not_at_book`) the event is not on that book's SGP board."
          },
          "422": {
            "description": "A leg could not be matched to exactly one stored outcome (`leg_unmatched`), or the bookmaker is unsupported."
          },
          "503": {
            "description": "The book's pricer did not answer (`book_unavailable`)."
          }
        },
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ]
      }
    },
    "/v1/freshness": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Per-book data freshness",
        "description": "Last update and staleness in seconds for every book, split by market class (game lines vs player props). Free, no auth. Check it before trusting any book's prices \u2014 a book that has stopped delivering shows here first.",
        "operationId": "getFreshness",
        "responses": {
          "200": {
            "description": "Freshness per bookmaker.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "as_of": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "stale_threshold_seconds": {
                      "type": "integer"
                    },
                    "bookmakers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "market_count": {
                            "type": "integer"
                          },
                          "latest_update": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "staleness_seconds": {
                            "type": "integer"
                          },
                          "is_stale": {
                            "type": "boolean"
                          },
                          "worst_staleness_seconds": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Max of the per-class worst_staleness_seconds."
                          },
                          "suspended_market_count": {
                            "type": "integer"
                          },
                          "market_classes": {
                            "type": "object",
                            "description": "Per market-class split (game_lines, props).",
                            "additionalProperties": {
                              "type": "object",
                              "properties": {
                                "market_count": {
                                  "type": "integer"
                                },
                                "latest_update": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                },
                                "staleness_seconds": {
                                  "type": "integer",
                                  "nullable": true
                                },
                                "active_market_count": {
                                  "type": "integer",
                                  "description": "Non-suspended markets on the active board (events from 6h ago to 48h out)."
                                },
                                "oldest_market_update": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true,
                                  "description": "Oldest non-suspended market's last update on the active board."
                                },
                                "worst_staleness_seconds": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "Age of the oldest non-suspended market on the active board - the pessimistic twin of staleness_seconds. Suspended markets are excluded: a flagged withdrawal is not a capture hole."
                                },
                                "suspended_market_count": {
                                  "type": "integer",
                                  "description": "Markets on the active board the book has withdrawn (suspended_at set on /odds)."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/sports/{sport_key}/players/{player_name}/games": {
      "get": {
        "tags": [
          "Players"
        ],
        "summary": "Player game log & head-to-head",
        "description": "A player's recent games with every raw box-score stat per game - one call instead of one request per event. Build L5/L10/L20, season splits, charts and H2H from the raw rows. Reads the RAW-STATS archive rather than graded-prop history, so it covers every game with a box score on file, including games no sportsbook priced; a 'last 10 games' window here is genuinely the last 10 games, unlike one derived from /history or /trends. Carries no line, price or grade - use /trends for hit rates against a posted line. Free tier.",
        "operationId": "get_player_game_log_v1_sports__sport_key__players__player_name__games_get",
        "security": [
          {
            "APIKeyHeader": []
          },
          {
            "APIKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport_key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "baseball_mlb"
          },
          {
            "name": "player_name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "Aaron Judge",
            "description": "Case-insensitive prefix match, accent-insensitive."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "opponent",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": ""
            },
            "description": "Head-to-head filter. Accepts a full name, nickname or abbreviation (\"Boston Red Sox\", \"Red Sox\", \"BOS\"). The limit applies AFTER this filter, so ?opponent=BOS&limit=10 returns the last 10 meetings rather than the Boston games among the last 10 games. Not capped to the current season."
          },
          {
            "name": "stat_type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": ""
            },
            "description": "Comma-separated stat names to return; omit for all. Vocabulary is per-sport - see the /stats endpoint."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlayerGameLogOut"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "BestLineOut": {
        "description": "One (market_key, player, point) tuple with the best price per side.",
        "properties": {
          "market_key": {
            "type": "string",
            "title": "Market Key"
          },
          "description": {
            "type": "string",
            "description": "Player name (props) or empty string (game lines).",
            "title": "Description"
          },
          "point": {
            "type": "number",
            "description": "Shared O/U or h2h line. Null for h2h moneylines.",
            "title": "Point",
            "nullable": true
          },
          "sides": {
            "additionalProperties": {
              "$ref": "#/components/schemas/BestLineSideOut"
            },
            "type": "object",
            "description": "Map of side name (e.g. \"Over\"/\"Under\" or team name) to best price + alternatives.",
            "title": "Sides"
          }
        },
        "type": "object",
        "required": [
          "market_key",
          "description",
          "point",
          "sides"
        ],
        "title": "BestLineOut"
      },
      "BestLineSideOut": {
        "description": "Best price + every alternative for one side of a market line.",
        "properties": {
          "best": {
            "$ref": "#/components/schemas/BestPriceOut",
            "description": "The highest American price across the books returning a valid quote for this side."
          },
          "all_prices": {
            "items": {
              "$ref": "#/components/schemas/BestPriceOut"
            },
            "type": "array",
            "description": "Every book's price, sorted best-first (descending price), one row per book. DFS pick'em books (PrizePicks, Sleeper, Dabble) are excluded; Underdog only at clean two-way lines (payout_multiplier == 1.0).",
            "title": "All Prices"
          }
        },
        "type": "object",
        "required": [
          "best",
          "all_prices"
        ],
        "title": "BestLineSideOut"
      },
      "BestPriceOut": {
        "description": "A single book's price at a given line.",
        "properties": {
          "book": {
            "type": "string",
            "title": "Book"
          },
          "book_title": {
            "type": "string",
            "title": "Book Title"
          },
          "price": {
            "type": "integer",
            "description": "American odds. Null only on free-tier redacted responses (book identity and ranking stay visible; the price is paid).",
            "title": "Price",
            "nullable": true
          },
          "last_update": {
            "type": "string",
            "format": "date-time",
            "description": "When this book last refreshed the market carrying this price \u2014 discount stale quotes.",
            "title": "Last Update",
            "nullable": true
          },
          "link": {
            "type": "string",
            "description": "Public event-page URL at this book. Only set when the request passed includeLinks=true and the book has a verified URL template.",
            "title": "Link",
            "nullable": true
          },
          "app_link": {
            "type": "string",
            "nullable": true,
            "description": "Mobile app-open deep link at this book \u2014 opens the book's native app on this fixture (with an app-store fallback for users without the app), vs `link` which is the desktop web event page. Only set when includeLinks=true and the book has a verified app-link template. ProphetX only today (an AppsFlyer OneLink); null for every other book."
          },
          "liquidity_updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the exchange last touched the resting order behind `liquidity` \u2014 the BOOK'S own timestamp, not our observation time. ProphetX does not continuously re-publish resting size, so a size can be materially stale while the book has moved on (measured 2026-08-23: median 137s, 27% of rungs older than 20 minutes). Drop or discount sizes older than your own threshold. Null for books without resting size."
          },
          "liquidity": {
            "type": "number",
            "nullable": true,
            "description": "Dollars a bettor can actually stake at the quoted price. Exchange books publishing resting size (ProphetX and Novig) plus Pinnacle, where it is the book's posted max risk stake on the market (its own cap on what one bettor can risk at that price); null for every other book, and for an exchange quote whose size the feed omitted. On a P2P exchange the best price is often a thin offer with only a few dollars behind it \u2014 filter or discount small values before treating the price as bettable. Refreshed every poll cycle independently of price movement. Exchange size changes never appear in /odds/history; a Pinnacle limit change does (its own snapshot row, price unchanged), and /odds/closing carries opening_liquidity beside liquidity. Neither fires line_movement webhooks. Pair with liquidity_updated_at: ProphetX does not continuously re-publish resting size, so a rung keeps its last-known amount until something happens to it (~27% of served rungs are more than 20 minutes stale)."
          }
        },
        "type": "object",
        "required": [
          "book",
          "book_title",
          "price"
        ],
        "title": "BestPriceOut"
      },
      "BookmakerHistoryOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "markets": {
            "items": {
              "$ref": "#/components/schemas/MarketHistoryOut"
            },
            "type": "array",
            "title": "Markets"
          }
        },
        "type": "object",
        "required": [
          "key",
          "title",
          "markets"
        ],
        "title": "BookmakerHistoryOut"
      },
      "BookmakerOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "last_update": {
            "type": "string",
            "format": "date-time",
            "title": "Last Update",
            "nullable": true
          },
          "markets": {
            "items": {
              "$ref": "#/components/schemas/MarketOut"
            },
            "type": "array",
            "title": "Markets"
          },
          "link": {
            "type": "string",
            "description": "Public event-page URL at this book. Only set when the request passed includeLinks=true and the book has a verified URL template.",
            "title": "Link",
            "nullable": true
          },
          "app_link": {
            "type": "string",
            "nullable": true,
            "description": "Mobile app-open deep link at this book \u2014 opens the book's native app on this fixture (app-store fallback otherwise), vs `link` which is the desktop web event page. Only set when includeLinks=true and the book has a verified app-link template. ProphetX only today (an AppsFlyer OneLink); null for every other book. PropLine-specific, not a the-odds-api field."
          },
          "book_event_id": {
            "type": "string",
            "nullable": true,
            "description": "This book's own event identifier (Kalshi event ticker, DraftKings/BetMGM numeric event id, Pinnacle matchup id, PrizePicks game id, ...). Only set when the request passed includeBookIds=true and this book publishes a stable id."
          },
          "pregame_only": {
            "type": "boolean",
            "default": false,
            "description": "True when the event is LIVE and this book does not price it in play. Its prices are the last PREGAME quote and will not move again until the game ends \u2014 they are not a live price. This is the one staleness class `suspended_at` cannot show: that signal infers a withdrawal from a market being absent from a poll, and these books are never polled for a fixture once it starts. Always false before kickoff. Annotation only \u2014 the prices are still served, because on the DFS books the frozen pregame line is the one the bet settles against."
          }
        },
        "type": "object",
        "required": [
          "key",
          "title",
          "markets"
        ],
        "title": "BookmakerOut"
      },
      "BookmakerResultOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "markets": {
            "items": {
              "$ref": "#/components/schemas/ResolvedMarketOut"
            },
            "type": "array",
            "title": "Markets"
          }
        },
        "type": "object",
        "required": [
          "key",
          "title",
          "markets"
        ],
        "title": "BookmakerResultOut"
      },
      "EvCalcOut": {
        "properties": {
          "market": {
            "type": "string",
            "title": "Market"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "point": {
            "type": "number",
            "title": "Point",
            "nullable": true
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "price": {
            "type": "integer",
            "title": "Price"
          },
          "fair_source": {
            "type": "string",
            "title": "Fair Source"
          },
          "devig_method": {
            "type": "string",
            "title": "Devig Method",
            "default": "multiplicative"
          },
          "fair_prob": {
            "type": "number",
            "title": "Fair Prob"
          },
          "implied_prob": {
            "type": "number",
            "title": "Implied Prob"
          },
          "ev_pct": {
            "type": "number",
            "title": "Ev Pct"
          },
          "is_plus_ev": {
            "type": "boolean",
            "title": "Is Plus Ev"
          }
        },
        "type": "object",
        "required": [
          "market",
          "name",
          "point",
          "description",
          "price",
          "fair_source",
          "fair_prob",
          "implied_prob",
          "ev_pct",
          "is_plus_ev"
        ],
        "title": "EvCalcOut",
        "description": "User-supplied price evaluated against the event's no-vig fair anchor."
      },
      "EvLineOut": {
        "properties": {
          "market_key": {
            "type": "string",
            "title": "Market Key"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "point": {
            "type": "number",
            "title": "Point",
            "nullable": true
          },
          "fair_source": {
            "type": "string",
            "title": "Fair Source"
          },
          "fair_probs": {
            "additionalProperties": {
              "type": "number"
            },
            "type": "object",
            "title": "Fair Probs"
          },
          "outcomes": {
            "items": {
              "$ref": "#/components/schemas/EvOutcomeOut"
            },
            "type": "array",
            "title": "Outcomes"
          }
        },
        "type": "object",
        "required": [
          "market_key",
          "description",
          "point",
          "fair_source",
          "fair_probs",
          "outcomes"
        ],
        "title": "EvLineOut"
      },
      "EvOutcomeOut": {
        "properties": {
          "book": {
            "type": "string",
            "title": "Book"
          },
          "book_title": {
            "type": "string",
            "title": "Book Title"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "price": {
            "type": "integer",
            "title": "Price"
          },
          "ev_pct": {
            "type": "number",
            "title": "Ev Pct"
          },
          "is_plus_ev": {
            "type": "boolean",
            "title": "Is Plus Ev"
          },
          "point": {
            "type": "number",
            "title": "Point",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "book",
          "book_title",
          "name",
          "price",
          "ev_pct",
          "is_plus_ev"
        ],
        "title": "EvOutcomeOut"
      },
      "EventBestLineOut": {
        "description": "Cross-book best-line response for one event.",
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "books_considered": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "description": "Books that quoted at least one line on this event. DFS pick'em books (PrizePicks, Sleeper, Dabble) are always excluded.",
            "title": "Books Considered"
          },
          "lines": {
            "items": {
              "$ref": "#/components/schemas/BestLineOut"
            },
            "type": "array",
            "title": "Lines"
          },
          "redacted": {
            "type": "boolean",
            "default": false,
            "description": "True on free-tier responses: structure, book identities, and best-first ranking are visible but every price is nulled.",
            "title": "Redacted"
          },
          "upgrade_url": {
            "type": "string",
            "description": "Set on redacted responses \u2014 where to upgrade for full prices.",
            "title": "Upgrade Url",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "books_considered",
          "lines"
        ],
        "title": "EventBestLineOut"
      },
      "EventEvOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "fair_source_default": {
            "type": "string",
            "title": "Fair Source Default"
          },
          "devig_method": {
            "type": "string",
            "title": "Devig Method",
            "default": "multiplicative",
            "description": "Echo of the devig query param: how the anchor's vig was removed before every fair_probs in lines was derived (multiplicative or shin)."
          },
          "lines": {
            "items": {
              "$ref": "#/components/schemas/EvLineOut"
            },
            "type": "array",
            "title": "Lines"
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "fair_source_default",
          "lines"
        ],
        "title": "EventEvOut"
      },
      "EventHistoryOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "live": {
            "type": "boolean",
            "title": "Live",
            "default": false
          },
          "bookmakers": {
            "items": {
              "$ref": "#/components/schemas/BookmakerHistoryOut"
            },
            "type": "array",
            "title": "Bookmakers"
          },
          "upgrade_url": {
            "type": "string",
            "title": "Upgrade Url",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "bookmakers"
        ],
        "title": "EventHistoryOut"
      },
      "ClosingOutcomeOut": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "price": {
            "type": "integer",
            "title": "Price",
            "nullable": true
          },
          "point": {
            "type": "number",
            "title": "Point",
            "nullable": true
          },
          "outcome_id": {
            "type": "integer",
            "title": "Outcome Id",
            "description": "PropLine's stable id for this priced selection \u2014 the same value /odds?includeBookIds=true, /odds/history, the resolved-props CSV and webhook payloads carry.",
            "nullable": true
          },
          "closing_at": {
            "type": "string",
            "format": "date-time",
            "title": "Closing At",
            "description": "recorded_at of the chosen pre-commence snapshot",
            "nullable": true
          },
          "closing_age_seconds": {
            "type": "integer",
            "title": "Closing Age Seconds",
            "description": "Seconds between closing_at and commence_time. Large values mean the book stopped quoting early and this is not a true close.",
            "nullable": true
          },
          "is_stale": {
            "type": "boolean",
            "title": "Is Stale",
            "default": false,
            "description": "True when closing_age_seconds > 600 (10 min before kickoff). Advisory, not a hard filter."
          },
          "opening_price": {
            "type": "integer",
            "title": "Opening Price",
            "description": "American price of the first snapshot in the 14 days before kickoff.",
            "nullable": true
          },
          "opening_point": {
            "type": "number",
            "title": "Opening Point",
            "description": "Line that went with opening_price. On spreads and totals the point moves as much as the price, so both ends are needed for CLV.",
            "nullable": true
          },
          "opening_at": {
            "type": "string",
            "format": "date-time",
            "title": "Opening At",
            "description": "recorded_at of the chosen opening snapshot.",
            "nullable": true
          },
          "opening_age_seconds": {
            "type": "integer",
            "title": "Opening Age Seconds",
            "description": "Seconds between opening_at and commence_time. The archive starts 2026-04, so for a book/sport we began polling after the line was posted this is 'first observed by us', not the book's true open \u2014 a small value here is the tell.",
            "nullable": true
          },
          "book_updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Book Updated At",
            "nullable": true
          },
          "book_version": {
            "type": "integer",
            "title": "Book Version",
            "nullable": true
          },
          "liquidity": {
            "type": "number",
            "title": "Liquidity",
            "description": "Stake limit / resting size at the CLOSING snapshot (Pinnacle: its max risk stake). Null for books without the signal and on snapshots predating 2026-09-10.",
            "nullable": true
          },
          "opening_liquidity": {
            "type": "number",
            "title": "Opening Liquidity",
            "description": "Stake limit / resting size at the OPENING snapshot. Opening vs closing limit beside opening vs closing price says whether the book raised its limit as the line moved.",
            "nullable": true
          },
          "redacted": {
            "type": "boolean",
            "title": "Redacted",
            "default": false
          },
          "dfs_odds_type": {
            "type": "string",
            "title": "Dfs Odds Type",
            "description": "PrizePicks projection tier (standard/goblin/demon); null for traditional sportsbooks.",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "name",
          "description",
          "price",
          "point"
        ],
        "title": "ClosingOutcomeOut"
      },
      "ClosingMarketOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "period": {
            "type": "string",
            "description": "Game-period bucket. Null for full-game markets.",
            "nullable": true
          },
          "team": {
            "type": "string",
            "nullable": true,
            "description": "Canonical event team name when this market is scoped to ONE team \u2014 i.e. a team total \u2014 and null for the game total. Both ride the `totals` key, so this is the machine-readable form of `description`: every book spells the team differently in that string (Bovada suffixes \" - {team}\", BetUS prefixes \"Team Total - \", Smarkets and TAB say nothing), and a client bucketing on (key, point) otherwise reads a team total as a wildly mispriced game total. Always null outside `totals`.",
            "example": "Ipswich Town"
          },
          "outcomes": {
            "items": {
              "$ref": "#/components/schemas/ClosingOutcomeOut"
            },
            "type": "array",
            "title": "Outcomes"
          }
        },
        "type": "object",
        "required": [
          "key",
          "description",
          "outcomes"
        ],
        "title": "ClosingMarketOut"
      },
      "ClosingBookmakerOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "markets": {
            "items": {
              "$ref": "#/components/schemas/ClosingMarketOut"
            },
            "type": "array",
            "title": "Markets"
          }
        },
        "type": "object",
        "required": [
          "key",
          "title",
          "markets"
        ],
        "title": "ClosingBookmakerOut"
      },
      "EventClosingOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "bookmakers": {
            "items": {
              "$ref": "#/components/schemas/ClosingBookmakerOut"
            },
            "type": "array",
            "title": "Bookmakers"
          },
          "upgrade_url": {
            "type": "string",
            "title": "Upgrade Url",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "bookmakers"
        ],
        "title": "EventClosingOut"
      },
      "EventOddsOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "home_team_key": {
            "type": "string",
            "nullable": true,
            "description": "Stable per-team join key. Every bookmaker spelling of a club resolves to the same key ('St Mirren FC', 'St. Mirren' -> 'st_mirren'); a published key is never renamed. Null when the team cannot be identified with certainty (individual sports, small coverage tail) \u2014 fall back to the name."
          },
          "away_team_key": {
            "type": "string",
            "nullable": true,
            "description": "Away-side counterpart of home_team_key."
          },
          "home_team_id": {
            "type": "string",
            "nullable": true,
            "description": "The league's own permanent team id, namespaced by source ('mlb:147', 'espn.soccer:363', 'espn.nfl:12'). Use home_team_key to key data inside PropLine; use this to join against external datasets keyed on the same league ids. Sourced from the grading stats feeds, never guessed; null where unconfirmed."
          },
          "away_team_id": {
            "type": "string",
            "nullable": true,
            "description": "Away-side counterpart of home_team_id."
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "live": {
            "type": "boolean",
            "title": "Live",
            "default": false
          },
          "last_update": {
            "type": "string",
            "format": "date-time",
            "title": "Last Update",
            "nullable": true
          },
          "bookmakers": {
            "items": {
              "$ref": "#/components/schemas/BookmakerOut"
            },
            "type": "array",
            "title": "Bookmakers"
          },
          "merged_from_event_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true,
            "description": "Event ids that were merged INTO this event when duplicate fixtures from different bookmakers were folded into one. Always present (no flag); null for the large majority of events, which have never been merged. Lets a client reconcile a stored id from the response it was already fetching instead of re-requesting every saved id."
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "bookmakers"
        ],
        "title": "EventOddsOut"
      },
      "EventOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "home_team_key": {
            "type": "string",
            "nullable": true,
            "description": "Stable per-team join key. Every bookmaker spelling of a club resolves to the same key ('St Mirren FC', 'St. Mirren' -> 'st_mirren'); a published key is never renamed. Null when the team cannot be identified with certainty (individual sports, small coverage tail) \u2014 fall back to the name."
          },
          "away_team_key": {
            "type": "string",
            "nullable": true,
            "description": "Away-side counterpart of home_team_key."
          },
          "home_team_id": {
            "type": "string",
            "nullable": true,
            "description": "The league's own permanent team id, namespaced by source ('mlb:147', 'espn.soccer:363', 'espn.nfl:12'). Use home_team_key to key data inside PropLine; use this to join against external datasets keyed on the same league ids. Sourced from the grading stats feeds, never guessed; null where unconfirmed."
          },
          "away_team_id": {
            "type": "string",
            "nullable": true,
            "description": "Away-side counterpart of home_team_id."
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "live": {
            "type": "boolean",
            "title": "Live",
            "default": false
          },
          "bookmakers": {
            "items": {
              "$ref": "#/components/schemas/BookmakerOut"
            },
            "type": "array",
            "title": "Bookmakers",
            "nullable": true
          },
          "merged_from_event_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true,
            "description": "Event ids that were merged INTO this event when duplicate fixtures from different bookmakers were folded into one. Always present (no flag); null for the large majority of events, which have never been merged. Lets a client reconcile a stored id from the response it was already fetching instead of re-requesting every saved id."
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time"
        ],
        "title": "EventOut"
      },
      "EventResultOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "live": {
            "type": "boolean",
            "title": "Live",
            "default": false
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "home_score": {
            "type": "integer",
            "title": "Home Score",
            "nullable": true
          },
          "away_score": {
            "type": "integer",
            "title": "Away Score",
            "nullable": true
          },
          "bookmakers": {
            "items": {
              "$ref": "#/components/schemas/BookmakerResultOut"
            },
            "type": "array",
            "title": "Bookmakers"
          },
          "context": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/GameContextOut"
              }
            ],
            "nullable": true
          },
          "upgrade_url": {
            "type": "string",
            "title": "Upgrade Url",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "status",
          "home_score",
          "away_score",
          "bookmakers"
        ],
        "title": "EventResultOut"
      },
      "MovementOutcomeOut": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "type": "string",
            "title": "Description",
            "nullable": true
          },
          "open_price": {
            "type": "integer",
            "title": "Open Price",
            "nullable": true
          },
          "open_point": {
            "type": "number",
            "title": "Open Point",
            "nullable": true
          },
          "open_at": {
            "type": "string",
            "format": "date-time",
            "title": "Open At",
            "nullable": true
          },
          "latest_price": {
            "type": "integer",
            "title": "Latest Price",
            "nullable": true
          },
          "latest_point": {
            "type": "number",
            "title": "Latest Point",
            "nullable": true
          },
          "latest_at": {
            "type": "string",
            "format": "date-time",
            "title": "Latest At",
            "nullable": true
          },
          "prob_shift": {
            "type": "number",
            "title": "Prob Shift",
            "nullable": true
          },
          "point_shift": {
            "type": "number",
            "title": "Point Shift",
            "nullable": true
          },
          "direction": {
            "type": "string",
            "title": "Direction",
            "nullable": true
          },
          "num_snapshots": {
            "type": "integer",
            "default": 0,
            "title": "Num Snapshots"
          },
          "redacted": {
            "type": "boolean",
            "default": false,
            "title": "Redacted"
          }
        },
        "type": "object",
        "required": [
          "name"
        ],
        "title": "MovementOutcomeOut"
      },
      "MovementMarketOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "description": {
            "type": "string",
            "default": "",
            "description": "The book's OWN name for this market row, and the only thing that separates a TEAM total from the game total \u2014 both ride the `totals` key (e.g. \"Total\" at 2.5 alongside \"Team Total - Arsenal\" at 1.5). Wording is per book, so match on the team name rather than an exact string. Empty where the scraper set none."
          },
          "period": {
            "type": "string",
            "title": "Period",
            "nullable": true
          },
          "team": {
            "type": "string",
            "nullable": true,
            "description": "Canonical event team name when this market is scoped to ONE team \u2014 i.e. a team total \u2014 and null for the game total. Both ride the `totals` key, so this is the machine-readable form of `description`: every book spells the team differently in that string (Bovada suffixes \" - {team}\", BetUS prefixes \"Team Total - \", Smarkets and TAB say nothing), and a client bucketing on (key, point) otherwise reads a team total as a wildly mispriced game total. Always null outside `totals`.",
            "example": "Ipswich Town"
          },
          "outcomes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MovementOutcomeOut"
            },
            "title": "Outcomes"
          }
        },
        "type": "object",
        "required": [
          "key",
          "outcomes"
        ],
        "title": "MovementMarketOut"
      },
      "MovementBookmakerOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "markets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MovementMarketOut"
            },
            "title": "Markets"
          }
        },
        "type": "object",
        "required": [
          "key",
          "title",
          "markets"
        ],
        "title": "MovementBookmakerOut"
      },
      "SteamMoveOut": {
        "properties": {
          "market": {
            "type": "string",
            "title": "Market"
          },
          "period": {
            "type": "string",
            "title": "Period",
            "nullable": true
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "type": "string",
            "title": "Description",
            "nullable": true
          },
          "books_quoting": {
            "type": "integer",
            "title": "Books Quoting"
          },
          "books_moved": {
            "type": "integer",
            "title": "Books Moved"
          },
          "consensus_direction": {
            "type": "string",
            "title": "Consensus Direction"
          },
          "avg_prob_shift": {
            "type": "number",
            "title": "Avg Prob Shift"
          },
          "consensus_point_shift": {
            "type": "number",
            "title": "Consensus Point Shift",
            "nullable": true
          },
          "steam_score": {
            "type": "number",
            "title": "Steam Score"
          }
        },
        "type": "object",
        "required": [
          "market",
          "name",
          "books_quoting",
          "books_moved",
          "consensus_direction",
          "avg_prob_shift",
          "steam_score"
        ],
        "title": "SteamMoveOut"
      },
      "EventMovementOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "bookmakers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MovementBookmakerOut"
            },
            "title": "Bookmakers"
          },
          "steam": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SteamMoveOut"
            },
            "title": "Steam"
          },
          "redacted": {
            "type": "boolean",
            "default": false,
            "title": "Redacted"
          },
          "upgrade_url": {
            "type": "string",
            "title": "Upgrade Url",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "bookmakers",
          "steam"
        ],
        "title": "EventMovementOut"
      },
      "WeatherOut": {
        "properties": {
          "temperature_f": {
            "type": "number",
            "title": "Temperature F",
            "nullable": true
          },
          "humidity_pct": {
            "type": "number",
            "title": "Humidity Pct",
            "nullable": true
          },
          "precip_probability_pct": {
            "type": "number",
            "title": "Precip Probability Pct",
            "nullable": true
          },
          "precip_in": {
            "type": "number",
            "title": "Precip In",
            "nullable": true
          },
          "wind_speed_mph": {
            "type": "number",
            "title": "Wind Speed Mph",
            "nullable": true
          },
          "wind_gust_mph": {
            "type": "number",
            "title": "Wind Gust Mph",
            "nullable": true
          },
          "wind_direction_deg": {
            "type": "number",
            "title": "Wind Direction Deg",
            "nullable": true
          },
          "wind_direction": {
            "type": "string",
            "title": "Wind Direction",
            "nullable": true
          },
          "conditions": {
            "type": "string",
            "title": "Conditions",
            "nullable": true
          },
          "observed_for": {
            "type": "string",
            "title": "Observed For",
            "nullable": true
          }
        },
        "type": "object",
        "title": "WeatherOut"
      },
      "GameContextOut": {
        "properties": {
          "event_id": {
            "type": "string",
            "title": "Event Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "venue": {
            "type": "string",
            "title": "Venue",
            "nullable": true
          },
          "roof_type": {
            "type": "string",
            "title": "Roof Type",
            "nullable": true
          },
          "is_indoor": {
            "type": "boolean",
            "default": false,
            "title": "Is Indoor"
          },
          "home_probable_pitcher": {
            "type": "string",
            "title": "Home Probable Pitcher",
            "nullable": true
          },
          "away_probable_pitcher": {
            "type": "string",
            "title": "Away Probable Pitcher",
            "nullable": true
          },
          "home_probable_pitcher_hand": {
            "type": "string",
            "title": "Home Probable Pitcher Hand",
            "description": "Throwing hand of the probable starter: L, R, or S (switch).",
            "nullable": true
          },
          "away_probable_pitcher_hand": {
            "type": "string",
            "title": "Away Probable Pitcher Hand",
            "description": "Throwing hand of the probable starter: L, R, or S (switch).",
            "nullable": true
          },
          "lineup_confirmed": {
            "type": "boolean",
            "default": false,
            "title": "Lineup Confirmed"
          },
          "home_plate_umpire": {
            "type": "string",
            "title": "Home Plate Umpire",
            "nullable": true
          },
          "weather": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/WeatherOut"
              }
            ],
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Updated At",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "event_id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time"
        ],
        "title": "GameContextOut"
      },
      "EventStatsOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "home_score": {
            "type": "integer",
            "title": "Home Score",
            "nullable": true
          },
          "away_score": {
            "type": "integer",
            "title": "Away Score",
            "nullable": true
          },
          "stats": {
            "items": {
              "$ref": "#/components/schemas/PlayerStatOut"
            },
            "type": "array",
            "title": "Stats"
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "status",
          "home_score",
          "away_score",
          "stats"
        ],
        "title": "EventStatsOut"
      },
      "FuturesEventOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "markets": {
            "items": {
              "$ref": "#/components/schemas/FuturesMarketOut"
            },
            "type": "array",
            "title": "Markets"
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "title",
          "commence_time",
          "markets"
        ],
        "title": "FuturesEventOut"
      },
      "FuturesMarketOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "bookmaker": {
            "type": "string",
            "title": "Bookmaker"
          },
          "bookmaker_title": {
            "type": "string",
            "title": "Bookmaker Title"
          },
          "last_update": {
            "type": "string",
            "format": "date-time",
            "title": "Last Update"
          },
          "book_updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Book Updated At",
            "nullable": true
          },
          "outcomes": {
            "items": {
              "$ref": "#/components/schemas/FuturesOutcomeOut"
            },
            "type": "array",
            "title": "Outcomes"
          }
        },
        "type": "object",
        "required": [
          "key",
          "description",
          "bookmaker",
          "bookmaker_title",
          "last_update",
          "outcomes"
        ],
        "title": "FuturesMarketOut"
      },
      "FuturesOutcomeOut": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name",
            "description": "For winner-style futures (Super Bowl winner, MVP, division winner) this is the team or player. For Over/Under-style futures (season win totals) it is \"Over\" or \"Under\" and the subject moves to `description`."
          },
          "description": {
            "type": "string",
            "default": "",
            "title": "Description",
            "description": "Blank on winner-style futures. On Over/Under-style futures it carries the subject the line belongs to \u2014 the team for a season win total (\"Buffalo Bills\"), the player for a season player prop."
          },
          "price": {
            "type": "integer",
            "title": "Price",
            "nullable": true
          },
          "price_decimal": {
            "type": "number",
            "title": "Price Decimal",
            "nullable": true
          },
          "point": {
            "type": "number",
            "default": null,
            "title": "Point",
            "description": "The line on Over/Under-style futures (e.g. 10.5 wins). Null on winner-style futures, which have no line.",
            "nullable": true
          },
          "resolution": {
            "type": "string",
            "nullable": true,
            "description": "Settlement, for the season-long team futures a final regular-season table decides (win totals, division winners, conference #1 seed on NFL/NBA/MLB): won | lost | push | void. Null on postseason futures and awards, which no standings table can answer."
          },
          "actual_value": {
            "type": "number",
            "nullable": true,
            "description": "The figure settled against \u2014 the team's season wins for a win total, 1/0 for a yes-style outright. Null until settled."
          },
          "settled_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "When the outcome was settled. Null until settled."
          }
        },
        "type": "object",
        "required": [
          "name",
          "price",
          "price_decimal"
        ],
        "title": "FuturesOutcomeOut"
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "MarketHistoryOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "period": {
            "type": "string",
            "description": "Game-period bucket. Null for full-game markets.",
            "nullable": true
          },
          "team": {
            "type": "string",
            "nullable": true,
            "description": "Canonical event team name when this market is scoped to ONE team \u2014 i.e. a team total \u2014 and null for the game total. Both ride the `totals` key, so this is the machine-readable form of `description`: every book spells the team differently in that string (Bovada suffixes \" - {team}\", BetUS prefixes \"Team Total - \", Smarkets and TAB say nothing), and a client bucketing on (key, point) otherwise reads a team total as a wildly mispriced game total. Always null outside `totals`.",
            "example": "Ipswich Town"
          },
          "outcomes": {
            "items": {
              "$ref": "#/components/schemas/OutcomeHistoryOut"
            },
            "type": "array",
            "title": "Outcomes"
          }
        },
        "type": "object",
        "required": [
          "key",
          "description",
          "outcomes"
        ],
        "title": "MarketHistoryOut"
      },
      "MarketOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "description": {
            "type": "string",
            "default": "",
            "description": "The book's OWN name for this market row, and the only thing that separates a TEAM total from the game total \u2014 both ride the `totals` key (e.g. \"Total\" at 2.5 alongside \"Team Total - Arsenal\" at 1.5). Wording is per book, so match on the team name rather than an exact string. Empty where the scraper set none."
          },
          "last_update": {
            "type": "string",
            "format": "date-time",
            "title": "Last Update"
          },
          "period": {
            "type": "string",
            "description": "Game-period bucket (q1..q4, h1/h2, p1..p3, i1..i9, f3/f5/f7). Null for full-game markets.",
            "nullable": true
          },
          "team": {
            "type": "string",
            "nullable": true,
            "description": "Canonical event team name when this market is scoped to ONE team \u2014 i.e. a team total \u2014 and null for the game total. Both ride the `totals` key, so this is the machine-readable form of `description`: every book spells the team differently in that string (Bovada suffixes \" - {team}\", BetUS prefixes \"Team Total - \", Smarkets and TAB say nothing), and a client bucketing on (key, point) otherwise reads a team total as a wildly mispriced game total. Always null outside `totals`.",
            "example": "Ipswich Town"
          },
          "suspended_at": {
            "type": "string",
            "format": "date-time",
            "description": "Set when this book has taken the market off the board pregame (the pull-side twin of the market_suspended webhook). Null = on the board. The outcomes are then the last quoted legs, not a live price. Clears the moment the book delivers the market again.",
            "nullable": true
          },
          "outcomes": {
            "items": {
              "$ref": "#/components/schemas/OutcomeOut"
            },
            "type": "array",
            "title": "Outcomes"
          }
        },
        "type": "object",
        "required": [
          "key",
          "last_update",
          "outcomes"
        ],
        "title": "MarketOut"
      },
      "OddsSnapshotOut": {
        "properties": {
          "price": {
            "type": "integer",
            "title": "Price",
            "nullable": true
          },
          "point": {
            "type": "number",
            "title": "Point",
            "nullable": true
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time",
            "title": "Recorded At"
          },
          "book_updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Book Updated At",
            "nullable": true
          },
          "book_version": {
            "type": "integer",
            "title": "Book Version",
            "nullable": true
          },
          "liquidity": {
            "type": "number",
            "title": "Liquidity",
            "description": "Stake limit / resting size in force at this snapshot \u2014 the same field as the /odds outcome's liquidity, frozen per row. Pinnacle: its posted max risk stake; a Pinnacle limit change with NO price move is its own snapshot row (price/point repeat, this moves) and survives changes_only=true. Exchanges: the size at the moment of a price move only. Null for other books and on rows predating 2026-09-10.",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "price",
          "point",
          "recorded_at"
        ],
        "title": "OddsSnapshotOut"
      },
      "OutcomeHistoryOut": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "outcome_id": {
            "type": "integer",
            "title": "Outcome Id",
            "description": "PropLine's stable id for this priced selection \u2014 the same value /odds?includeBookIds=true, /odds/closing, the resolved-props CSV and webhook payloads carry.",
            "nullable": true
          },
          "snapshots": {
            "items": {
              "$ref": "#/components/schemas/OddsSnapshotOut"
            },
            "type": "array",
            "title": "Snapshots"
          },
          "snapshots_available": {
            "type": "integer",
            "title": "Snapshots Available",
            "nullable": true
          },
          "redacted": {
            "type": "boolean",
            "title": "Redacted",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "name",
          "description",
          "snapshots"
        ],
        "title": "OutcomeHistoryOut"
      },
      "OutcomeOut": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "price": {
            "type": "integer",
            "title": "Price",
            "nullable": true
          },
          "point": {
            "type": "number",
            "title": "Point",
            "nullable": true
          },
          "book_updated_at": {
            "type": "string",
            "format": "date-time",
            "title": "Book Updated At",
            "nullable": true
          },
          "book_version": {
            "type": "integer",
            "title": "Book Version",
            "nullable": true
          },
          "payout_multiplier": {
            "type": "number",
            "title": "Payout Multiplier",
            "description": "DFS payout multiplier for boosted/discounted picks (Underdog Fantasy). Populated on EVERY Underdog outcome; null means the book is not Underdog. 1.0 = standard pick whose quoted price carries the full payout; a value such as 0.75 (discount) or 1.5 (boost) scales the effective payout. Keep only payout_multiplier == 1.0 when comparing DFS lines to sportsbook consensus \u2014 filtering on non-null would drop every Underdog line.",
            "nullable": true
          },
          "dfs_odds_type": {
            "type": "string",
            "title": "DFS Odds Type",
            "description": "PrizePicks projection flavor: 'standard' (the true market line), 'goblin' (easier line / lower payout) or 'demon' (harder line / higher payout). Null for every traditional sportsbook. Filter to 'standard' to get PrizePicks's market line; goblin/demon are boosted/discounted alternates that each appear as their own per-line market. PrizePicks publishes no numeric multiplier for these, so only the flavor is surfaced.",
            "nullable": true
          },
          "last_change_at": {
            "type": "string",
            "format": "date-time",
            "title": "Last Change At",
            "description": "PropLine's observed timestamp of the last time this outcome's price actually changed. Distinct from book_updated_at (the book's own publish-time, which only Bovada exposes): last_change_at is derived by PropLine and is populated for EVERY book, including Pinnacle and PrizePicks. We only advance it when price_american moves, so it is a true 'this line last changed at T' signal \u2014 compare it across books in one /odds call to detect repricing lag without a separate /odds/history call.",
            "nullable": true
          },
          "last_seen_at": {
            "type": "string",
            "format": "date-time",
            "title": "Last Seen At",
            "description": "The last delivery this outcome appeared in \u2014 the book still had it on the board at that poll, whether or not the price moved. For an outcome present in its market's latest delivery it equals the market's last_update; an older value means the book has stopped sending this selection while still sending the market (a withdrawal in progress \u2014 /odds withholds it entirely once that gap passes ~2 minutes, on every book; a briefly suspended in-play selection can therefore leave the response and come back). Consumer rule: a value older than the market's last_update means the selection missed the latest delivery \u2014 treat it as unavailable. last_change_at = when the price last moved; last_seen_at = when it was last offered. /odds only; null on rows predating 2026-08-26.",
            "nullable": true
          },
          "line_gap": {
            "type": "number",
            "title": "Line Gap",
            "description": "Signed line-difficulty delta for a PrizePicks goblin/demon outcome: point - the standard line for the same player+stat. Positive on a harder (demon) line, negative on an easier (goblin) line. Null when the outcome is not a PrizePicks goblin/demon, or when no standard line exists for that player+stat (PrizePicks often posts a player goblin/demon-only). PrizePicks publishes no numeric per-pick payout multiplier, so the flavor + this line gap are the modelable signals for fitting per-pick payout adjustments.",
            "nullable": true
          },
          "outcome_id": {
            "type": "integer",
            "nullable": true,
            "description": "PropLine's own canonical id for this outcome row \u2014 the same value carried as `outcome_id` on `line_movement` and `resolution` webhook payloads, so a push delivery joins onto exactly one REST row with no name, side or line matching. Globally unique (one row per book/market/side; Over and Under are separate rows) and stable across price and point changes for the same (market, side, player). Books whose alt ladders put the line in the market description (PrizePicks goblin/demon, ProphetX, Fanatics, Marathon) get a NEW market \u2014 and a new id \u2014 when that line moves. Only set when the request passed includeBookIds=true."
          },
          "book_outcome_id": {
            "type": "string",
            "nullable": true,
            "description": "This book's own identifier for the priced selection / contract (Kalshi: the per-contract market ticker; PrizePicks: the projection id used to build a populated entry URL; also Underdog, Sleeper and Dabble). Only set when the request passed includeBookIds=true. Whether the two legs of a market share one id varies by book \u2014 Kalshi and PrizePicks share it (`name` is the side), Underdog/Sleeper/Dabble give each leg its own \u2014 so group a pair by (market, player, point), not by id."
          },
          "liquidity_updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the exchange last touched the resting order behind `liquidity` \u2014 the BOOK'S own timestamp, not our observation time. ProphetX does not continuously re-publish resting size, so a size can be materially stale while the book has moved on (measured 2026-08-23: median 137s, 27% of rungs older than 20 minutes). Drop or discount sizes older than your own threshold. Null for books without resting size."
          },
          "liquidity": {
            "type": "number",
            "nullable": true,
            "description": "Dollars a bettor can actually stake at the quoted price. Exchange books publishing resting size (ProphetX and Novig) plus Pinnacle, where it is the book's posted max risk stake on the market (its own cap on what one bettor can risk at that price); null for every other book, and for an exchange quote whose size the feed omitted. On a P2P exchange the best price is often a thin offer with only a few dollars behind it \u2014 filter or discount small values before treating the price as bettable. Refreshed every poll cycle independently of price movement. Exchange size changes never appear in /odds/history; a Pinnacle limit change does (its own snapshot row, price unchanged), and /odds/closing carries opening_liquidity beside liquidity. Neither fires line_movement webhooks. Pair with liquidity_updated_at: ProphetX does not continuously re-publish resting size, so a rung keeps its last-known amount until something happens to it (~27% of served rungs are more than 20 minutes stale)."
          },
          "player_id": {
            "type": "string",
            "nullable": true,
            "description": "Stable, cross-referenceable league player id for joining the SAME player across books WITHOUT name matching \u2014 \"{source}:{league_id}\" (mlb:592450 = MLBAM person id, nba:/wnba: = CDN personId, nhl: = api-web playerId, espn:8439 = ESPN athlete id for soccer/NFL/NCAAF). Present only on player-prop markets; always null on game lines and futures. Null whenever we do not have a CONFIRMED, unambiguous id: a sport with no stable-id stats feed (tennis/golf/UFC/boxing/cricket/darts/esports/... \u2014 null forever), a player who has never graded, a book spelling that diverges from the league's (an extra surname / nickname \u2014 the id lands on the well-spelled book and stays null on the odd one), or a name two players share (ambiguous). We never guess an id: a missed join is safe, a wrong join is not. Unconditional (no query param). Coverage warms as games grade after launch."
          }
        },
        "type": "object",
        "required": [
          "name",
          "description",
          "price",
          "point"
        ],
        "title": "OutcomeOut"
      },
      "PlayerHistoryEntryOut": {
        "properties": {
          "event_id": {
            "type": "string",
            "title": "Event Id"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "bookmaker": {
            "type": "string",
            "title": "Bookmaker"
          },
          "bookmaker_title": {
            "type": "string",
            "title": "Bookmaker Title"
          },
          "line": {
            "type": "number",
            "title": "Line",
            "nullable": true
          },
          "over_price": {
            "type": "integer",
            "title": "Over Price",
            "nullable": true
          },
          "under_price": {
            "type": "integer",
            "title": "Under Price",
            "nullable": true
          },
          "actual_value": {
            "type": "number",
            "title": "Actual Value",
            "nullable": true
          },
          "over_result": {
            "type": "string",
            "title": "Over Result",
            "nullable": true
          },
          "under_result": {
            "type": "string",
            "title": "Under Result",
            "nullable": true
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "title": "Resolved At",
            "nullable": true
          },
          "redacted": {
            "type": "boolean",
            "title": "Redacted",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "event_id",
          "commence_time",
          "home_team",
          "away_team",
          "bookmaker",
          "bookmaker_title",
          "line",
          "over_price",
          "under_price",
          "actual_value",
          "over_result",
          "under_result",
          "resolved_at"
        ],
        "title": "PlayerHistoryEntryOut"
      },
      "PlayerHistoryOut": {
        "properties": {
          "player_name": {
            "type": "string",
            "title": "Player Name"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "market": {
            "type": "string",
            "title": "Market"
          },
          "entries": {
            "items": {
              "$ref": "#/components/schemas/PlayerHistoryEntryOut"
            },
            "type": "array",
            "title": "Entries"
          },
          "upgrade_url": {
            "type": "string",
            "title": "Upgrade Url",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "player_name",
          "sport_key",
          "market",
          "entries"
        ],
        "title": "PlayerHistoryOut"
      },
      "HitRateSplitOut": {
        "properties": {
          "window": {
            "type": "integer",
            "title": "Window"
          },
          "games": {
            "type": "integer",
            "title": "Games"
          },
          "over": {
            "type": "integer",
            "title": "Over"
          },
          "under": {
            "type": "integer",
            "title": "Under"
          },
          "push": {
            "type": "integer",
            "title": "Push"
          },
          "over_pct": {
            "type": "number",
            "title": "Over Pct",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "window",
          "games",
          "over",
          "under",
          "push",
          "over_pct"
        ],
        "title": "HitRateSplitOut"
      },
      "TrendStreakOut": {
        "properties": {
          "result": {
            "type": "string",
            "title": "Result"
          },
          "length": {
            "type": "integer",
            "title": "Length"
          }
        },
        "type": "object",
        "required": [
          "result",
          "length"
        ],
        "title": "TrendStreakOut"
      },
      "TrendLastGameOut": {
        "properties": {
          "event_id": {
            "type": "string",
            "title": "Event Id"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "line": {
            "type": "number",
            "title": "Line",
            "nullable": true
          },
          "actual_value": {
            "type": "number",
            "title": "Actual Value",
            "nullable": true
          },
          "result": {
            "type": "string",
            "title": "Result",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "event_id",
          "commence_time",
          "line",
          "actual_value",
          "result"
        ],
        "title": "TrendLastGameOut"
      },
      "PlayerMarketTrendOut": {
        "properties": {
          "market": {
            "type": "string",
            "title": "Market"
          },
          "games_graded": {
            "type": "integer",
            "title": "Games Graded"
          },
          "reference_bookmaker": {
            "type": "string",
            "title": "Reference Bookmaker",
            "nullable": true
          },
          "reference_bookmaker_title": {
            "type": "string",
            "title": "Reference Bookmaker Title",
            "nullable": true
          },
          "recent_line": {
            "type": "number",
            "title": "Recent Line",
            "nullable": true
          },
          "avg_actual": {
            "type": "number",
            "title": "Avg Actual",
            "nullable": true
          },
          "last_5": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/HitRateSplitOut"
              }
            ],
            "nullable": true
          },
          "last_10": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/HitRateSplitOut"
              }
            ],
            "nullable": true
          },
          "last_20": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/HitRateSplitOut"
              }
            ],
            "nullable": true
          },
          "last_50": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/HitRateSplitOut"
              }
            ],
            "nullable": true
          },
          "current_streak": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/TrendStreakOut"
              }
            ],
            "nullable": true
          },
          "last_game": {
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/TrendLastGameOut"
              }
            ],
            "nullable": true
          },
          "redacted": {
            "type": "boolean",
            "default": false,
            "title": "Redacted"
          }
        },
        "type": "object",
        "required": [
          "market",
          "games_graded"
        ],
        "title": "PlayerMarketTrendOut"
      },
      "PlayerTrendsOut": {
        "properties": {
          "player_name": {
            "type": "string",
            "title": "Player Name"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "dfs_odds_type": {
            "type": "string",
            "title": "DFS Odds Type",
            "description": "Echo of the dfs_odds_type filter that scoped these trends (PrizePicks flavor: standard/goblin/demon). Null = cross-book, flavor-agnostic.",
            "nullable": true
          },
          "markets": {
            "items": {
              "$ref": "#/components/schemas/PlayerMarketTrendOut"
            },
            "type": "array",
            "title": "Markets"
          },
          "upgrade_url": {
            "type": "string",
            "title": "Upgrade Url",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "player_name",
          "sport_key",
          "markets"
        ],
        "title": "PlayerTrendsOut"
      },
      "PlayerStatOut": {
        "properties": {
          "player_name": {
            "type": "string",
            "title": "Player Name"
          },
          "team_abbr": {
            "type": "string",
            "title": "Team Abbr",
            "nullable": true
          },
          "stat_type": {
            "type": "string",
            "title": "Stat Type"
          },
          "stat_value": {
            "type": "number",
            "title": "Stat Value"
          }
        },
        "type": "object",
        "required": [
          "player_name",
          "team_abbr",
          "stat_type",
          "stat_value"
        ],
        "title": "PlayerStatOut"
      },
      "ResolvedMarketOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "outcomes": {
            "items": {
              "$ref": "#/components/schemas/ResolvedOutcomeOut"
            },
            "type": "array",
            "title": "Outcomes"
          }
        },
        "type": "object",
        "required": [
          "key",
          "description",
          "outcomes"
        ],
        "title": "ResolvedMarketOut"
      },
      "ResolvedOutcomeOut": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "type": "string",
            "title": "Description"
          },
          "outcome_id": {
            "type": "integer",
            "title": "Outcome Id",
            "description": "PropLine's stable id for this priced selection \u2014 the same value /odds?includeBookIds=true, /odds/history, /odds/closing, the resolved-props CSV and webhook payloads carry.",
            "nullable": true
          },
          "price": {
            "type": "integer",
            "title": "Price",
            "nullable": true
          },
          "point": {
            "type": "number",
            "title": "Point",
            "nullable": true
          },
          "resolution": {
            "type": "string",
            "title": "Resolution",
            "nullable": true
          },
          "actual_value": {
            "type": "number",
            "title": "Actual Value",
            "nullable": true
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "title": "Resolved At",
            "nullable": true
          },
          "redacted": {
            "type": "boolean",
            "title": "Redacted",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "name",
          "description",
          "price",
          "point",
          "resolution",
          "actual_value",
          "resolved_at"
        ],
        "title": "ResolvedOutcomeOut"
      },
      "ScoreOut": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "home_team": {
            "type": "string",
            "title": "Home Team"
          },
          "away_team": {
            "type": "string",
            "title": "Away Team"
          },
          "home_team_key": {
            "type": "string",
            "nullable": true,
            "description": "Stable per-team join key. Every bookmaker spelling of a club resolves to the same key ('St Mirren FC', 'St. Mirren' -> 'st_mirren'); a published key is never renamed. Null when the team cannot be identified with certainty (individual sports, small coverage tail) \u2014 fall back to the name."
          },
          "away_team_key": {
            "type": "string",
            "nullable": true,
            "description": "Away-side counterpart of home_team_key."
          },
          "home_team_id": {
            "type": "string",
            "nullable": true,
            "description": "The league's own permanent team id, namespaced by source ('mlb:147', 'espn.soccer:363', 'espn.nfl:12'). Use home_team_key to key data inside PropLine; use this to join against external datasets keyed on the same league ids. Sourced from the grading stats feeds, never guessed; null where unconfirmed."
          },
          "away_team_id": {
            "type": "string",
            "nullable": true,
            "description": "Away-side counterpart of home_team_id."
          },
          "commence_time": {
            "type": "string",
            "format": "date-time",
            "title": "Commence Time"
          },
          "live": {
            "type": "boolean",
            "title": "Live",
            "default": false
          },
          "status": {
            "type": "string",
            "title": "Status"
          },
          "home_score": {
            "type": "integer",
            "title": "Home Score",
            "nullable": true
          },
          "away_score": {
            "type": "integer",
            "title": "Away Score",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "id",
          "sport_key",
          "home_team",
          "away_team",
          "commence_time",
          "status",
          "home_score",
          "away_score"
        ],
        "title": "ScoreOut"
      },
      "GrandSalamiBookOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "games_priced": {
            "type": "integer",
            "title": "Games Priced"
          },
          "line": {
            "type": "number",
            "title": "Line"
          },
          "result": {
            "type": "string",
            "title": "Result",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "key",
          "title",
          "games_priced",
          "line"
        ],
        "title": "GrandSalamiBookOut"
      },
      "GrandSalamiOut": {
        "properties": {
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "date": {
            "type": "string",
            "title": "Date"
          },
          "games_total": {
            "type": "integer",
            "title": "Games Total"
          },
          "games_completed": {
            "type": "integer",
            "title": "Games Completed"
          },
          "games_in_progress": {
            "type": "integer",
            "title": "Games In Progress"
          },
          "games_upcoming": {
            "type": "integer",
            "title": "Games Upcoming"
          },
          "actual_total_runs": {
            "type": "integer",
            "title": "Actual Total Runs",
            "nullable": true
          },
          "bookmakers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GrandSalamiBookOut"
            },
            "title": "Bookmakers"
          }
        },
        "type": "object",
        "required": [
          "sport_key",
          "date",
          "games_total",
          "games_completed",
          "games_in_progress",
          "games_upcoming",
          "actual_total_runs",
          "bookmakers"
        ],
        "title": "GrandSalamiOut"
      },
      "DailyGoalsTotalBookOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "games_priced": {
            "type": "integer",
            "title": "Games Priced"
          },
          "line": {
            "type": "number",
            "title": "Line"
          },
          "result": {
            "type": "string",
            "title": "Result",
            "nullable": true
          }
        },
        "type": "object",
        "required": [
          "key",
          "title",
          "games_priced",
          "line"
        ],
        "title": "DailyGoalsTotalBookOut"
      },
      "DailyGoalsTotalOut": {
        "properties": {
          "sport_key": {
            "type": "string",
            "title": "Sport Key"
          },
          "date": {
            "type": "string",
            "title": "Date"
          },
          "games_total": {
            "type": "integer",
            "title": "Games Total"
          },
          "games_completed": {
            "type": "integer",
            "title": "Games Completed"
          },
          "games_in_progress": {
            "type": "integer",
            "title": "Games In Progress"
          },
          "games_upcoming": {
            "type": "integer",
            "title": "Games Upcoming"
          },
          "actual_total_goals": {
            "type": "integer",
            "title": "Actual Total Goals",
            "nullable": true
          },
          "bookmakers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DailyGoalsTotalBookOut"
            },
            "title": "Bookmakers"
          }
        },
        "type": "object",
        "required": [
          "sport_key",
          "date",
          "games_total",
          "games_completed",
          "games_in_progress",
          "games_upcoming",
          "actual_total_goals",
          "bookmakers"
        ],
        "title": "DailyGoalsTotalOut"
      },
      "SportOut": {
        "properties": {
          "key": {
            "type": "string",
            "title": "Key"
          },
          "title": {
            "type": "string",
            "title": "Title"
          },
          "active": {
            "type": "boolean",
            "title": "Active"
          }
        },
        "type": "object",
        "required": [
          "key",
          "title",
          "active"
        ],
        "title": "SportOut"
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "type": "array",
            "title": "Location"
          },
          "msg": {
            "type": "string",
            "title": "Message"
          },
          "type": {
            "type": "string",
            "title": "Error Type"
          },
          "input": {
            "title": "Input"
          },
          "ctx": {
            "type": "object",
            "title": "Context"
          }
        },
        "type": "object",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError"
      },
      "PlayerGameOut": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string"
          },
          "commence_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string"
          },
          "home_team": {
            "type": "string"
          },
          "away_team": {
            "type": "string"
          },
          "home_score": {
            "type": "integer",
            "nullable": true
          },
          "away_score": {
            "type": "integer",
            "nullable": true
          },
          "team_abbr": {
            "type": "string",
            "nullable": true,
            "description": "The box score's own team abbreviation for this player."
          },
          "player_team": {
            "type": "string",
            "nullable": true,
            "description": "Null when the side cannot be identified, and always for individual sports (tennis, golf, UFC)."
          },
          "opponent": {
            "type": "string",
            "nullable": true
          },
          "is_home": {
            "type": "boolean",
            "nullable": true,
            "description": "Null rather than guessed when the side is unidentifiable - a wrong home/away flag would corrupt every split built on it."
          },
          "stats": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Flat map of every stat_type recorded for this player in this game. Vocabulary is per-sport."
          }
        },
        "required": [
          "event_id",
          "commence_time",
          "status",
          "home_team",
          "away_team",
          "stats"
        ]
      },
      "PlayerGameLogOut": {
        "type": "object",
        "properties": {
          "player_name": {
            "type": "string"
          },
          "sport_key": {
            "type": "string"
          },
          "opponent": {
            "type": "string",
            "nullable": true
          },
          "games": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlayerGameOut"
            }
          }
        },
        "required": [
          "player_name",
          "sport_key",
          "games"
        ]
      }
    },
    "securitySchemes": {
      "APIKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      },
      "APIKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "apiKey"
      }
    }
  },
  "servers": [
    {
      "url": "https://api.prop-line.com",
      "description": "PropLine production"
    }
  ],
  "security": [
    {
      "APIKeyHeader": []
    },
    {
      "APIKeyQuery": []
    }
  ]
}
