{
  "openapi": "3.1.0",
  "info": {
    "title": "Big Ball Sports API",
    "description": "Unified sports data API (REST + GraphQL + WebSocket) aggregating 19+\nfree-tier upstream sources and 10 MCP scrapers behind a Stripe-style\ngateway. Every response carries a confidence score and source attribution\nso callers can render appropriate caveats.\n\n## Pagination\n\nList endpoints page with `?limit=` and `?offset=`. `?page=N` is an alias\nfor `?offset=(N-1)*limit`, resolved against that endpoint's own `limit`\ndefault, so `?page=2` and `?offset=<limit>` are the same request. When both\nare sent, `?offset=` wins; it is the more specific instruction.\n\nEndpoints that return a single page (leaderboards, one team's form, one\nplayer's stat line) document a `limit` and no `offset`. On those, `?page=`\nis **rejected with a 400** naming `?limit=` rather than accepted and\nignored, so a paging attempt can never silently look like it worked.\n",
    "version": "1.0.0",
    "contact": {
      "email": "support@bigballsdata.com"
    }
  },
  "servers": [
    {
      "url": "https://api.bigballsdata.com",
      "description": "Production"
    },
    {
      "url": "https://bbsgateway-production.up.railway.app",
      "description": "Production (Railway direct, fallback)"
    },
    {
      "url": "http://localhost:3000",
      "description": "Local development"
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Big Ball Sports API key. Or pass via `Authorization: Bearer <key>`."
      },
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "bbs_<env>_<32hex>"
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Stripe-style idempotency token. Retries with the same key within\n24h replay the original response without re-running the handler.\n"
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Rate limited (`429`). Two causes share `error.code: rate_limited` and are\ntold apart by `error.message` and headers:\n  - **Quota exhaustion** (per-minute or per-day cap). The envelope is\n    enriched: `suggested_fix` names the next tier and its cap, and\n    `meta.current_usage` + `meta.upgrade_path` carry live usage and the\n    upgrade target. `X-RateLimit-Limit/-Remaining/-Reset` are present.\n  - **4xx circuit breaker** (sustained high 4xx rate: malformed\n    requests, or ignoring Retry-After and hammering past quota; key in\n    cooldown). Carries `X-RateLimit-4xx-Cooldown`; no `upgrade_path`.\nAdditive: existing parsers keying on `error.code`, `error.message`,\n`docs_url`, `support`, `meta.request_id` are unaffected.\n`X-RateLimit-Limit/-Remaining/-Reset` are returned on every authenticated\nresponse (2xx/4xx/5xx), not only on 429s, so clients can self-throttle.\n",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the limit (or 4xx cooldown) resets.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "description": "Effective request cap (tighter bucket).",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests remaining in the tighter bucket.",
            "schema": {
              "type": "string"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds at which the tighter bucket resets.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-4xx-Cooldown": {
            "description": "Seconds remaining on the 4xx breaker cooldown (only on breaker 429s).",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "example": "rate_limited"
                    },
                    "message": {
                      "type": "string",
                      "example": "Daily quota exhausted (250 of 250 requests used)"
                    }
                  }
                },
                "suggested_fix": {
                  "type": "string",
                  "description": "Quota 429 only, tier-aware upgrade guidance.",
                  "example": "Pace requests using the Retry-After and X-RateLimit-* response headers. Handling guide: https://bigballsdata.com/docs/rate-limits. If you're regularly at this ceiling, the solo plan is $19/month and raises your daily limit to 10,000/day: https://bigballsdata.com/dashboard/billing?plan=solo."
                },
                "docs_url": {
                  "type": "string"
                },
                "support": {
                  "type": "object"
                },
                "meta": {
                  "type": "object",
                  "properties": {
                    "request_id": {
                      "type": "string"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "limiting_bucket": {
                      "type": "string",
                      "enum": [
                        "minute",
                        "day"
                      ],
                      "description": "Quota 429 only. WHICH of the two buckets refused this\nrequest, and therefore which cap `error.message` and\n`suggested_fix` are about. `current_usage` reports both\nbuckets without saying which one was binding; this says.\n\nUse it for backoff: a `minute` refusal clears within\nseconds, a `day` refusal not until the UTC rollover.\n`Retry-After` already carries the right wait in seconds.\n",
                      "example": "minute"
                    },
                    "current_usage": {
                      "type": "object",
                      "description": "Quota 429 only, live per-bucket usage.",
                      "properties": {
                        "minute": {
                          "type": "object",
                          "properties": {
                            "used": {
                              "type": "integer"
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "resets_at": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        },
                        "day": {
                          "type": "object",
                          "properties": {
                            "used": {
                              "type": "integer"
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "resets_at": {
                              "type": "string",
                              "format": "date-time"
                            }
                          }
                        }
                      }
                    },
                    "upgrade_path": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/UpgradePath"
                        }
                      ],
                      "description": "The upgrade target for THIS 429: the next tier up the\nladder, because the caller's problem is volume.\n`recommended_tier` is null at the top self-serve tier.\n"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Service temporarily unavailable (`503`). Returned when a backing query\nexceeds the endpoint's `statement_timeout` (Postgres cancels it) instead\nof holding the connection for seconds. Transient under heavy load;\nretry after a short delay. Standard error envelope with\n`error.code = upstream_unavailable`.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string",
                      "example": "upstream_unavailable"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                },
                "docs_url": {
                  "type": "string"
                },
                "support": {
                  "type": "object"
                },
                "meta": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "SingleMatchPrediction": {
        "type": "object",
        "description": "Latest stored prediction for one match. Check valid_until and expired before use.",
        "required": [
          "match_id",
          "prediction_type",
          "model_version",
          "valid_until",
          "expired",
          "generated_at"
        ],
        "properties": {
          "match_id": {
            "type": "string",
            "format": "uuid"
          },
          "prediction_type": {
            "type": "string",
            "enum": [
              "match_result",
              "value_prop"
            ]
          },
          "home_team": {
            "type": "string",
            "nullable": true
          },
          "away_team": {
            "type": "string",
            "nullable": true
          },
          "home_win_probability": {
            "type": "number",
            "nullable": true
          },
          "draw_probability": {
            "type": "number",
            "nullable": true
          },
          "away_win_probability": {
            "type": "number",
            "nullable": true
          },
          "home_elo": {
            "type": "number",
            "nullable": true
          },
          "away_elo": {
            "type": "number",
            "nullable": true
          },
          "model_version": {
            "type": "string"
          },
          "confidence": {
            "type": "number",
            "nullable": true
          },
          "valid_until": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "expired": {
            "type": "boolean",
            "nullable": true,
            "description": "Null means the prediction has no stated expiry."
          },
          "expired_days": {
            "type": "number",
            "nullable": true
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "kickoff_utc": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "group": {
            "type": "string",
            "nullable": true
          },
          "value_bet": {
            "type": "boolean",
            "nullable": true
          },
          "best_side": {
            "type": "string",
            "nullable": true
          },
          "elo_prob": {
            "type": "number",
            "nullable": true
          },
          "market_prob": {
            "type": "number",
            "nullable": true
          },
          "edge": {
            "type": "number",
            "nullable": true
          }
        }
      },
      "SingleMatchPredictionResponse": {
        "type": "object",
        "required": [
          "data",
          "meta"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SingleMatchPrediction"
          },
          "meta": {
            "type": "object",
            "required": [
              "returned",
              "expired",
              "expiry_unknown",
              "as_of"
            ],
            "properties": {
              "returned": {
                "type": "integer",
                "enum": [
                  1
                ]
              },
              "expired": {
                "type": "integer"
              },
              "expiry_unknown": {
                "type": "integer"
              },
              "as_of": {
                "type": "string",
                "format": "date-time"
              },
              "stale": {
                "type": "boolean",
                "description": "Present when the prediction is expired."
              },
              "message": {
                "type": "string",
                "description": "Present when the prediction is expired."
              }
            }
          }
        }
      },
      "UnifiedNbaTeamGame": {
        "type": "object",
        "required": [
          "id",
          "archive_team_id",
          "name",
          "short_name",
          "logo_url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Canonical team UUID."
          },
          "archive_team_id": {
            "type": "string",
            "nullable": true,
            "description": "Original archive participant ID; null on match-only rows."
          },
          "name": {
            "type": "string"
          },
          "short_name": {
            "type": "string",
            "nullable": true
          },
          "logo_url": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "UnifiedNbaMatch": {
        "type": "object",
        "description": "NBA's additive v1 match shape. It preserves the ordinary Match fields, adds archive identity/statistics, and preserves a date-only historical game as midnight UTC plus time_precision=date. The marker explicitly says that the time component is not a known tip-off.",
        "required": [
          "id",
          "sport",
          "league",
          "season",
          "game_date",
          "kickoff_utc",
          "time_precision",
          "status",
          "home",
          "away"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "sport": {
            "type": "string",
            "enum": [
              "basketball"
            ]
          },
          "league": {
            "type": "string",
            "enum": [
              "NBA"
            ]
          },
          "archive_game_id": {
            "type": "string",
            "nullable": true
          },
          "match_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "season": {
            "type": "integer"
          },
          "season_type": {
            "type": "string"
          },
          "game_date": {
            "type": "string",
            "format": "date"
          },
          "kickoff_utc": {
            "type": "string",
            "format": "date-time"
          },
          "time_precision": {
            "type": "string",
            "enum": [
              "date",
              "timestamp"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/MatchStatus"
          },
          "home": {
            "$ref": "#/components/schemas/UnifiedNbaTeamGame"
          },
          "away": {
            "$ref": "#/components/schemas/UnifiedNbaTeamGame"
          },
          "score": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/MatchScore"
              },
              {
                "type": "null"
              }
            ]
          },
          "linescore": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Linescore"
              },
              {
                "type": "null"
              }
            ]
          },
          "attendance": {
            "type": [
              "integer",
              "null"
            ]
          },
          "broadcast": {
            "type": [
              "string",
              "null"
            ]
          },
          "round": {
            "type": [
              "string",
              "null"
            ]
          },
          "has_odds": {
            "type": "boolean"
          },
          "archive_stats": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true
          }
        }
      },
      "NbaGameTeamBox": {
        "type": "object",
        "description": "One team's box score on an NBA game row. `oreb` and `dreb` are stored team totals and always satisfy `oreb + dreb = reb` when present; they are not derived from the play-by-play log.",
        "properties": {
          "team_id": {
            "type": "string",
            "nullable": true,
            "description": "Upstream numeric team id, stable across seasons. Not a BBS team UUID."
          },
          "abbreviation": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "matchup": {
            "type": "string",
            "nullable": true
          },
          "wl": {
            "type": "string",
            "nullable": true,
            "enum": [
              "W",
              "L",
              null
            ]
          },
          "pts": {
            "type": "integer",
            "nullable": true
          },
          "fgm": {
            "type": "integer",
            "nullable": true
          },
          "fga": {
            "type": "integer",
            "nullable": true
          },
          "fg_pct": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Exact decimal ratio. PostgreSQL numeric values may serialize as JSON strings (for example, '0.455')."
          },
          "fg3m": {
            "type": "integer",
            "nullable": true
          },
          "fg3a": {
            "type": "integer",
            "nullable": true
          },
          "fg3_pct": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Exact decimal ratio. PostgreSQL numeric values may serialize as a JSON string."
          },
          "ftm": {
            "type": "integer",
            "nullable": true
          },
          "fta": {
            "type": "integer",
            "nullable": true
          },
          "ft_pct": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "Exact decimal ratio. PostgreSQL numeric values may serialize as a JSON string."
          },
          "reb": {
            "type": "integer",
            "nullable": true,
            "description": "Total rebounds."
          },
          "oreb": {
            "type": "integer",
            "nullable": true,
            "description": "Offensive rebounds. Complete for the 1985-86 season onward; null for 1984-85 and earlier, where the split was not recorded. A 0 is a real zero, not a missing value."
          },
          "dreb": {
            "type": "integer",
            "nullable": true,
            "description": "Defensive rebounds. Same coverage boundary as `oreb`."
          },
          "ast": {
            "type": "integer",
            "nullable": true
          },
          "stl": {
            "type": "integer",
            "nullable": true
          },
          "blk": {
            "type": "integer",
            "nullable": true
          },
          "tov": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "NbaReboundSplitMeta": {
        "type": "object",
        "description": "Self-teaching disclosure of what `oreb`/`dreb` are and where they stop, returned on every /v1/nba/games response.",
        "properties": {
          "data_type": {
            "type": "string",
            "example": "stored_team_totals"
          },
          "coverage": {
            "type": "string",
            "description": "Which seasons carry the split."
          },
          "semantics": {
            "type": "string",
            "description": "What the numbers mean and how they were NOT produced."
          },
          "known_gap": {
            "type": "string",
            "description": "A season absent from this relation on every field, not just rebounds."
          }
        }
      },
      "NbaTeamSeasonAverage": {
        "type": "object",
        "description": "One team's season averages for ONE SLICE of the team cube. The grain is\n(season, season_type, category, stat_type), not one row per season: the\nsame team-season appears once per populated slice.\n\n`stats` is an OPEN OBJECT on purpose. The key set differs per\n(category, stat_type) because the upstream dashboards differ - 486\ndistinct keys across the 1,290 rows we hold - so flattening them into a\nfixed shape would invent nulls for every combination that does not carry\nthem. Values are JSON numbers except `play_type` and `type_grouping`,\nwhich are strings. Vendor-internal join ids are removed before the\nresponse is built.\n",
        "properties": {
          "team_id": {
            "type": "string",
            "format": "uuid",
            "description": "BigBalls team id."
          },
          "season": {
            "type": "integer",
            "example": 2025,
            "description": "Season LABEL: the year the season starts in."
          },
          "season_type": {
            "type": "string",
            "example": "regular",
            "description": "`regular` or `playoffs`. Only `regular` is populated today."
          },
          "category": {
            "type": "string",
            "example": "tracking",
            "description": "Cube dimension: general, clutch, tracking, playtype, shooting, shotdashboard or hustle."
          },
          "stat_type": {
            "type": "string",
            "example": "drives",
            "description": "Cube dimension inside the category. The EMPTY STRING is a real value, and it is the only one the `hustle` category uses, so `?stat_type=` selects that slice.\n"
          },
          "stats": {
            "type": "object",
            "additionalProperties": true,
            "description": "The slice's statistics. Key set varies by (category, stat_type)."
          }
        }
      },
      "NbaPlayerSeasonAverage": {
        "type": "object",
        "description": "One player's per-game averages for one regular season, as the vendor\npublishes them. Numeric fields are JSON numbers. Every column is\npopulated on every row we hold, so a `0` means the player really did\nrecord none of that stat per game; it is not a placeholder for\n\"unknown\".\n",
        "properties": {
          "player_id": {
            "type": "string",
            "format": "uuid",
            "description": "BigBalls player id."
          },
          "season": {
            "type": "integer",
            "example": 2025,
            "description": "Season LABEL: the year the season starts in."
          },
          "games_played": {
            "type": "integer",
            "nullable": true
          },
          "min": {
            "type": "string",
            "nullable": true,
            "example": "36:12",
            "description": "Average minutes per game as \"MM:SS\". This is a STRING because the vendor writes it as one; we do not convert it to a decimal, because that would be a number we invented published under the vendor's name.\n"
          },
          "pts": {
            "type": "number",
            "nullable": true,
            "description": "Points per game."
          },
          "ast": {
            "type": "number",
            "nullable": true,
            "description": "Assists per game."
          },
          "reb": {
            "type": "number",
            "nullable": true,
            "description": "Total rebounds per game."
          },
          "oreb": {
            "type": "number",
            "nullable": true,
            "description": "Offensive rebounds per game."
          },
          "dreb": {
            "type": "number",
            "nullable": true,
            "description": "Defensive rebounds per game."
          },
          "stl": {
            "type": "number",
            "nullable": true,
            "description": "Steals per game."
          },
          "blk": {
            "type": "number",
            "nullable": true,
            "description": "Blocks per game."
          },
          "turnover": {
            "type": "number",
            "nullable": true,
            "description": "Turnovers per game."
          },
          "pf": {
            "type": "number",
            "nullable": true,
            "description": "Personal fouls per game."
          },
          "fga": {
            "type": "number",
            "nullable": true
          },
          "fgm": {
            "type": "number",
            "nullable": true
          },
          "fg_pct": {
            "type": "number",
            "nullable": true,
            "description": "Field goal percentage as a ratio (0.576), not a percentage."
          },
          "fg3a": {
            "type": "number",
            "nullable": true
          },
          "fg3m": {
            "type": "number",
            "nullable": true
          },
          "fg3_pct": {
            "type": "number",
            "nullable": true,
            "description": "Three-point percentage as a ratio."
          },
          "fta": {
            "type": "number",
            "nullable": true
          },
          "ftm": {
            "type": "number",
            "nullable": true
          },
          "ft_pct": {
            "type": "number",
            "nullable": true,
            "description": "Free throw percentage as a ratio."
          }
        }
      },
      "NbaPlayerAdvancedSeason": {
        "type": "object",
        "description": "One player's advanced aggregates for one season and season type.\nPercentage-style fields are ratios (0.663), not percentages, except the\nturnover rates and `ast_ratio`, which the vendor publishes on a 0-100\nscale. `e_*` variants are the vendor's estimated counterparts to the\ncolumn of the same name.\n",
        "properties": {
          "player_id": {
            "type": "string",
            "format": "uuid"
          },
          "season": {
            "type": "integer",
            "example": 2025
          },
          "season_type": {
            "type": "string",
            "enum": [
              "regular",
              "playoffs"
            ]
          },
          "games_played": {
            "type": "integer",
            "nullable": true
          },
          "wins": {
            "type": "integer",
            "nullable": true
          },
          "losses": {
            "type": "integer",
            "nullable": true
          },
          "w_pct": {
            "type": "number",
            "nullable": true
          },
          "min": {
            "type": "number",
            "nullable": true,
            "description": "Average minutes as a decimal. Note this differs from `min` on NbaPlayerSeasonAverage, which is an \"MM:SS\" string; the two relations store it differently and neither is reshaped here."
          },
          "poss": {
            "type": "number",
            "nullable": true,
            "description": "Possessions."
          },
          "ts_pct": {
            "type": "number",
            "nullable": true,
            "description": "True shooting percentage."
          },
          "efg_pct": {
            "type": "number",
            "nullable": true,
            "description": "Effective field goal percentage."
          },
          "usg_pct": {
            "type": "number",
            "nullable": true,
            "description": "Usage rate."
          },
          "e_usg_pct": {
            "type": "number",
            "nullable": true
          },
          "ast_pct": {
            "type": "number",
            "nullable": true
          },
          "ast_ratio": {
            "type": "number",
            "nullable": true
          },
          "ast_to": {
            "type": "number",
            "nullable": true,
            "description": "Assist-to-turnover ratio."
          },
          "oreb_pct": {
            "type": "number",
            "nullable": true
          },
          "dreb_pct": {
            "type": "number",
            "nullable": true
          },
          "reb_pct": {
            "type": "number",
            "nullable": true
          },
          "off_rating": {
            "type": "number",
            "nullable": true,
            "description": "Offensive rating (points produced per 100 possessions)."
          },
          "def_rating": {
            "type": "number",
            "nullable": true,
            "description": "Defensive rating (points allowed per 100 possessions)."
          },
          "net_rating": {
            "type": "number",
            "nullable": true,
            "description": "Offensive rating minus defensive rating."
          },
          "e_off_rating": {
            "type": "number",
            "nullable": true
          },
          "e_def_rating": {
            "type": "number",
            "nullable": true
          },
          "e_net_rating": {
            "type": "number",
            "nullable": true
          },
          "pace": {
            "type": "number",
            "nullable": true,
            "description": "Possessions per 48 minutes."
          },
          "e_pace": {
            "type": "number",
            "nullable": true
          },
          "pace_per40": {
            "type": "number",
            "nullable": true
          },
          "tm_tov_pct": {
            "type": "number",
            "nullable": true
          },
          "e_tov_pct": {
            "type": "number",
            "nullable": true
          },
          "pie": {
            "type": "number",
            "nullable": true,
            "description": "Player impact estimate."
          },
          "ranks": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "integer"
            },
            "description": "The vendor's league rank for each column, keyed `<column>_rank` (e.g. `pie_rank`, `off_rating_rank`). Rank 1 is best; for `def_rating` and `l_rank` that means lowest.\n"
          }
        }
      },
      "NbaPlayerAdvancedGameStat": {
        "type": "object",
        "description": "One player's advanced and tracking measures for one game, or for one\nperiod of it. `period` is 0 for the whole game, 1-4 for quarters and 5+\nfor overtimes.\n\n`stats` is a flat map of 116 measures. It is a map rather than a fixed\nfield list because the relation stores it that way upstream and\nenumerating the keys here would let the contract and the data drift\nsilently; every value is a number, except `matchup_minutes`, which the\nvendor publishes as an \"MM:SS\" string and is not reshaped.\n",
        "properties": {
          "player_id": {
            "type": "string",
            "format": "uuid"
          },
          "team_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "BigBalls team id. Null where the game has no bridged team row."
          },
          "match_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "BigBalls match id, joinable to /v1/matches. Null for 2,097 of 132,621 rows whose game has no canonical match."
          },
          "season": {
            "type": "integer",
            "example": 2025
          },
          "game_date": {
            "type": "string",
            "format": "date"
          },
          "period": {
            "type": "integer",
            "description": "0 is the whole game; 1-4 are quarters; 5+ are overtimes."
          },
          "postseason": {
            "type": "boolean"
          },
          "stats": {
            "type": "object",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "number"
                },
                {
                  "type": "string"
                }
              ]
            },
            "description": "Flat map of the 116 advanced and tracking measures."
          }
        }
      },
      "NbaPlayerContract": {
        "type": "object",
        "description": "One NBA contract, as a career total. Money is whole US dollars\n(`meta.currency` is `USD`). A null money field means the figure is\nabsent upstream, which is ordinary on two-way, ten-day and\nrest-of-season deals; it never means zero.\n",
        "properties": {
          "player_id": {
            "type": "string",
            "format": "uuid"
          },
          "team_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "start_year": {
            "type": "integer",
            "description": "Season LABEL the deal starts in."
          },
          "end_year": {
            "type": "integer",
            "description": "Season LABEL the deal ends in."
          },
          "contract_type": {
            "type": "string",
            "nullable": true,
            "example": "Veteran Extension"
          },
          "contract_status": {
            "type": "string",
            "nullable": true,
            "example": "CURRENT"
          },
          "contract_years": {
            "type": "integer",
            "nullable": true
          },
          "total_value": {
            "type": "integer",
            "nullable": true
          },
          "average_salary": {
            "type": "integer",
            "nullable": true
          },
          "guaranteed_at_signing": {
            "type": "integer",
            "nullable": true
          },
          "total_guaranteed": {
            "type": "integer",
            "nullable": true
          },
          "signed_using": {
            "type": "string",
            "nullable": true,
            "example": "Bird Rights"
          },
          "free_agent_year": {
            "type": "integer",
            "nullable": true
          },
          "free_agent_status": {
            "type": "string",
            "nullable": true,
            "example": "UFA"
          },
          "contract_notes": {
            "nullable": true,
            "description": "Free-form notes as stored: usually a list of strings (option years, trade bonuses)."
          }
        }
      },
      "NbaPlayerContractSeason": {
        "type": "object",
        "description": "One contract-season. MORE THAN ONE ROW PER SEASON IS REAL DATA, not a\nduplicate: a player traded mid-deal, or signed to a second contract\ninside one league year, has two, and both figures are true. Nothing is\ndeduplicated here.\n",
        "properties": {
          "player_id": {
            "type": "string",
            "format": "uuid"
          },
          "team_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "season": {
            "type": "integer",
            "description": "Season LABEL, not a calendar year."
          },
          "cap_hit": {
            "type": "integer",
            "nullable": true,
            "description": "Whole US dollars."
          },
          "total_cash": {
            "type": "integer",
            "nullable": true,
            "description": "Whole US dollars."
          },
          "base_salary": {
            "type": "integer",
            "nullable": true,
            "description": "Whole US dollars."
          },
          "salary_rank": {
            "type": "integer",
            "nullable": true,
            "description": "League salary rank for that season. 1 is the highest paid."
          }
        }
      },
      "NflPlayerWeekStat": {
        "type": "object",
        "description": "One player's stat line for one game-week. All stat fields are returned\nfor every row regardless of position: a quarterback row carries 0 or\nnull in the defensive columns, a linebacker row carries 0 or null in\nthe passing columns.\n\nDefensive columns come from the same weekly player data as the\noffensive ones and cover 2020 onward at the same grain.\n\n**TACKLE TOTALS WILL NOT MATCH `/v1/stored/matches/{id}/stats`, and that\nis expected.** This dataset carries three tackle counts and no combined\ntotal: `tackles_solo`, `tackle_assists` (assisted tackles the player\ntook part in) and `tackles_with_assist` (a much smaller, separate\ncount). The other endpoint serves a single `totalTackles` from a\ndifferent provider, at per-match rather than per-game-week grain, over\na different set of seasons.\n\nWe do NOT derive a combined total here. The usual definition is solo +\nassisted, but the exact relationship between these three counts\nwas not verified against a reference box score, and a plausible number\ncomputed from an unverified relationship is worse than no number; it\nis indistinguishable from a correct one. Add `tackles_solo` and\n`tackle_assists` yourself if you need a total, knowing that is your\ndefinition rather than ours.\n",
        "properties": {
          "player_id": {
            "type": "string",
            "example": "00-0033873",
            "description": "NFL player id (gsis format)."
          },
          "player_name": {
            "type": "string",
            "nullable": true
          },
          "team": {
            "type": "string",
            "nullable": true,
            "description": "Team abbreviation the player suited up for that week."
          },
          "season": {
            "type": "integer",
            "example": 2025,
            "description": "Season LABEL: the year the season starts in."
          },
          "week": {
            "type": "integer",
            "example": 1
          },
          "season_type": {
            "type": "string",
            "nullable": true,
            "enum": [
              "REG",
              "POST",
              "PRE"
            ]
          },
          "position": {
            "type": "string",
            "nullable": true,
            "example": "WR"
          },
          "passing_yards": {
            "type": "integer",
            "nullable": true
          },
          "passing_tds": {
            "type": "integer",
            "nullable": true
          },
          "completions": {
            "type": "integer",
            "nullable": true
          },
          "attempts": {
            "type": "integer",
            "nullable": true
          },
          "rushing_yards": {
            "type": "integer",
            "nullable": true
          },
          "rushing_tds": {
            "type": "integer",
            "nullable": true
          },
          "carries": {
            "type": "integer",
            "nullable": true
          },
          "receiving_yards": {
            "type": "integer",
            "nullable": true
          },
          "receiving_tds": {
            "type": "integer",
            "nullable": true
          },
          "receptions": {
            "type": "integer",
            "nullable": true
          },
          "targets": {
            "type": "integer",
            "nullable": true
          },
          "fantasy_points_ppr": {
            "type": "number",
            "nullable": true,
            "description": "Standard PPR: 1/reception, 0.1/rush-rec yard, 0.04/pass yard, 6/TD, -2/int or fumble lost."
          },
          "interceptions": {
            "type": "integer",
            "nullable": true,
            "deprecated": true,
            "description": "DEPRECATED and ALWAYS NULL. The loader read a column the dataset\ndoes not carry, so this was never written in any of 112,333 rows. The\nkey is retained only so existing clients do not break. Read\n`interceptions_thrown` or `interceptions_caught`, which say which\none they mean.\n"
          },
          "interceptions_thrown": {
            "type": "integer",
            "nullable": true,
            "description": "Interceptions thrown by this player (the quarterback turnover stat)."
          },
          "interceptions_caught": {
            "type": "integer",
            "nullable": true,
            "description": "Interceptions caught by this player (the defensive takeaway stat)."
          },
          "tackles_solo": {
            "type": "integer",
            "nullable": true,
            "description": "Unassisted tackles. Not a combined total; see the schema description before comparing against totalTackles elsewhere."
          },
          "tackle_assists": {
            "type": "integer",
            "nullable": true,
            "description": "Assisted tackles this player participated in. Not interchangeable with tackles_with_assist."
          },
          "tackles_with_assist": {
            "type": "integer",
            "nullable": true,
            "description": "Tackles made WITH an assist from a team-mate. A much smaller count than tackle_assists."
          },
          "tackles_for_loss": {
            "type": "integer",
            "nullable": true,
            "description": "Tackles made behind the line of scrimmage."
          },
          "tackles_for_loss_yards": {
            "type": "integer",
            "nullable": true,
            "description": "Yards lost by the offence on those tackles."
          },
          "sacks": {
            "type": "number",
            "nullable": true,
            "example": 1.5,
            "description": "Sacks credited. FRACTIONAL; a shared sack is credited as 0.5 to each defender."
          },
          "sack_yards": {
            "type": "number",
            "nullable": true,
            "description": "Yards lost on this player's sacks. Fractional for the same half-credit reason as sacks."
          },
          "qb_hits": {
            "type": "integer",
            "nullable": true,
            "description": "Hits on the quarterback",
            "sacks excluded.": null
          },
          "passes_defended": {
            "type": "integer",
            "nullable": true,
            "description": "Passes broken up without intercepting."
          },
          "fumbles_forced": {
            "type": "integer",
            "nullable": true,
            "description": "Fumbles forced. Forcing is not recovering; recoveries are not currently served."
          },
          "interception_yards": {
            "type": "integer",
            "nullable": true,
            "description": "Return yards on this player's interceptions. MAY BE NEGATIVE; a return can be tackled behind the catch point."
          },
          "defensive_tds": {
            "type": "integer",
            "nullable": true,
            "description": "Touchdowns scored on defence. Distinct from passing_tds / rushing_tds / receiving_tds on the same row."
          },
          "safeties": {
            "type": "integer",
            "nullable": true
          },
          "punt_blocks": {
            "type": "integer",
            "nullable": true
          },
          "pat_blocks": {
            "type": "integer",
            "nullable": true,
            "description": "Extra-point attempts blocked."
          },
          "fg_blocks": {
            "type": "integer",
            "nullable": true,
            "description": "Field-goal attempts blocked."
          },
          "defensive_two_point_attempts": {
            "type": "integer",
            "nullable": true,
            "description": "Two-point conversion returns attempted by the defence."
          },
          "defensive_two_point_conversions": {
            "type": "integer",
            "nullable": true,
            "description": "Two-point conversion returns scored by the defence."
          },
          "times_sacked": {
            "type": "integer",
            "nullable": true,
            "description": "Times this OFFENSIVE player (almost always a QB) was sacked. Distinct from `sacks` above, which is the DEFENSIVE credit."
          },
          "sack_yards_lost": {
            "type": "number",
            "nullable": true,
            "description": "Yards lost on this player's own sacks. Distinct from `sack_yards` above, which is the DEFENSIVE figure."
          },
          "passing_air_yards": {
            "type": "number",
            "nullable": true,
            "description": "Weekly sum of air yards on this player's pass attempts. Per-play value on GET /v1/nfl/games/{id}/plays as `air_yards`."
          },
          "passing_yards_after_catch": {
            "type": "number",
            "nullable": true,
            "description": "Weekly sum of yards after the catch on this player's completions. Unofficial figure."
          },
          "passing_first_downs": {
            "type": "integer",
            "nullable": true
          },
          "passing_epa": {
            "type": "number",
            "nullable": true,
            "description": "Total EPA on this player's pass attempts and sacks."
          },
          "passing_2pt_conversions": {
            "type": "integer",
            "nullable": true
          },
          "dakota": {
            "type": "number",
            "nullable": true,
            "description": "Adjusted EPA + CPOE composite. A published advanced metric, not derived by us."
          },
          "rushing_first_downs": {
            "type": "integer",
            "nullable": true
          },
          "rushing_epa": {
            "type": "number",
            "nullable": true
          },
          "rushing_2pt_conversions": {
            "type": "integer",
            "nullable": true
          },
          "receiving_air_yards": {
            "type": "number",
            "nullable": true,
            "description": "Weekly sum of air yards on targets to this player. Per-play value on GET /v1/nfl/games/{id}/plays as `air_yards`."
          },
          "receiving_yards_after_catch": {
            "type": "number",
            "nullable": true,
            "description": "Weekly sum of yards after the catch by this receiver. Per-play value on GET /v1/nfl/games/{id}/plays as `yards_after_catch`."
          },
          "receiving_2pt_conversions": {
            "type": "integer",
            "nullable": true
          },
          "fantasy_points": {
            "type": "number",
            "nullable": true,
            "description": "Standard (non-PPR) fantasy total. See fantasy_points_ppr for the PPR total."
          },
          "air_yards_share": {
            "type": "number",
            "nullable": true,
            "description": "This player's share of the team's air yards this game-week. Fraction in [0,1]."
          },
          "pacr": {
            "type": "number",
            "nullable": true,
            "description": "Passing (yards) Air (yards) Conversion Ratio. A published advanced metric, not derived by us."
          },
          "racr": {
            "type": "number",
            "nullable": true,
            "description": "Receiving (yards) Air (yards) Conversion Ratio. A published advanced metric, not derived by us."
          },
          "receiving_epa": {
            "type": "number",
            "nullable": true
          },
          "receiving_first_downs": {
            "type": "integer",
            "nullable": true
          },
          "special_teams_tds": {
            "type": "integer",
            "nullable": true,
            "description": "Kick/punt return touchdowns. Distinct from defensive_tds and the offensive *_tds columns."
          },
          "wopr": {
            "type": "number",
            "nullable": true,
            "description": "Weighted Opportunity Rating: 1.5 x target_share + 0.7 x air_yards_share. A published advanced metric, not derived by us."
          },
          "target_share": {
            "type": "number",
            "nullable": true,
            "description": "This player's share of the team's receiving targets this game-week. Fraction in [0,1]."
          }
        }
      },
      "ResponseMeta": {
        "type": "object",
        "description": "Response metadata. NOTHING here is guaranteed: measured over 38 live envelopes on 2026-09-18, `cached` and `request_id` appeared in 29, `source` in 28, `cache_age_ms` in 9 and `confidence` in 6. This block previously declared all five REQUIRED, which no response has ever satisfied. Treat every field as optional and branch on its absence. Routes add their own keys here (`count`, `limit`, `offset`, `note`, `total`, and per-endpoint ones), so do not assume a closed set.",
        "properties": {
          "date_window": {
            "type": "object",
            "description": "Present when ?date= was applied. The exact window the filter used, so an omission at a day boundary is visible. start_utc is inclusive, end_utc exclusive. timezone is UTC, or the zone named by ?tz=, in which case the window is that local day. Boxing card dates are published event dates, so timezone and bounds are null there.",
            "properties": {
              "date": {
                "type": "string"
              },
              "timezone": {
                "type": "string",
                "nullable": true,
                "example": "UTC"
              },
              "start_utc": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "example": "2026-09-13T00:00:00.000Z"
              },
              "end_utc": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "example": "2026-09-14T00:00:00.000Z"
              },
              "note": {
                "type": "string"
              }
            }
          },
          "source": {
            "type": "string",
            "description": "Opaque source-tier label. Customers never see the vendor identity.\n`forecast` marks a weather forecast (GET /v1/matches/{id}/weather).\n",
            "enum": [
              "official-league",
              "aggregator-paid",
              "aggregator-free",
              "community-scraper",
              "forecast",
              "mixed",
              "cache",
              "none",
              "unknown-source",
              "stored",
              "catalogue",
              "gateway",
              "coverage-snapshot"
            ],
            "example": "official-league"
          },
          "confidence": {
            "type": "number",
            "format": "float",
            "minimum": 0,
            "maximum": 1,
            "example": 0.95
          },
          "cached": {
            "type": "boolean"
          },
          "cache_age_ms": {
            "type": "integer",
            "example": 4200
          },
          "request_id": {
            "type": "string",
            "example": "req_abc123"
          },
          "fields_missing": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "note": {
            "type": "string",
            "description": "Short factual disclosure about something this response deliberately\nwithheld, present only when there is something to disclose.\n\nOn live scores it appears when `match_id` could not be resolved to a\nstored match and was therefore omitted: it gives the counts and\npoints at `/v1/stored/matches`, whose ids you can call back into.\nAbsent when every served id resolves.\n\nDistinct from `message`, which explains `coverage: false` or a stale\nresponse. This one is attached to a response that DID serve data and\nnames the part of it that is deliberately missing.\n",
            "example": "match_id is omitted on 380 of 380 rows in this response. The live feed behind them does not carry an id we can resolve to a stored match, so we return no id rather than one that will not work. For rows with ids you can call back into, use /v1/stored/matches with the same sport or league, for example /v1/stored/matches?league=epl."
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "example": "NOT_FOUND"
          },
          "message": {
            "type": "string"
          },
          "details": {}
        }
      },
      "Envelope": {
        "type": "object",
        "description": "The standard response wrapper. Only `data` is guaranteed. `error` is absent (not null) on 16 of 40 live 200 responses measured 2026-09-18, so test for the KEY before reading it; this block declared it required until then. On a non-2xx, `error` is present and `data` is absent.",
        "required": [
          "data"
        ],
        "properties": {
          "data": {},
          "meta": {
            "$ref": "#/components/schemas/ResponseMeta"
          },
          "error": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ApiError"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "FieldResult": {
        "type": "object",
        "required": [
          "value",
          "source",
          "via",
          "confidence",
          "fetchedAt",
          "ttlSeconds"
        ],
        "properties": {
          "value": {
            "description": "The canonical value for this field. Shape varies by FieldKey."
          },
          "source": {
            "type": "string"
          },
          "via": {
            "type": "string",
            "enum": [
              "api",
              "cache",
              "mcp"
            ]
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "fetchedAt": {
            "type": "string",
            "format": "date-time"
          },
          "ttlSeconds": {
            "type": "integer"
          }
        }
      },
      "CricketBattingCareer": {
        "type": "object",
        "required": [
          "innings",
          "not_outs",
          "runs",
          "balls_faced",
          "fifties",
          "hundreds",
          "ducks",
          "fours",
          "sixes"
        ],
        "properties": {
          "innings": {
            "type": "integer"
          },
          "not_outs": {
            "type": "integer",
            "description": "Innings ended undismissed (includes retired hurt / retired not out)."
          },
          "runs": {
            "type": "integer"
          },
          "balls_faced": {
            "type": "integer"
          },
          "average": {
            "type": [
              "number",
              "null"
            ],
            "description": "runs / dismissals. null when never dismissed."
          },
          "strike_rate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Runs per 100 balls. null when no balls faced are recorded."
          },
          "highest_score": {
            "type": [
              "integer",
              "null"
            ]
          },
          "highest_score_not_out": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "The asterisk in cricket's `254*`."
          },
          "fifties": {
            "type": "integer",
            "description": "Scores of 50–99. A hundred counts only in `hundreds`."
          },
          "hundreds": {
            "type": "integer"
          },
          "ducks": {
            "type": "integer",
            "description": "Dismissed for 0."
          },
          "fours": {
            "type": "integer"
          },
          "sixes": {
            "type": "integer"
          }
        }
      },
      "CricketBowlingCareer": {
        "type": "object",
        "required": [
          "innings",
          "balls_bowled",
          "maidens",
          "runs_conceded",
          "wickets"
        ],
        "properties": {
          "innings": {
            "type": "integer"
          },
          "overs": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cricket notation, e.g. \"206.4\" (206 overs and 4 balls). Derived from balls_bowled."
          },
          "balls_bowled": {
            "type": "integer"
          },
          "maidens": {
            "type": "integer"
          },
          "runs_conceded": {
            "type": "integer"
          },
          "wickets": {
            "type": "integer"
          },
          "average": {
            "type": [
              "number",
              "null"
            ],
            "description": "runs conceded / wickets. null when wicketless."
          },
          "economy": {
            "type": [
              "number",
              "null"
            ],
            "description": "Runs conceded per over. null when no balls bowled."
          },
          "strike_rate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Balls per wicket. null when wicketless."
          },
          "best_bowling": {
            "type": [
              "string",
              "null"
            ],
            "description": "Best innings figures as wickets/runs, e.g. \"5/23\". null when wicketless."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error",
          "docs_url",
          "support",
          "meta"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "example": "bad_request"
              },
              "message": {
                "type": "string",
                "example": "One or more query parameters are invalid."
              },
              "validation_errors": {
                "description": "Structured field errors (e.g. a zod flatten) when applicable."
              }
            }
          },
          "suggested_fix": {
            "type": "string",
            "description": "A concrete corrected request, present only when unambiguously inferable.",
            "example": "Lower limit to 200 or less, then walk the rest with ?offset=200, ?offset=400, … (?page=2 is an alias for the same thing)."
          },
          "docs_url": {
            "type": "string",
            "example": "https://bigballsdata.com/docs/errors"
          },
          "support": {
            "type": "object",
            "required": [
              "discord",
              "email"
            ],
            "properties": {
              "discord": {
                "type": "string",
                "example": "https://discord.gg/H2WJBQurbq"
              },
              "email": {
                "type": "string",
                "example": "support@bigballsdata.com"
              }
            }
          },
          "meta": {
            "type": "object",
            "required": [
              "request_id",
              "timestamp"
            ],
            "properties": {
              "request_id": {
                "type": "string",
                "example": "req_abc123"
              },
              "timestamp": {
                "type": "string",
                "format": "date-time"
              },
              "upgrade_path": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/UpgradePath"
                  }
                ],
                "description": "Present ONLY on the two errors where upgrading is the literal\nfix: the quota `429` and a plan-gate `403 forbidden`. Absent on\nevery other error, including the `403 key_paused` reactivation\nerror; a paused key is reactivated, not bought.\n"
              }
            }
          }
        }
      },
      "UpgradePath": {
        "type": "object",
        "description": "The machine-readable half of an upgrade prompt: what to buy, what it\ncosts, and where to buy it. Identical in shape on the quota `429` and\nthe plan-gate `403`, so one parser handles both, but note the two\nrecommend DIFFERENT things, correctly. The 429 recommends the next tier\nUP (the problem is volume); the 403 recommends the LOWEST tier that\nunlocks the endpoint (the problem is access), so a free key blocked on\nan Edge route is pointed at Edge, never at Solo.\n\nEvery figure is real: `price` is the published list price, and\n`daily_limit` and `minute_limit` are the two caps the gateway actually\nenforces. Nothing here is computed from a per-request rate, and there\nis no urgency or scarcity framing anywhere in these bodies.\n\nBOTH caps are always present, because the limiter has two buckets and\neither can be the one that refused you. On a `429`, read\n`meta.limiting_bucket` to know which of them you actually hit.\n",
        "required": [
          "current_tier",
          "recommended_tier",
          "price",
          "daily_limit",
          "minute_limit",
          "url"
        ],
        "properties": {
          "current_tier": {
            "type": "string",
            "description": "The caller's tier at the time of the error.",
            "example": "free"
          },
          "recommended_tier": {
            "type": "string",
            "nullable": true,
            "description": "Null only where there is nothing to recommend: the top of the\nself-serve ladder on a 429.\n",
            "example": "edge"
          },
          "price": {
            "type": "string",
            "nullable": true,
            "description": "Monthly list price. Null for a tier that is quoted rather than\nlisted (Enterprise), in which case `url` is the pricing page rather\nthan a checkout link.\n",
            "example": "$149/month"
          },
          "daily_limit": {
            "type": "integer",
            "nullable": true,
            "description": "Requests/day granted by `recommended_tier`; null if unmetered.",
            "example": 100000
          },
          "minute_limit": {
            "type": "integer",
            "nullable": true,
            "description": "Requests/MINUTE granted by `recommended_tier`; null if unmetered.\n\nAdded 2026-09-18. The per-minute bucket is the one a bursty client\nactually hits: on 2026-09-18, the first day the gateway labelled\nthe two apart, 1,284 quota refusals were per-minute against 241\nper-day, and until this field the object could only ever quote the\ndaily cap while the prose correctly quoted the per-minute one.\n",
            "example": 1500
          },
          "url": {
            "type": "string",
            "description": "Checkout, deep-linked to `recommended_tier`, whenever there is a\nlisted price to buy. Falls back to the pricing page when there is\nnot; we never deep-link a purchase that cannot complete.\n",
            "example": "https://bigballsdata.com/dashboard/billing?plan=edge"
          }
        }
      },
      "StoredId": {
        "type": "string",
        "format": "uuid",
        "description": "A stored BigBalls id. This is the identifier every match, team, player and league endpoint returns, and the one every `{id}` path segment accepts. Take it from the `id` field of the corresponding list endpoint and use it as-is.",
        "example": "cf2d81c8-8e7c-4eaf-a515-c51be3962e57"
      },
      "LeagueCode": {
        "type": "string",
        "description": "A short league code, not an id. `GET /v1/leagues` returns the authoritative list in its `id` field; that is the value to pass to every `?league=` parameter. Some codes accept an alias (`serie_a` and `seriea` both resolve).",
        "example": "epl"
      },
      "CanonicalId": {
        "type": "string",
        "description": "BigBalls canonical id, format `bb_<entity>_<10-32 lowercase base32 chars>`. NARROW SCOPE: the Formula 1 stored-session surface is the only place this is minted and the only place it is accepted. F1 sessions, drivers and constructors are keyed this way end to end (1,668 of 1,668 stored result rows on 2026-09-18). Everywhere else, ids are a `StoredId` and this format is REJECTED with a 400 - so do not construct one, and do not carry one across surfaces.",
        "pattern": "^bb_(player|team|league|circuit|session)_[a-z0-9]{10,32}$",
        "example": "bb_session_du57sgi3ttnx"
      },
      "Sport": {
        "type": "string",
        "enum": [
          "football",
          "basketball",
          "baseball",
          "ice_hockey",
          "cricket",
          "mma",
          "boxing",
          "formula1",
          "american_football"
        ]
      },
      "SessionType": {
        "type": "string",
        "enum": [
          "practice",
          "qualifying",
          "sprint",
          "race"
        ]
      },
      "MatchStatus": {
        "type": "string",
        "enum": [
          "scheduled",
          "live",
          "in_progress",
          "finished",
          "postponed",
          "cancelled",
          "suspended"
        ]
      },
      "OddsMarket": {
        "type": "string",
        "enum": [
          "moneyline",
          "spread",
          "total",
          "btts",
          "draw_no_bet",
          "double_chance"
        ]
      },
      "SelectionName": {
        "type": "string",
        "enum": [
          "home",
          "away",
          "draw",
          "over",
          "under",
          "yes",
          "no",
          "home_or_draw",
          "away_or_draw",
          "home_or_away"
        ]
      },
      "MetricUnit": {
        "type": "string",
        "enum": [
          "count",
          "percent",
          "seconds",
          "meters",
          "unit"
        ]
      },
      "CricketFormat": {
        "type": "string",
        "enum": [
          "t10",
          "t20",
          "odi",
          "test",
          "first_class"
        ]
      },
      "FightMethod": {
        "type": "string",
        "enum": [
          "ko",
          "tko",
          "submission",
          "unanimous_decision",
          "split_decision",
          "majority_decision",
          "technical_decision",
          "disqualification",
          "no_contest",
          "draw"
        ]
      },
      "FightStance": {
        "type": "string",
        "enum": [
          "orthodox",
          "southpaw",
          "switch"
        ]
      },
      "SportInfo": {
        "type": "object",
        "properties": {
          "slug": {
            "$ref": "#/components/schemas/Sport"
          },
          "name": {
            "type": "string",
            "example": "Football"
          }
        }
      },
      "Team": {
        "type": "object",
        "required": [
          "id",
          "sport",
          "name"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "sport": {
            "$ref": "#/components/schemas/Sport"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "short_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "league": {
            "type": [
              "string",
              "null"
            ],
            "description": "League display name",
            "e.g. NHL.": null,
            "example": "NHL"
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Treat as opaque; do not construct it."
          },
          "flag_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "National-team flag where applicable",
            "else null.": null
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Z]{3}$",
            "description": "ISO 3166-1 alpha-3. NOT EMITTED by /v1/teams or /v1/teams/{id} as measured on 2026-09-18; branch on its absence."
          },
          "founded_year": {
            "type": "integer",
            "minimum": 1800,
            "maximum": 2100,
            "description": "NOT EMITTED as measured on 2026-09-18."
          }
        }
      },
      "TeamAggregateStats": {
        "type": "object",
        "description": "Aggregate over EVERY finished match we hold for the team, not the current season; `meta.stats_scope` restates that on each response. The goal-shaped names are used for all sports: `goals_scored` is runs in baseball and points in basketball.",
        "properties": {
          "matches_played": {
            "type": "integer",
            "minimum": 0
          },
          "matches_with_scores": {
            "type": "integer",
            "minimum": 0
          },
          "wins": {
            "type": "integer",
            "minimum": 0
          },
          "draws": {
            "type": "integer",
            "minimum": 0
          },
          "losses": {
            "type": "integer",
            "minimum": 0
          },
          "goals_scored": {
            "type": "integer",
            "minimum": 0
          },
          "goals_conceded": {
            "type": "integer",
            "minimum": 0
          },
          "goal_difference": {
            "type": "integer"
          },
          "clean_sheets": {
            "type": "integer",
            "minimum": 0
          },
          "points": {
            "type": "integer",
            "minimum": 0
          },
          "form_string": {
            "type": [
              "string",
              "null"
            ],
            "description": "Most recent results",
            "newest first.": null,
            "example": "WLWLL"
          },
          "btts_rate": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "over_2_5_rate": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "avg_goals_scored": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0
          },
          "avg_goals_conceded": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0
          }
        }
      },
      "TeamDetail": {
        "description": "The team exactly as `GET /v1/teams` lists it, plus the `stats` aggregate that `GET /v1/teams/{id}/stats` returns. One implementation serves both, so the two cannot disagree.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Team"
          },
          {
            "type": "object",
            "properties": {
              "stats": {
                "$ref": "#/components/schemas/TeamAggregateStats"
              }
            }
          }
        ]
      },
      "Player": {
        "type": "object",
        "required": [
          "id",
          "sport",
          "full_name",
          "display_name"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "sport": {
            "$ref": "#/components/schemas/Sport"
          },
          "full_name": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "current_team_id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "position": {
            "type": "string"
          },
          "shirt_number": {
            "type": "integer",
            "minimum": 0,
            "maximum": 99
          },
          "date_of_birth": {
            "type": "string",
            "format": "date"
          },
          "nationality": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          },
          "height_cm": {
            "type": "integer",
            "minimum": 120,
            "maximum": 260
          },
          "weight_kg": {
            "type": "number",
            "minimum": 35,
            "maximum": 250
          },
          "headshot_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Player headshot, served from our own `/v1/assets/{id}` endpoint (or, for official league CDNs, as a direct hotlink). ALWAYS PRESENT when a route serves this field, and `null` rather than omitted when there is no image; so `'headshot_url' in player` asks whether the route serves headshots, while `player.headshot_url !== null` asks whether this player has one.\n\nPlan-gated as a field inside a 200, never a 403: below Solo the key is present with `null` and the response carries one `meta.headshot_note` naming the plan that grants it. Solo and above get the URL. Added to `/v1/players/{id}` on 2026-09-28; it was absent there on every tier, including Pro, while `/v1/players`, `/v1/players/{id}/profile` and `/v1/stored/players` all served it."
          }
        }
      },
      "League": {
        "type": "object",
        "required": [
          "id",
          "sport",
          "name"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/LeagueCode"
          },
          "sport": {
            "$ref": "#/components/schemas/Sport"
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          }
        }
      },
      "PeriodScore": {
        "type": "object",
        "required": [
          "period",
          "label",
          "home",
          "away"
        ],
        "properties": {
          "period": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20
          },
          "label": {
            "type": "string"
          },
          "home": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500
          },
          "away": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500
          }
        }
      },
      "Clock": {
        "type": "object",
        "required": [
          "elapsed_seconds",
          "period"
        ],
        "properties": {
          "elapsed_seconds": {
            "type": "integer",
            "minimum": 0
          },
          "period": {
            "type": "integer",
            "minimum": 0,
            "maximum": 20
          },
          "added_seconds": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "Score": {
        "type": "object",
        "required": [
          "home",
          "away",
          "status",
          "updated_at"
        ],
        "properties": {
          "match_id": {
            "type": "string",
            "format": "uuid",
            "description": "Stored match id, usable as-is with `/v1/matches/{id}`, `/v1/stored/matches/{id}/stats` and the other id-taking endpoints. OMITTED when this live row cannot be resolved to a match we hold; an absent id is a condition you can branch on, an id that does not resolve is not. The scores on such a row are still real.",
            "example": "71c604d0-0193-4e75-aba4-cc76822e3508"
          },
          "home": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 500
          },
          "away": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 500
          },
          "status": {
            "$ref": "#/components/schemas/MatchStatus"
          },
          "period_scores": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PeriodScore"
            }
          },
          "clock": {
            "$ref": "#/components/schemas/Clock"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Selection": {
        "type": "object",
        "required": [
          "name",
          "decimal_odds"
        ],
        "properties": {
          "name": {
            "$ref": "#/components/schemas/SelectionName"
          },
          "decimal_odds": {
            "type": "number",
            "minimum": 1.01,
            "maximum": 1000
          },
          "line": {
            "type": "number"
          }
        }
      },
      "Odds": {
        "type": "object",
        "required": [
          "match_id",
          "market",
          "selections",
          "updated_at"
        ],
        "properties": {
          "match_id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "market": {
            "$ref": "#/components/schemas/OddsMarket"
          },
          "selections": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/Selection"
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Stat": {
        "type": "object",
        "required": [
          "metric",
          "value",
          "unit",
          "updated_at"
        ],
        "properties": {
          "match_id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "team_id": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StoredId"
              },
              {
                "type": "null"
              }
            ]
          },
          "player_id": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StoredId"
              },
              {
                "type": "null"
              }
            ]
          },
          "metric": {
            "type": "string",
            "description": "Canonical metric name from a closed set."
          },
          "value": {
            "type": "number"
          },
          "unit": {
            "$ref": "#/components/schemas/MetricUnit"
          },
          "period": {
            "type": "integer",
            "minimum": 0,
            "maximum": 20
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Event": {
        "type": "object",
        "description": "One play/incident row from `/v1/matches/{id}/events`. The ordering key is `sequence_number`, and there is no per-event `id` or `match_id`: the match is the one in the path. Until 2026-09-18 this schema required `id`, `match_id` and `sequence`, none of which is returned.",
        "properties": {
          "sequence_number": {
            "type": "integer",
            "minimum": 1,
            "description": "Ordering key within the match."
          },
          "elapsed": {
            "type": "integer",
            "minimum": 0,
            "description": "Soccer minute from the live adapter."
          },
          "elapsed_extra": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "team": {
            "type": [
              "string",
              "null"
            ]
          },
          "player_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "player_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "headshot_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "assist_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "assist_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "assist_headshot_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "event_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "event_detail": {
            "type": [
              "string",
              "null"
            ]
          },
          "card_class": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "second_yellow",
              "straight_red",
              "yellow",
              null
            ],
            "description": "Football only. Whether a dismissal was a SECOND YELLOW or a DIRECT (straight) RED, derived from the player's card sequence in this match, because the upstream feed labels every card either `Yellow Card` or `Red Card` and states nothing more. `event_detail` is unchanged and still carries the upstream label verbatim. Definitions, applied consistently: `second_yellow` means the player has exactly two yellows and exactly one red in this match; `straight_red` means exactly one red and no yellow; `yellow` is any booking. `null` means WE WILL NOT SAY, and for a red card that is a deliberate verdict rather than missing data: it covers a red following exactly one booking (which cannot be told apart from a second yellow with a missing first booking), any player-match where VAR reviewed, upgraded or cancelled a card, more than one dismissal for one player, and events whose player cannot be identified. An unlabelled red is still returned in full."
          },
          "period": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "period_display": {
            "type": [
              "string",
              "null"
            ],
            "example": "2nd Inning"
          },
          "clock": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Clock"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null on feeds that carry no clock; 8 of 8 sampled baseball events were null."
          },
          "type": {
            "type": "string",
            "example": "Play Result"
          },
          "team_id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "related_player_id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "description": {
            "type": "string"
          },
          "scoring_play": {
            "type": "boolean"
          },
          "score_value": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Points/runs this event added."
          },
          "home_score": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Running home score AFTER this event."
          },
          "away_score": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Running away score AFTER this event."
          },
          "coordinate_x": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "description": "Soccer shot location: the SHOOTER's position on a 0-100 normalised pitch. `null` on non-shot events and on any event from a feed that carries no positions."
          },
          "coordinate_y": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ]
          },
          "xg": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "description": "Expected goals for a shot, 0-1. `null` means the source does not model xG for this event, never \"zero chance\"."
          },
          "xgot": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "description": "Expected goals on target. Present only for shots that were on target; `null` for misses and blocks by design."
          },
          "down": {
            "oneOf": [
              {
                "type": "integer",
                "minimum": 1,
                "maximum": 4
              },
              {
                "type": "null"
              }
            ],
            "description": "NFL down; null for plays without a down and for non-NFL events."
          },
          "distance": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "NFL yards to gain; null for non-NFL events."
          },
          "yards_to_endzone": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "NFL field position measured in yards from the opponent end zone."
          },
          "yards_gained": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "NFL yards gained on the play; null for non-NFL events."
          },
          "epa": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "description": "NFL expected points added; null when unavailable or on non-NFL events."
          },
          "win_probability": {
            "oneOf": [
              {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              },
              {
                "type": "null"
              }
            ],
            "description": "NFL possession-team win probability after the play."
          },
          "defending_team_id": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StoredId"
              },
              {
                "type": "null"
              }
            ],
            "description": "Canonical defending-team UUID on NFL rows; null otherwise."
          }
        }
      },
      "FixtureTeamStatistics": {
        "type": "object",
        "description": "Post-match team stat line for one team. All stat fields are\nnullable: a metric a league doesn't track is null, and `expected_goals`\n(xG) is present only on covered leagues. Counts are integers; percentages\nand xG are numbers.\n",
        "properties": {
          "team_id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "team_name": {
            "type": "string",
            "nullable": true
          },
          "shots_on_target": {
            "type": "integer",
            "nullable": true
          },
          "shots_off_target": {
            "type": "integer",
            "nullable": true
          },
          "total_shots": {
            "type": "integer",
            "nullable": true
          },
          "blocked_shots": {
            "type": "integer",
            "nullable": true
          },
          "shots_inside_box": {
            "type": "integer",
            "nullable": true
          },
          "shots_outside_box": {
            "type": "integer",
            "nullable": true
          },
          "fouls": {
            "type": "integer",
            "nullable": true
          },
          "corner_kicks": {
            "type": "integer",
            "nullable": true
          },
          "offsides": {
            "type": "integer",
            "nullable": true
          },
          "ball_possession": {
            "type": "number",
            "nullable": true,
            "description": "percent 0-100"
          },
          "yellow_cards": {
            "type": "integer",
            "nullable": true
          },
          "red_cards": {
            "type": "integer",
            "nullable": true
          },
          "goalkeeper_saves": {
            "type": "integer",
            "nullable": true
          },
          "total_passes": {
            "type": "integer",
            "nullable": true
          },
          "accurate_passes": {
            "type": "integer",
            "nullable": true
          },
          "pass_percentage": {
            "type": "number",
            "nullable": true,
            "description": "percent 0-100"
          },
          "expected_goals": {
            "type": "number",
            "nullable": true,
            "description": "xG",
            "covered leagues only": null
          }
        }
      },
      "StandingRow": {
        "type": "object",
        "description": "One team's row in a standings table. The field names below are the ones production returns, verified identical across all 9 leagues that serve a UUID-keyed table (laliga, seriea, bundesliga, ligue1, mls, nhl, nba, nfl, mlb) on 2026-09-18. Until then this schema declared `position`, `played`, `won`, `drawn`, `lost`, `goals_for`, `goals_against`, `goal_difference` and `points`, none of which has ever been returned.",
        "required": [
          "rank",
          "team_id",
          "team_name",
          "wins",
          "losses",
          "games_played"
        ],
        "properties": {
          "rank": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 200,
            "description": "1-indexed position in the table AS WE SERVE IT, computed by us at read time over exactly the rows in this response, so it is always contiguous 1..N with no gaps and never null while any row in the group carries an ordering metric. `rows` are returned in this order, so the array order and this field cannot disagree. ORDERING: by the competition's own metric. For association football (sport `football`, including MLS and the Champions League) that is league points, 3 per win and 1 per draw, then goal difference (points_for minus points_against), then goals for. For every other sport it is `win_pct` as the league itself defines it. For NHL that is points percentage, (2W+OTL)/(2*GP), not W/(W+L). It falls back to wins/(wins+losses+ties) when the source supplies no win_pct. TIEBREAK, identical for every sport and applied in this order: more wins, then fewer losses, then team_name ascending, then team_id ascending. team_id makes it a total order, so the same rows always produce the same table. Teams that are genuinely level therefore get ADJACENT DISTINCT positions rather than a shared one: `rank` is a position, not a ranking class. BEFORE 2026-09-24 this field was the ingest's running counter over the vendor's conference/division tree and was NOT a table position: measured over the served rows that day, 19 of 29 adjacent NBA pairs had the lower-ranked team with the better record, and NHL (32 rows) and NCAAF (138 rows) carried null on every row. It is null now only when no row in the group carries any ordering metric at all, in which case we decline to invent one."
          },
          "team_id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "team_name": {
            "type": "string"
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Treat as opaque; do not construct it."
          },
          "wins": {
            "type": "integer",
            "minimum": 0
          },
          "losses": {
            "type": "integer",
            "minimum": 0
          },
          "ties": {
            "type": "integer",
            "minimum": 0,
            "description": "Draws. Always present, and 0 for sports that cannot draw, so a 0 here does not distinguish \"no draws\" from \"draws impossible\"; branch on `sport` for that."
          },
          "win_pct": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "games_played": {
            "type": "integer",
            "minimum": 0
          },
          "points_for": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Goals/runs/points scored, named uniformly across sports. This is NOT competition points; no points column is served."
          },
          "points_against": {
            "type": [
              "integer",
              "null"
            ]
          },
          "streak": {
            "type": [
              "string",
              "null"
            ],
            "example": "W3"
          },
          "conference": {
            "type": [
              "string",
              "null"
            ],
            "example": "Big Ten Conference",
            "description": "The team's grouping inside the league, spelled as the league spells it. NULL wherever no grouping is recorded, which is most leagues; NULL means \"not recorded\", never \"independent\", which is itself a label (\"Division I Independents\"). Filter a table down to one grouping with `?conference=`."
          },
          "conference_wins": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Wins counted ONLY over matches where both teams carry the same non-null `conference`. NULL when `conference` is null."
          },
          "conference_losses": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "conference_rank": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "1-indexed position inside `conference`. NULL when `conference` is null; a rank inside no group is not a number."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Standings": {
        "type": "object",
        "description": "One league's table. `/v1/standings` returns these under `data.standings`.",
        "required": [
          "league_id",
          "league_name",
          "sport",
          "season",
          "rows"
        ],
        "properties": {
          "league_id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "league_name": {
            "type": "string",
            "example": "NHL"
          },
          "sport": {
            "$ref": "#/components/schemas/Sport"
          },
          "season": {
            "type": "string"
          },
          "rows": {
            "type": "array",
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/StandingRow"
            }
          }
        }
      },
      "MatchSide": {
        "type": "object",
        "description": "One side of a match. The field names are `id`/`name`; there is no `team_id`/`team_name` here. Until 2026-09-18 this schema said `team_id` + `team_name`, which no endpoint has ever returned.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "short_name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Served from our own asset endpoint; treat it as opaque and do not construct it."
          }
        }
      },
      "MatchScore": {
        "type": "object",
        "description": "Final or current goal/run/point totals. Null on a match with no score yet.",
        "required": [
          "home",
          "away"
        ],
        "properties": {
          "home": {
            "type": [
              "integer",
              "null"
            ]
          },
          "away": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Linescore": {
        "type": "object",
        "description": "Per-period totals. The array length and meaning follow the sport, and are not uniform even within one sport, so read it positionally only after checking `sport`.",
        "properties": {
          "home": {
            "type": "array",
            "items": {
              "type": [
                "integer",
                "null"
              ]
            }
          },
          "away": {
            "type": "array",
            "items": {
              "type": [
                "integer",
                "null"
              ]
            }
          }
        }
      },
      "Match": {
        "type": "object",
        "description": "The match object. This one shape is returned by `/v1/matches`, `/v1/stored/matches` and `/v1/cricket/matches` alike, for every sport (verified field-for-field across all three on 2026-09-18).",
        "required": [
          "id",
          "sport",
          "league",
          "kickoff_utc",
          "status",
          "home",
          "away"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/StoredId"
          },
          "sport": {
            "$ref": "#/components/schemas/Sport"
          },
          "league": {
            "type": "string",
            "description": "The league's DISPLAY name as served here (e.g. \"MLB\", \"NHL\"). This is not the `?league=` code; see LeagueCode for that.",
            "example": "MLB"
          },
          "kickoff_utc": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "$ref": "#/components/schemas/MatchStatus"
          },
          "home": {
            "$ref": "#/components/schemas/MatchSide"
          },
          "away": {
            "$ref": "#/components/schemas/MatchSide"
          },
          "score": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/MatchScore"
              },
              {
                "type": "null"
              }
            ]
          },
          "linescore": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Linescore"
              },
              {
                "type": "null"
              }
            ]
          },
          "attendance": {
            "type": [
              "integer",
              "null"
            ]
          },
          "broadcast": {
            "type": [
              "string",
              "null"
            ]
          },
          "has_odds": {
            "type": "boolean",
            "description": "Whether stored odds exist for this match; see /v1/matches/{id}/odds."
          },
          "round": {
            "type": [
              "string",
              "null"
            ],
            "description": "Competition segment as the source names it, e.g. \"Regular Season - 2\" or \"Group Stage\". This is the axis to group gameweek analytics by: a postponed fixture keeps its round while its kickoff_utc moves by weeks, so bucketing by date splits one gameweek and merges another. Free text rather than an integer, because the phase carries meaning an integer would discard. Null where the source supplies none, currently populated for soccer only.",
            "example": "Regular Season - 2"
          },
          "season": {
            "type": "string",
            "description": "NOT EMITTED by the list endpoints as measured on 2026-09-18. Declared because callers may hold it from older payloads; branch on its absence rather than expecting it."
          },
          "venue": {
            "type": "string",
            "description": "NOT EMITTED by the list endpoints as measured on 2026-09-18."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "NOT EMITTED by the list endpoints as measured on 2026-09-18."
          }
        }
      },
      "SessionResult": {
        "type": "object",
        "required": [
          "position",
          "driver_id",
          "dnf"
        ],
        "properties": {
          "position": {
            "type": "integer",
            "minimum": 1,
            "maximum": 40,
            "description": "1-indexed finishing position."
          },
          "driver_id": {
            "$ref": "#/components/schemas/CanonicalId"
          },
          "team_id": {
            "$ref": "#/components/schemas/CanonicalId"
          },
          "driver_name": {
            "type": "string",
            "description": "Driver full name",
            "joined from the players table when available.": null
          },
          "team_name": {
            "type": "string",
            "description": "Constructor name",
            "joined from the teams table when available.": null
          },
          "best_lap_seconds": {
            "type": "number",
            "minimum": 30,
            "maximum": 600
          },
          "gap_to_leader_seconds": {
            "type": "number",
            "minimum": 0,
            "maximum": 7200
          },
          "laps_completed": {
            "type": "integer",
            "minimum": 0,
            "maximum": 200
          },
          "dnf": {
            "type": "boolean",
            "description": "True when the driver did not finish (mechanical",
            "accident": null,
            "DSQ).": null
          }
        }
      },
      "Session": {
        "type": "object",
        "required": [
          "id",
          "sport",
          "session_type",
          "league_id",
          "circuit_id",
          "start_time",
          "status",
          "results",
          "updated_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/CanonicalId"
          },
          "sport": {
            "type": "string",
            "enum": [
              "formula1"
            ]
          },
          "session_type": {
            "$ref": "#/components/schemas/SessionType"
          },
          "league_id": {
            "$ref": "#/components/schemas/CanonicalId",
            "description": "GP weekend / round identifier."
          },
          "season": {
            "type": "string",
            "pattern": "^\\d{4}(-\\d{2,4})?$"
          },
          "circuit_id": {
            "$ref": "#/components/schemas/CanonicalId"
          },
          "start_time": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "$ref": "#/components/schemas/MatchStatus"
          },
          "results": {
            "type": "array",
            "maxItems": 40,
            "description": "Position ranking. Empty until the results worker has processed the session.",
            "items": {
              "$ref": "#/components/schemas/SessionResult"
            }
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Injury": {
        "$ref": "#/components/schemas/Player"
      },
      "OddsRow": {
        "$ref": "#/components/schemas/Odds"
      },
      "EventRow": {
        "$ref": "#/components/schemas/Event"
      },
      "Standing": {
        "$ref": "#/components/schemas/Standings"
      },
      "WebhookRegistration": {
        "type": "object",
        "required": [
          "url",
          "events"
        ],
        "properties": {
          "id": {
            "type": "string",
            "readOnly": true
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "score_update",
                "match_start",
                "match_end"
              ]
            }
          },
          "secret": {
            "type": "string",
            "readOnly": true,
            "description": "HMAC-SHA256 secret. Shown once at creation."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          },
          "enabled": {
            "type": "boolean",
            "readOnly": true
          },
          "consecutiveFailures": {
            "type": "integer",
            "minimum": 0,
            "readOnly": true
          },
          "parkedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "readOnly": true
          },
          "parkedReason": {
            "type": [
              "string",
              "null"
            ],
            "readOnly": true
          }
        }
      },
      "LiveScoreEvent": {
        "type": "object",
        "description": "Payload of a `score_update` / `match_start` / `match_end` frame on the WebSocket feed at wss://api.bigballsdata.com/live (Pro and above).\n\nNOT reachable over HTTP: no path in this document returns it. It is declared here because this file is the canonical contract and the served /openapi.json is what agents and SDK generators read; a payload customers integrate against being absent from it is the same undocumented-surface problem whether or not the transport is REST.\n\nTIMING FIELDS. `polled_at` and `emitted_at` are the two instants we can observe first-hand, and they exist so a latency-sensitive consumer can measure our segment instead of estimating it. Both are ISO-8601 with an explicit `Z` (UTC, millisecond precision), never a local offset.\n\n\n  * `emitted_at - polled_at`: our internal segment, exactly.\n  * `now - emitted_at`: publish, gateway relay and your own\n                               transport, measured against your clock.\n  * anything before `polled_at`: upstream. We do NOT decompose this\n    and we do not publish an estimate of it; treat it as an upper\n    bound. Our upstream is a POLL, not a push, so this segment includes\n    both the upstream's own refresh cadence and our poll gap. See\n    docs/live-latency-2026-09-06.md for both, measured.\n\n\nNo upstream-provided timestamp appears in this payload, deliberately: every time value here is a reading of our own clock.",
        "required": [
          "match_id",
          "sport",
          "league_id",
          "status",
          "period",
          "clock",
          "linescore",
          "polled_at",
          "emitted_at"
        ],
        "properties": {
          "match_id": {
            "type": "string",
            "format": "uuid"
          },
          "sport": {
            "type": "string",
            "example": "football"
          },
          "league_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "scheduled",
              "live",
              "finished"
            ],
            "example": "live"
          },
          "period": {
            "type": "integer",
            "nullable": true,
            "example": 2
          },
          "clock": {
            "type": "string",
            "nullable": true,
            "example": "83'"
          },
          "linescore": {
            "type": "object",
            "nullable": true,
            "description": "Per-period scores. Null for a row with no linescore yet.",
            "example": {
              "home": [
                4
              ],
              "away": [
                2
              ]
            }
          },
          "polled_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 UTC (`Z`). The instant the upstream HTTP response was RECEIVED (response resolved, body not yet read). Reading and parsing that body, reconciling the match and writing the row all fall AFTER this instant, i.e. inside our segment. Stamped per upstream request, not per poll cycle.",
            "example": "2026-09-06T18:30:00.000Z"
          },
          "emitted_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 UTC (`Z`). The instant we published this frame to our internal bus, immediately after building it. Excludes gateway relay and socket delivery, which is why `now - emitted_at` on your side bounds them.",
            "example": "2026-09-06T18:30:02.400Z"
          },
          "down": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "maximum": 4,
            "example": 1,
            "description": "Current down. `american_football` only; null for every other sport, and null for a football match that is not currently live. Absence, not zero."
          },
          "distance": {
            "type": "integer",
            "nullable": true,
            "example": 10,
            "description": "Yards to go for a first down. Paired with `down`."
          },
          "yard_line": {
            "type": "integer",
            "nullable": true,
            "minimum": 0,
            "maximum": 100,
            "example": 51,
            "description": "Distance to the opponent's goal line, 0-100."
          },
          "possession_team_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The team currently in possession, as OUR team id, never a raw upstream id."
          }
        }
      },
      "LiveOddsMoveEvent": {
        "type": "object",
        "description": "Payload of an `odds_move` frame on the WebSocket feed at wss://api.bigballsdata.com/live (Pro and above). Emitted after a changed main line has been persisted to both the append-only odds history and the current-state odds table. Not reachable over HTTP; use the REST odds route on Edge+ to read or reconcile current state.\n\n`sportsbook` is the public brand resolved through `books_reference`, never a vendor-prefixed sportsbook id. No source name, affiliate id, upstream event id or vendor timestamp is included. `observed_at` and `emitted_at` are readings of our own clock.",
        "required": [
          "match_id",
          "sport",
          "league_id",
          "sportsbook",
          "market",
          "participant",
          "value",
          "price",
          "observed_at",
          "emitted_at"
        ],
        "properties": {
          "match_id": {
            "type": "string",
            "format": "uuid"
          },
          "sport": {
            "type": "string",
            "example": "basketball"
          },
          "league_id": {
            "type": "string",
            "format": "uuid"
          },
          "sportsbook": {
            "type": "string",
            "example": "BetMGM"
          },
          "market": {
            "type": "string",
            "example": "moneyline"
          },
          "participant": {
            "type": "string",
            "example": "Minnesota Lynx"
          },
          "value": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "example": null,
            "description": "Line value; null for a moneyline."
          },
          "price": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ],
            "example": -135
          },
          "observed_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-05T18:00:00.000Z",
            "description": "Our clock when the upstream frame reached the worker."
          },
          "emitted_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-05T18:00:00.125Z",
            "description": "Our clock immediately before publication to the internal bus."
          }
        }
      },
      "LivePlayerStatsEvent": {
        "type": "object",
        "description": "Payload of a `player_stats_update` frame on the WebSocket feed at wss://api.bigballsdata.com/live (Pro and above). One frame per match per poll tick, carrying only the stat cells that CHANGED on that tick.\n\nNOT reachable over HTTP: no path in this document returns it. The closest REST equivalent is `GET /v1/live-stats/{sport}/{matchId}/players`, which returns the full current box score on demand; this frame is the same data pushed as it moves.\n\nCOVERAGE, stated rather than implied, because the four sports are NOT equivalent. Frames are published for `basketball`, `baseball`, `ice_hockey` and `american_football`. Only baseball has broad demonstrated coverage (1,290 matches carry live per-player stats as of 2026-09-07); basketball (8 matches) and ice_hockey (9) were largely out of season while the writer matured, so treat their live coverage as unproven rather than absent. `american_football` is newer still: its publisher ships BEFORE its first game, so per-player coverage begins at the 2026 season opener (2026-09-10 00:20 UTC) and nothing has been measured in play before then; subscribing today is valid and correctly silent. Subscribe to a `sport:` room to receive the `heartbeat`, which is how you tell \"quiet\" from \"broken\".\n\nSNAPSHOT vs DELTA. `snapshot: true` means the frame carries every cell we hold for that match, sent for the first frame of a match and after a publisher restart. `snapshot: false` means it carries only what changed, so an ABSENT field means \"unchanged\", not \"no data\". Keep the last snapshot and apply deltas onto it.\n\nSTATS ARE AN ARRAY, NOT AN OBJECT. `{field, value, display}` per cell rather than `{points: 20}`. `value` is null for composite readings such as `\"6-12\"` (made-attempted) or a clock, which are carried in `display`; a consumer that needs a number must read `value` and handle the null rather than parsing `display`.\n\n`player_id` and `team_id` are Big Balls UUIDs, not the ids of any upstream. `polled_at` / `emitted_at` follow the identical definition and boundaries as `LiveScoreEvent`; see that schema.",
        "required": [
          "match_id",
          "sport",
          "league_id",
          "snapshot",
          "player_count",
          "stat_count",
          "players",
          "polled_at",
          "emitted_at"
        ],
        "properties": {
          "match_id": {
            "type": "string",
            "format": "uuid"
          },
          "sport": {
            "type": "string",
            "enum": [
              "basketball",
              "baseball",
              "ice_hockey"
            ],
            "example": "baseball"
          },
          "league_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "Null when the match has no league. The frame is still routed to its match room."
          },
          "snapshot": {
            "type": "boolean",
            "description": "True when `players` is the complete held state rather than a delta.",
            "example": false
          },
          "player_count": {
            "type": "integer",
            "minimum": 1,
            "description": "Players in this frame. Never 0: a frame is not published when nothing changed, so its arrival is itself the change signal.",
            "example": 1
          },
          "stat_count": {
            "type": "integer",
            "minimum": 1,
            "description": "Stat cells across all players in this frame.",
            "example": 3
          },
          "players": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "player_id",
                "name",
                "team_id",
                "stats"
              ],
              "properties": {
                "player_id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string",
                  "nullable": true,
                  "example": "Shohei Ohtani"
                },
                "team_id": {
                  "type": "string",
                  "format": "uuid",
                  "nullable": true,
                  "description": "Null when the team is not bridged to a Big Balls id yet."
                },
                "stats": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "field",
                      "value",
                      "display"
                    ],
                    "properties": {
                      "field": {
                        "type": "string",
                        "description": "Canonical stat name. A stat we have no canonical name for yet is carried under its lowercased upstream spelling rather than dropped.",
                        "example": "hits"
                      },
                      "value": {
                        "type": "number",
                        "nullable": true,
                        "description": "Numeric reading, or null for a composite; read `display` then.",
                        "example": 2
                      },
                      "display": {
                        "type": "string",
                        "description": "The cell as it reads in a box score, composites included.",
                        "example": "2"
                      }
                    }
                  }
                }
              }
            }
          },
          "polled_at": {
            "type": "string",
            "format": "date-time",
            "description": "See `LiveScoreEvent.polled_at` (identical definition and boundary).",
            "example": "2026-09-07T18:30:00.000Z"
          },
          "emitted_at": {
            "type": "string",
            "format": "date-time",
            "description": "See `LiveScoreEvent.emitted_at` (identical definition and boundary).",
            "example": "2026-09-07T18:30:01.250Z"
          }
        }
      }
    }
  },
  "security": [
    {
      "apiKey": []
    },
    {
      "bearer": []
    }
  ],
  "paths": {
    "/v1/sports": {
      "get": {
        "tags": [
          "sports"
        ],
        "summary": "List supported sports",
        "description": "Public: no API key required. Rate-limited by IP address instead of\nby key (see docs/rate-limits) so this stays keyless without becoming\nan abuse vector.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SportInfo"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/leagues": {
      "get": {
        "tags": [
          "sports"
        ],
        "summary": "List leagues, optionally filtered by sport + country",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "country",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Envelope"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/leagues/{id}/top-scorers": {
      "get": {
        "tags": [
          "sports"
        ],
        "summary": "Domestic-league top scorers (season aggregates)",
        "description": "Per-league leaderboard sourced from our soccer_player_season_stats\n(season aggregates), summing a player across their team\nrows in the season. Flat data[] shape: rank, id, player_name, team,\ngoals, assists, minutes, matches (matches = season appearances). `id` is\nthe BigBalls player id (uuid), stable across seasons and joinable to\n/v1/players/{id}, so a consumer never has to match on player name. It\nis null for a leader we have not yet bridged to a player row (19 of the\ncurrent La Liga top 20 resolve, measured 2026-09-14). `headshot_url`\nis a first-party asset URL on eligible plans and null otherwise.\nSupports a\n?category sort and excludes players with a zero in the sorted category.\nCovers the big-five leagues. When `season` is omitted, this endpoint\nand `/v1/leagues/{id}/xg-leaders` select the newest season present in\nboth datasets, so their defaults remain comparable. 5-minute Redis\ncache keyed by league+season+category+limit.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "epl",
                "laliga",
                "serie-a",
                "bundesliga",
                "ligue-1"
              ]
            },
            "description": "League code (epl, laliga, serie-a, bundesliga, ligue-1) or its raw numeric league id (39, 140, 135, 78, 61)."
          },
          {
            "in": "query",
            "name": "category",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "goals",
                "assists",
                "minutes",
                "matches"
              ],
              "default": "goals"
            },
            "description": "Sort metric. Defaults to goals."
          },
          {
            "in": "query",
            "name": "season",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Season label: the year the campaign starts. If omitted, uses the newest season also present on the sibling xG leaderboard. Pass explicitly when reproducing a historical result."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Number of leaders to return."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "unknown league code"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/leagues/{id}/xg-leaders": {
      "get": {
        "tags": [
          "sports"
        ],
        "summary": "Domestic-league expected-goals leaderboard (season aggregates)",
        "description": "Per-league season xG leaderboard for the big-five European leagues,\n2014 through the current season. Ranks players by expected goals and\nexposes the surrounding shot-quality picture: xG, xA, npxG, xGChain,\nxGBuildup, plus goals, assists, shots, key passes, matches and minutes.\n`goals_above_xg` (goals minus xG) is derived for finishing over- and\nunder-performance.\n\n**Solo plan or higher**: every field this endpoint ranks on is a model\nestimate rather than a record of what happened, so it sits with the rest\nof the model layer. Free receives a 403 naming the tier and linking the\ndocs and pricing pages. The factual view of the same leagues, goals and\nassists, stays free on `GET /v1/leagues/{id}/top-scorers`.\n\nLEAGUE-LEVEL ONLY. Rows are keyed by player NAME, not by a BigBalls\n`player_id`; the xG-to-players entity bridge is not built yet,\nso a per-player or per-match xG surface would serve a mostly-empty\njoin. Those endpoints are not offered until that bridge lands; see\ndocs/scoping/soccer-xg-bridge.md. Season rows we cannot attribute to a\nleague are excluded, so this leaderboard only ever reflects players in\nthe league you asked for.\n\n`team` is the source team title verbatim and may be a comma-separated\nlist for a player who transferred mid-season. Refreshed daily;\nwhen `season` is omitted, this endpoint and the sibling top-scorers\nendpoint use the newest season present in both datasets. 5-minute\nRedis cache keyed by league+season+stat+min_minutes+limit.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "epl",
                "laliga",
                "serie-a",
                "bundesliga",
                "ligue-1"
              ]
            },
            "description": "League code. Same vocabulary as /v1/leagues/{id}/top-scorers."
          },
          {
            "in": "query",
            "name": "stat",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "xg",
                "xa",
                "npxg",
                "goals",
                "assists",
                "shots",
                "key_passes"
              ],
              "default": "xg"
            },
            "description": "Sort metric. Defaults to xg."
          },
          {
            "in": "query",
            "name": "season",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 2014,
              "maximum": 2099
            },
            "description": "Season label: the year the campaign starts. If omitted, uses the newest season also present on the sibling top-scorers leaderboard. Pass explicitly when reproducing a historical result."
          },
          {
            "in": "query",
            "name": "min_minutes",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 6000,
              "default": 0
            },
            "description": "Minimum minutes played. Defaults to 0: no player is silently dropped."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Number of leaders to return."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "league",
                        "season",
                        "stat",
                        "min_minutes",
                        "leaders"
                      ],
                      "properties": {
                        "league": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "description": "The league code you requested."
                            },
                            "name": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "season": {
                          "type": "integer"
                        },
                        "stat": {
                          "type": "string"
                        },
                        "min_minutes": {
                          "type": "integer"
                        },
                        "leaders": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "rank": {
                                "type": "integer"
                              },
                              "player_name": {
                                "type": "string"
                              },
                              "team": {
                                "type": "string",
                                "nullable": true,
                                "description": "Source team title; a comma-list on a mid-season transfer."
                              },
                              "position": {
                                "type": "string",
                                "nullable": true
                              },
                              "matches": {
                                "type": "integer",
                                "nullable": true
                              },
                              "minutes": {
                                "type": "integer",
                                "nullable": true
                              },
                              "goals": {
                                "type": "integer",
                                "nullable": true
                              },
                              "assists": {
                                "type": "integer",
                                "nullable": true
                              },
                              "shots": {
                                "type": "integer",
                                "nullable": true
                              },
                              "key_passes": {
                                "type": "integer",
                                "nullable": true
                              },
                              "xg": {
                                "type": "number",
                                "nullable": true
                              },
                              "xa": {
                                "type": "number",
                                "nullable": true
                              },
                              "npxg": {
                                "type": "number",
                                "nullable": true
                              },
                              "xg_chain": {
                                "type": "number",
                                "nullable": true
                              },
                              "xg_buildup": {
                                "type": "number",
                                "nullable": true
                              },
                              "goals_above_xg": {
                                "type": "number",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter. Carries a suggested_fix naming the legal values.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "League has no xG coverage. The error names the leagues that do.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "solo",
        "x-bbs-capability": "expected goals",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/matches": {
      "get": {
        "tags": [
          "matches"
        ],
        "summary": "List matches with filters",
        "description": "Filter the scheduled and recent match list. Both ?sport= and ?league= are optional: a bare /v1/matches returns upcoming matches across all sports. `data` is ALWAYS an array of match objects, for every sport and every league. `?league=nba` is the one NBA history surface and reaches all 80 archive seasons plus canonical match-only rows. Its shape is an additive superset: archive identity/statistics and `time_precision` are added. Where only a date is genuinely known, `kickoff_utc` is that date at midnight UTC and `time_precision=date`; the time is not a tip-off. For the live per-adapter scores feed (integer scores keyed by match_id) use /v1/scores with the same parameters. ?date accepts an ISO date (YYYY-MM-DD) or the literal 'today'.",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "$ref": "#/components/schemas/LeagueCode"
            },
            "description": "Required for multi-league sports (e.g. football). League code such as epl, laliga, bundesliga, serie_a, ligue1, cl, mls.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string"
            },
            "description": "ISO date (YYYY-MM-DD) or the literal 'today' (the current UTC date). Selects a UTC calendar day: kickoffs at or after 00:00Z on that date and before 00:00Z the next day. An evening game in the Americas often kicks off after 00:00Z and is listed under the NEXT date. Responses served from stored matches echo the exact window applied in meta.date_window."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. America/New_York). ?date= and ?date=today then select the local calendar day in that zone, bounded by local midnights converted to UTC (23 or 25 hours on a clock-change day). Offsets such as -05:00 are rejected with 400. Omit for UTC. Example: ?league=nfl&date=2026-09-13&tz=America/New_York."
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "$ref": "#/components/schemas/MatchStatus"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1946,
              "maximum": 2200
            },
            "description": "Season start year. With ?league=nba, 2025 means the 2025-26 campaign."
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 64
            },
            "description": "NBA only: canonical team UUID, archive team ID, or abbreviation."
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string",
              "enum": [
                "REG",
                "POST",
                "PRE",
                "ALLSTAR",
                "Regular Season",
                "Playoffs",
                "Pre Season",
                "All Star"
              ]
            },
            "description": "NBA only: season segment."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "oneOf": [
                              {
                                "$ref": "#/components/schemas/Match"
                              },
                              {
                                "$ref": "#/components/schemas/UnifiedNbaMatch"
                              }
                            ]
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters: an invalid ?date (not 'today' or YYYY-MM-DD), ?limit > 200, or an unknown sport/league value (e.g. ?sport=soccer, it's 'football'). A bare multi-league ?sport= is NOT a 400: it returns 200 matches across that sport's leagues. The error body includes validation_errors and a working example_url.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/scores": {
      "get": {
        "tags": [
          "matches"
        ],
        "summary": "Live scores for one sport or league",
        "description": "The live per-adapter scores feed. Unlike /v1/matches (which returns an ARRAY of match objects with team identities, league and kickoff), this returns the raw live-scores field envelope, in which `home` and `away` are integer SCORES and the match is keyed as `match_id`. Requires ?sport= or ?league=: a live fetch dispatches on an adapter, and with no sport there is nothing to dispatch on. This payload was previously reachable only through /v1/matches for single-league sports (baseball, boxing) as a side effect of a performance guard; callers of that shape should move here, and the URL is the only change required.",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "$ref": "#/components/schemas/LeagueCode"
            },
            "description": "League code such as epl, laliga, bundesliga, serie_a, ligue1, cl, mls.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string"
            },
            "description": "ISO date (YYYY-MM-DD) or the literal 'today' (the current UTC date). Selects a UTC calendar day: kickoffs at or after 00:00Z on that date and before 00:00Z the next day. An evening game in the Americas often kicks off after 00:00Z and is listed under the NEXT date. Responses served from stored matches echo the exact window applied in meta.date_window."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. America/New_York). ?date= then selects the local calendar day in that zone. The live feed buckets by UTC day, so with ?tz= the response is served from stored matches and meta.date_window shows the window. Offsets such as -05:00 are rejected with 400. Example: ?league=nfl&date=2026-09-13&tz=America/New_York."
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "$ref": "#/components/schemas/MatchStatus"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer"
            },
            "description": "Explicit season, labelled by the year it STARTS. Omit for the live window; an explicit past season is subject to the plan's history depth.\nWHICH SEASON COUNTS AS \"CURRENT\" IS MEASURED, NOT A CALENDAR DATE (changed 2026-09-25). The depth a plan allows is counted back from the newest season we actually hold played matches for, not from a fixed rollover month. So while a new season is under way but has no results yet; the NHL's 2026-27 opens 2026-09-29, the NBA's 2026-10-03; the previous season stays inside the window, and the window narrows only once the new season has data to narrow to. This only ever widens what a plan reaches; it never moves the boundary earlier than the calendar rule it replaced.\n`?season=` and `?date=` on this endpoint now resolve that boundary through one shared rule. Before 2026-09-25 they disagreed for the NBA and the NHL: `?season=` counted from the bare calendar year and `?date=` from the per-sport boundary, so `?sport=basketball&season=2025` returned 403 while `?sport=basketball&date=2025-12-01` returned 200 on the same key. Both are served now."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The live-scores field envelope. `data.scores` is null when no adapter covers the sport/league, in which case meta.coverage is false: a 200 that makes no claim about the data, rather than an error about the request.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "scores": {
                              "description": "A FieldResult whose `value` is the live score array. Null when no adapter covers the sport/league, in which case meta.coverage is false.",
                              "oneOf": [
                                {
                                  "allOf": [
                                    {
                                      "$ref": "#/components/schemas/FieldResult"
                                    },
                                    {
                                      "type": "object",
                                      "properties": {
                                        "value": {
                                          "type": "array",
                                          "items": {
                                            "$ref": "#/components/schemas/Score"
                                          }
                                        }
                                      }
                                    }
                                  ]
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or unknown sport/league. Unlike /v1/matches there is no bare all-sports fallback here, because a live fetch has no adapter to dispatch on without a sport.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/matches/{id}": {
      "get": {
        "tags": [
          "matches"
        ],
        "summary": "Single match",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "in": "query",
            "name": "fields",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated: scores,odds,lineups,stats,events,player_market_value. ACCEPTED BUT DOES NOT CHANGE THE RESPONSE. Measured 2026-09-18 across 4 field combinations and 2 sports: the returned key set was byte-for-byte identical with the parameter absent, with one field and with all five. Do not build a projection on it. Tracked in issue #2761."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data` is a single Match object, the same shape `/v1/matches` returns in its array. Until 2026-09-18 this declared a FieldKey-keyed map of FieldResult envelopes, which this route has never returned.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Match"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/matches/{id}/odds": {
      "get": {
        "tags": [
          "matches"
        ],
        "summary": "Odds for a match (aggregated across bookmakers)",
        "description": "Latest odds snapshot for one match, aggregated across bookmakers (live\nadapter tier, falling back to the stored tier). **Edge plan or higher**:\nodds access starts at Edge; Free / Solo / Trio receive a 403 naming the\ntier and linking the docs and pricing pages. The same lines' history and\nCLV live at /v1/odds/historical (also Edge+).\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string"
            },
            "description": "Optional and NON-FILTERING. The match id alone identifies the match, so this only resolves the plan gate, the same role it has on /v1/teams/{id}. A value that disagrees with the match's real sport does not change the response: measured 2026-09-20, ?sport=football on an ice_hockey match id returned the ice_hockey match, byte-identical to ?sport=ice_hockey and to no sport at all."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "oneOf": [
                            {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/Odds"
                              }
                            },
                            {
                              "type": "object",
                              "required": [
                                "odds"
                              ],
                              "properties": {
                                "odds": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "required": [
                                      "market",
                                      "participant",
                                      "decimal_odds",
                                      "book_count"
                                    ],
                                    "properties": {
                                      "market": {
                                        "type": "string"
                                      },
                                      "participant": {
                                        "type": "string"
                                      },
                                      "decimal_odds": {
                                        "type": "number"
                                      },
                                      "line": {
                                        "type": [
                                          "number",
                                          "null"
                                        ]
                                      },
                                      "updated_at": {
                                        "type": "string",
                                        "format": "date-time"
                                      },
                                      "book_count": {
                                        "type": "integer",
                                        "minimum": 0
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "odds",
        "x-bbs-not-available-for-sports": [
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/matches/{id}/events": {
      "get": {
        "tags": [
          "matches"
        ],
        "summary": "Play-by-play / match events",
        "description": "Scoring events are included on every plan through the default view or\n`?type=goals`. The full soccer minute-by-minute stream (`?type=all`,\nincluding cards, substitutions, shots and VAR) requires Pro, matching\nthe NBA, NHL and NFL play-by-play floor. Where stored soccer event coverage answers\nthe request, `?type=goals` excludes penalties that were missed or saved\ninstead of mislabelling them as scored goals. Those events, and\nVAR-disallowed goals, stay visible with their minute and player under\nthe Pro `?type=all` view. In the measured stored-event corpus, this\nimproves agreement with the free `score` / `linescore` fields from\n22,482 of 22,755 matches (98.80%) to 22,685 of 22,755 (99.69%); 70 of\n22,755 matches still require separate reconciliation.\nFor NBA, NHL and NFL matches, `?type=all` returns the full play log,\nwhich is the same product as each league's dedicated plays route and\nlikewise requires Pro. Lower tiers receive a 403 naming the tier.\nThe match's sport must also be in your plan.\n\nOn `?type=all` only, NFL rows are projected from the stored NFL play\nlog through the match's canonical UUID. They include down, distance,\nfield position, yards gained, EPA and win probability when the feed\nsupplies them. `player_id` remains null on these rows: the upstream\nplayer identifiers do not yet map one-to-one onto canonical player\nUUIDs, so the API does not guess. The default scope and `?type=goals`\nreturn the scoring summary without those play-level fields, which is\nwhat keeps them available below Pro.\n\n**Soccer shot coordinates and xG.** Where we hold shot-level data for a\nmatch, `?type=all` also returns shot events carrying `coordinate_x` /\n`coordinate_y` (the shooter's position, 0-100 normalised), `xg` and\n`xgot`. Coverage is narrow and documented rather than implied: these\nfields are populated for FIFA World Cup 2026 matches only, and are\n`null` everywhere else. They are absent, not zero, when unknown. The\nsame rows are also served by\n`GET /v1/stored/matches/{id}/plays`.\n\n**`data` is ALWAYS an array, including when we hold nothing.** Until\n2026-09-20 the live-router fallback arm broke that promise: a match we\ndo not cover returned `data: {\"events\": null}`, and a match served from\nthe live fan-out returned `data: {\"events\": {\"value\": [...]}}`, so\n`data.map(...)` threw on two of the three success bodies this one path\ncould produce. Both now return the array, and the no-coverage signal\nmoved into `meta` rather than being dropped:\n\n- `meta.coverage: false` plus a human-readable `meta.message` mean\n  **we hold no events for this match**. These are the same keys\n  `GET /v1/nfl/games/{gameId}/plays` uses for its empty page.\n- `meta.coverage` absent with `data: []` means the match IS covered and\n  the feed reports no events (for example `?type=goals` on a goalless\n  match).\n- `meta.count` is the length of `data`, derived from the rows sent.\n\nAn empty array and an uncovered match are different answers, so read\n`meta.coverage` before concluding a match has no events.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string"
            },
            "description": "Optional and NON-FILTERING. The match id alone identifies the match, so this only resolves the plan gate, the same role it has on /v1/teams/{id}. A value that disagrees with the match's real sport does not change the response: measured 2026-09-20, ?sport=football on an ice_hockey match id returned the ice_hockey match, byte-identical to ?sport=ice_hockey and to no sport at all."
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string",
              "enum": [
                "goals",
                "all"
              ],
              "default": "goals"
            },
            "description": "`goals` is the Free scoring-event summary. `all` is the Pro minute-by-minute/full-play-log product."
          }
        ],
        "responses": {
          "200": {
            "x-bbs-field-entitlements": [
              {
                "path": "data[].headshot_url",
                "min_plan": "solo",
                "below_floor": "PRESENT-NULL"
              }
            ],
            "description": "ok. `data` is an array on every 200, empty when we hold no events; see `meta.coverage` / `meta.message` for why it is empty.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Event"
                          }
                        },
                        "meta": {
                          "type": "object",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "minimum": 0,
                              "description": "Length of `data`."
                            },
                            "coverage": {
                              "type": "boolean",
                              "description": "Present and `false` only when we hold no events for this match. Absent means the match is covered."
                            },
                            "message": {
                              "type": "string",
                              "description": "Human-readable reason, present with `coverage: false`."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Pro or higher for `type=all`)"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-min-plan-by-sport": {
          "basketball": "pro",
          "ice_hockey": "pro",
          "american_football": "pro",
          "football": "pro",
          "baseball": "pro"
        },
        "x-bbs-capability": "play-by-play",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "ice_hockey",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/matches/{id}/statistics": {
      "get": {
        "tags": [
          "matches"
        ],
        "summary": "Per-team post-match team statistics (soccer)",
        "description": "Per-team team-stat line (possession, shots breakdown, fouls, corners,\noffsides, cards, GK saves, passes, pass %, and xG where covered) from\nthe upstream feed, persisted hourly into `fixture_statistics`. Soccer-only;\nthe linescore stays authoritative for goals.\n**Free plan or higher**: the box score is a raw record of the match, so\nevery factual field is free. One COMPUTED field is not: `expected_goals`\nis omitted from both sides below the Solo plan. It is absent, never\nnull, so an omission is never mistaken for \"no xG on record\".\nStored-only: never fetches upstream. When a real match has no stats yet\n(the upstream posts within ~30 min of full-time, covered leagues only) the\nresponse is `200` with `meta.available=false` and null sides.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "x-bbs-field-entitlements": [
              {
                "path": "data.home.expected_goals",
                "min_plan": "solo",
                "below_floor": "ABSENT"
              },
              {
                "path": "data.away.expected_goals",
                "min_plan": "solo",
                "below_floor": "ABSENT"
              }
            ],
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "home": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/FixtureTeamStatistics"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "away": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/FixtureTeamStatistics"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            }
                          }
                        },
                        "meta": {
                          "type": "object",
                          "properties": {
                            "expected_goals_source": {
                              "type": "string",
                              "enum": [
                                "stored"
                              ],
                              "description": "Present only when `expected_goals` for one or both sides was served from our own stored xG because the box-score feed carried the rest of the line but not xG. Measured in production 2026-09-30: 17,115 team-sides in `fixture_statistics` hold a null `expected_goals` while our stored xG covers that same match and side, concentrated in the 2015-2022 seasons of EPL, La Liga and Serie A. The live 2025/26 big-five season is NOT among them (4 of 1,756 finished matches carry a null side and none of those four is fillable), so this fills history, not today. A value the box-score feed itself carried is never replaced."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid match id"
          },
          "404": {
            "description": "no match found with that id"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/teams/{id}": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Team profile + stored record",
        "description": "The team as `GET /v1/teams` lists it: `id`, `name`, `short_name`,\n`logo_url`, `flag_url`, `sport`, `league`, plus a `stats` object\ncarrying the same aggregate `GET /v1/teams/{id}/stats` returns. One\nimplementation serves both, so the two cannot disagree.\n\n**`stats` covers every finished match we hold for the team, not the\ncurrent season.** `meta.stats_scope` says so on each response. The\nsummary read \"current season stats\" until 2026-09-08; it was never\ntrue of the numbers this route or its `/stats` sibling produced.\n\nA well-formed uuid naming no team is a `404`, with or without\n`?sport=`.\n\n**`?season=` does not narrow this route** and never did. It used to be\nforwarded to a live lookup that returned nothing. Passing it now returns\n`meta.ignored_params: [\"season\"]` rather than silently answering a\nnarrower question with a wider number. For season-scoped figures use\n`/v1/teams/{id}/season` or `/v1/teams/{id}/matches?season=`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            },
            "description": "Optional. Inferred from the team when absent. Only affects the plan gate."
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "string"
            },
            "description": "Accepted but IGNORED. The response discloses it in meta.ignored_params."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/TeamDetail"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "no team with that id"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player profile + career stats",
        "description": "A field-router route, the same family as `/v1/athletes/{id}`: `data` is a map keyed by FieldKey and each value is a FieldResult, or null when that field could not be satisfied (still a 200, with the key named in `meta.fields_missing`). The profile is under `data.players.value`.\n\nThis operation declared a bare `'200': { description: ok }` with NO `content` until 2026-09-25, so a client generated from /openapi.json got no type at all for a flagship endpoint.\n\n`data.players.value[]` entries carry `headshot_url` as of 2026-09-28. The key was absent here on EVERY tier, including Pro, while `/v1/players`, `/v1/players/{id}/profile` and `/v1/stored/players` all served it; see the `Player` schema for the present-and-null contract and the Solo field gate.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            },
            "description": "Optional. Narrows the lookup; also selects the plan gate."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data.players` is a FieldResult, or null when unavailable (still 200).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "players": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/FieldResult"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "stats": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/FieldResult"
                                },
                                {
                                  "type": "null"
                                }
                              ],
                              "description": "Null on all four sports sampled 2026-09-25 (basketball, american_football, baseball, football). Per-season and per-game numbers live on `/v1/players/{id}/stats`, `/career` and `/game-log`."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}/club-form": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Football player club-season form",
        "description": "Football-only, stored club-season rows. The route remains available on Free; the derived rating field is omitted below Solo. A player without bridged club data returns an empty array, which cannot prove the field gate.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer"
            },
            "description": "Optional season, subject to the plan's history window."
          }
        ],
        "responses": {
          "200": {
            "x-bbs-field-entitlements": [
              {
                "path": "data[].rating",
                "min_plan": "solo",
                "below_floor": "ABSENT"
              }
            ],
            "description": "Club-season rows, possibly empty.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "league": {
                            "type": "string",
                            "nullable": true
                          },
                          "team": {
                            "type": "string",
                            "nullable": true
                          },
                          "season": {
                            "type": "integer"
                          },
                          "appearances": {
                            "type": "integer",
                            "nullable": true
                          },
                          "goals": {
                            "type": "integer",
                            "nullable": true
                          },
                          "assists": {
                            "type": "integer",
                            "nullable": true
                          },
                          "minutes": {
                            "type": "integer",
                            "nullable": true
                          },
                          "rating": {
                            "type": "number",
                            "nullable": true
                          },
                          "shots_on": {
                            "type": "integer",
                            "nullable": true
                          },
                          "passes_key": {
                            "type": "integer",
                            "nullable": true
                          },
                          "yellow_cards": {
                            "type": "integer",
                            "nullable": true
                          },
                          "red_cards": {
                            "type": "integer",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid player id or query"
          },
          "403": {
            "description": "sport or history entitlement refused"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}/stats": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player stats for a season + league",
        "description": "**Baseball responses carry a `level` field. Read it before summing anything.**\n\n- `level: \"match\"`: `batting`/`pitching` are totals folded from real\n  per-game box scores. 2026 onward.\n- `level: \"season\"`: no per-game box scores exist for the seasons\n  asked for. The numbers are full-season aggregates and are returned in\n  `seasons[]`, each entry additionally marked `level: \"season\"`.\n  `batting`/`pitching` are `null`.\n- `level: \"mixed\"`: both are present. `batting`/`pitching` (per-game\n  derived) and `seasons[]` (season aggregates) cover **disjoint**\n  periods; adding them together double-counts nothing but conflates two\n  different grains. Treat them separately.\n\nA season aggregate has no home/away dimension, so `?split=home|away`\nreturns no `seasons[]` entries rather than full-season totals that\nignore the filter. Fields a source cannot supply (e.g. pitch counts,\nwhich season-level history does not carry) are **omitted**, never\nreturned as `0`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Sport"
            },
            "description": "Required: the players table has no sport column, so this can't yet be inferred from the id (unlike /v1/teams/{id} and /v1/matches/{id})."
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "string"
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string"
            },
            "description": "SCOPE, not a general filter, and the same shape as ?split= below. Read ONLY on the live-upstream branch (football, cricket, mma, boxing, formula1), where it is passed through as the vendor's league id. The DB-backed branches (basketball, american_football, baseball, ice_hockey) serve one aggregate per player per sport and IGNORE it: measured 2026-09-20, ?sport=basketball with league=nba, league=epl and no league returned the same 283-byte body (hash 3ebe3df427), while ?split= over its three values returned three distinct hashes as the control.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "split",
            "schema": {
              "type": "string",
              "enum": [
                "home",
                "away",
                "all"
              ]
            },
            "description": "Per-game splits (basketball + baseball). Season-level aggregates cannot be split and are omitted when this is set to home or away."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`. `data` is POLYMORPHIC by sport and by what we hold; see the branches below. Declared no `content` at all until 2026-09-25.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "description": "Three branches were observed live on 2026-09-25, one request per sport. NO property is required, because the branches share none:\n\n(a) `{ stats: null }`: nothing held for this player in this sport. Seen on american_football and football.\n\n(b) per-game derived totals: `player_id, sport, season, split` plus the sport's own per-game keys (basketball: `gp, gs, ppg, rpg, apg, spg, bpg, fg_pct, fg3_pct, ft_pct`). Seen on basketball.\n\n(c) the `level`-bearing baseball shape documented above: `player_id, sport, season, split, level, batting, pitching, seasons`. Seen on baseball.\n\nbasketball and baseball were the only sports returning numbers on the day, so the per-sport key SETS are UNMEASURED for the rest and are deliberately left open rather than guessed.",
                          "properties": {
                            "player_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "sport": {
                              "$ref": "#/components/schemas/Sport"
                            },
                            "season": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "split": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "level": {
                              "type": "string",
                              "enum": [
                                "match",
                                "season",
                                "mixed"
                              ],
                              "description": "Baseball only. Read it before summing anything; see the route description."
                            },
                            "batting": {
                              "type": [
                                "object",
                                "null"
                              ]
                            },
                            "pitching": {
                              "type": [
                                "object",
                                "null"
                              ]
                            },
                            "seasons": {
                              "type": "array",
                              "items": {
                                "type": "object"
                              }
                            },
                            "stats": {
                              "type": "null",
                              "description": "Present, and null, on branch (a): we hold nothing for this player in this sport. An absence, never 0."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid player id or missing/invalid sport"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/players/{id}/gamelog": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "NBA player game log (flat per-game box lines)",
        "description": "Flat NBA per-game stat lines for a player, most recent first, one row per\ngame with min_played > 0. NBA-only; twin-safe across a player's id twins.\n`data` is a flat array of rows (date, opponent, abbr, min_played, pts,\nreb, ast, stl, blk, fg3m, turnovers, fgm, fga, ftm, fta, plus_minus,\npostseason). 5-minute Redis cache.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "in": "query",
            "name": "postseason",
            "schema": {
              "type": "boolean"
            },
            "description": "true for playoff games only; omit for all games. false explicitly includes regular-season games: until 2026-09-20 every spelling of false (false, 0, no, off) was read as TRUE by `Boolean(\"false\")` and silently returned the filtered set. Accepted: true|1|yes|on and false|0|no|off; anything else is a 400 naming the parameter. From 2026-10-14 it is a REAL three-state filter rather than a two-state one: false returns regular-season games ONLY, true returns playoff games only, and omitting the parameter is how you ask for every game. Between 2026-09-20 and 2026-10-14 false parsed correctly but was never applied to the query, so it behaved exactly as if omitted and there was no way to request a regular-season-only log."
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2000,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid player id or query"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/standings": {
      "get": {
        "tags": [
          "standings"
        ],
        "summary": "League standings table",
        "parameters": [
          {
            "in": "query",
            "name": "league",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/LeagueCode"
            },
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "string"
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "in": "query",
            "name": "conference",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "example": "Big Ten Conference",
            "description": "Narrow the table to one conference/division inside the league. Case-insensitive, matched against the `conference` field on each row. The response shape does not change; still one group per league; so a conference table and a whole-league table parse identically. A conference we do not hold returns 400 listing the ones we do, rather than an empty 200."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "required": [
                            "standings"
                          ],
                          "properties": {
                            "standings": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/Standings"
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "standings",
        "x-bbs-not-available-for-sports": [
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/coverage": {
      "get": {
        "tags": [
          "coverage"
        ],
        "summary": "What we hold, per sport and league (the discovery catalogue)",
        "description": "The catalogue an agent reads to decide whether a dataset exists here, precomputed on a schedule and served from a snapshot. Every league we hold appears with its match counts, seasons, freshness and per-datatype coverage.\n\nVOCABULARY. `data_types` is the stable default and does not change. `?v=2` adds `data_types_v2` beside it, which answers questions about a GRAIN rather than a datatype: \"do you have xG\" is a v1 question, \"do you have PER-PLAYER xG\" is a v2 one. In v2 each measure carries its grains (subject x period, e.g. `team.match` or `player.season`), every grain declares the denominator its figures count, and `not_applicable` names measures that CANNOT apply to a league as distinct from ones we simply lack. `data.vocabulary` echoes which shape was served.\n\nv2 is opt-in because the summary answers most questions and the nested shape is only needed for grain questions. An unrecognised `?v=` serves v1 rather than failing: a catalogue call is discovery, and a query-string typo should not 400.",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            },
            "description": "Narrow to one sport or league; accepts any registry alias. Unknown values are a 400, never a silent empty."
          },
          {
            "in": "query",
            "name": "v",
            "schema": {
              "type": "string",
              "enum": [
                "2"
              ]
            },
            "description": "Set to `2` to include `data_types_v2` (grains, denominators, not_applicable) alongside the unchanged `data_types`. Omit for the stable v1 shape."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`. Declared no `content` until 2026-09-25, on the one endpoint whose entire job is discovery.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "description": "Shape read off the live v1 snapshot on 2026-09-25 (9 sports). `data_types_v2` is present only with `?v=2`, which was NOT exercised by that read, so it is declared open rather than described.",
                          "properties": {
                            "generated_at": {
                              "type": "string",
                              "format": "date-time",
                              "description": "When the snapshot was precomputed, NOT when you asked."
                            },
                            "snapshot_age_seconds": {
                              "type": "number"
                            },
                            "vocabulary": {
                              "type": "string",
                              "description": "Echoes which shape was served, so a `?v=` typo is visible rather than silent."
                            },
                            "totals": {
                              "type": "object",
                              "properties": {
                                "sports": {
                                  "type": "integer"
                                },
                                "leagues": {
                                  "type": "integer"
                                },
                                "matches": {
                                  "type": "integer"
                                },
                                "counts": {
                                  "type": "object"
                                },
                                "leagues_by_depth": {
                                  "type": "object"
                                },
                                "leagues_by_sport": {
                                  "type": "object"
                                }
                              }
                            },
                            "sports": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "name": {
                                    "type": "string"
                                  },
                                  "slug": {
                                    "type": "string"
                                  },
                                  "seo_url": {
                                    "type": "string"
                                  },
                                  "aliases": {
                                    "type": "array",
                                    "items": {
                                      "type": "string"
                                    }
                                  },
                                  "leagues": {
                                    "type": "array",
                                    "items": {
                                      "type": "object"
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "The snapshot is still being built. Carries Retry-After; retry in about a minute."
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/as-of/odds": {
      "get": {
        "tags": [
          "as-of"
        ],
        "summary": "The odds board as it stood at an instant",
        "operationId": "getAsOfOdds",
        "description": "The board as it was actually observed at `as_of`, one row per market, sportsbook and participant, each carrying the `observed_at` instant it was recorded and whether that was before kickoff.\n\nNEVER AN EMPTY 200 FOR A PERIOD WE DID NOT OBSERVE. An `as_of` outside the observed windows is a 422 naming the floor, the ceiling and any gaps, because an empty list cannot be told apart from \"no market was offered\" and would let a backtest run across a window we have no record of. An in-coverage read that finds nothing IS a 200 with an empty list, and that is a different claim: it means no market was observed at that instant.\n\nCOVERAGE IS SHALLOW ON THIS RELATION AND IS NOT THE SAME AS THE PROP ARCHIVE'S. Call `/v1/as-of/coverage?relation=odds` before relying on a period. Depth differs per relation and per sport, and the two are never blended into one number.",
        "parameters": [
          {
            "in": "query",
            "name": "as_of",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "The instant to read the board as of, RFC3339 (e.g. 2026-10-05T23:00:00Z). REQUIRED and has no default: a default of now would make this indistinguishable from a live read."
          },
          {
            "in": "query",
            "name": "match_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The match to read, as a bbs_id from /v1/matches. Retired ids are resolved to their canonical match."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`. `meta.as_of_floor` and `meta.as_of_ceiling` bound what was observed; `meta.empty_means` states what an empty `data` means on an in-coverage read."
          },
          "400": {
            "description": "invalid or missing as_of/match_id"
          },
          "403": {
            "description": "plan_required (Edge or higher)",
            "or sport entitlement refused": null
          },
          "404": {
            "description": "no match with that id"
          },
          "422": {
            "description": "unprocessable: no point-in-time record exists for that instant. The body names as_of_floor, as_of_ceiling and gaps, and suggested_fix points at /v1/as-of/coverage."
          },
          "503": {
            "description": "the coverage windows are still being built",
            "so coverage cannot yet be decided": null
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "odds",
        "x-bbs-not-available-for-sports": [
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/as-of/player-props": {
      "get": {
        "tags": [
          "as-of"
        ],
        "summary": "The player-prop board as it stood at an instant",
        "operationId": "getAsOfPlayerProps",
        "description": "The deep point-in-time relation. One row per book, market, player, outcome and line as last observed at or before `as_of`, each carrying its `observed_at` instant and whether that was before kickoff.\n\nThis is the surface for reconstructing what was known before a game started. Measured on 2026-10-12 the archive spans 2023-05 to 2026-10 and is continuous within each season, with the offseason correctly absent in every year. Those offseason months are reported as gaps and an `as_of` inside one is refused rather than answered with an empty list.\n\nADDRESSED BY OUR OWN `match_id`, never by an upstream identifier, which is the same addressing `/v1/odds/player-props/historical` uses, so one id works across both routes. A match that cannot be resolved to exactly one archived game is a 404 rather than a guess.\n\nDEPTH HERE IS NOT THE DEPTH OF `/v1/as-of/odds`. Call `/v1/as-of/coverage?relation=player_props` for this relation's own windows; the two are never blended.",
        "parameters": [
          {
            "in": "query",
            "name": "as_of",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "The instant to read the prop board as of, RFC3339. REQUIRED and has no default."
          },
          {
            "in": "query",
            "name": "match_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The match to read, as a bbs_id from /v1/matches."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`, with `meta.as_of_floor`, `meta.as_of_ceiling` and `meta.empty_means`."
          },
          "400": {
            "description": "invalid or missing as_of/match_id"
          },
          "403": {
            "description": "plan_required (Edge or higher)",
            "or sport entitlement refused": null
          },
          "404": {
            "description": "that match has no point-in-time prop coverage",
            "or maps to more than one archived game and cannot be disambiguated": null
          },
          "422": {
            "description": "unprocessable: no point-in-time record exists for that instant. Names as_of_floor, as_of_ceiling and gaps."
          },
          "503": {
            "description": "the coverage windows are still being built"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "player props",
        "x-bbs-not-available-for-sports": [
          "football",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/as-of/injuries": {
      "get": {
        "tags": [
          "as-of"
        ],
        "summary": "Availability designations as they stood at an instant",
        "operationId": "getAsOfInjuries",
        "description": "The availability designations in force at `as_of`, one row per athlete, each as an interval with the instant we observed it.\n\nFORWARD-ONLY, AND THAT IS A CORRECTNESS LIMIT RATHER THAN A SCOPE ONE. Before this log existed we kept one row per athlete and overwrote it, so the prior states were not retained and cannot be reconstructed. The log therefore has a floor: the instant observation began. An `as_of` below it is refused, never answered. `meta.floor_means` says so in every response.\n\n`change_kind` keeps three states apart that a caller must be able to distinguish: `seeded` is the state when observation began, so anything earlier is UNKNOWN rather than absent; `listed` and `revised` are observed designations; `withdrawn` means the athlete is no longer listed, with no designation attached, which is how a recovery reaches you.\n\n`open_interval: true` means no end was OBSERVED. It is deliberately not called \"current\": the two differ the moment publishing stops, and `observed_at` lets you see how old the designation is.",
        "parameters": [
          {
            "in": "query",
            "name": "as_of",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "The instant to read designations as of, RFC3339. REQUIRED and has no default."
          },
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Sport"
            },
            "description": "The sport to read. Coverage differs per sport; call /v1/as-of/coverage?relation=player_availability for the observed windows."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`, with `meta.as_of_floor`, `meta.floor_means` and `meta.empty_means`."
          },
          "400": {
            "description": "invalid or missing as_of/sport"
          },
          "403": {
            "description": "plan_required (Edge or higher)",
            "or sport entitlement refused": null
          },
          "422": {
            "description": "unprocessable: no point-in-time record exists for that instant, typically an as_of before observation began. Names as_of_floor and as_of_ceiling."
          },
          "503": {
            "description": "the coverage windows are still being built"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "injuries",
        "x-bbs-not-available-for-sports": [
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/as-of/coverage": {
      "get": {
        "tags": [
          "as-of"
        ],
        "summary": "Which point-in-time windows we actually observed, per relation",
        "operationId": "getAsOfCoverage",
        "description": "What the `/v1/as-of/*` reads can and cannot answer, measured rather than asserted. FREE, for the same reason `/v1/coverage` is: you must be able to find out what we hold before paying for it.\n\nREPORTED PER RELATION AND NEVER BLENDED. The three relations have genuinely different shapes, and a single \"point-in-time coverage\" number would be wrong about all of them: the prop archive is deep, running back to 2023, while the odds board is shallow and begins in 2026-05. Every row is one `(relation, sport)` pair and `meta.blending` says the rows are not averaged.\n\nEVERY COUNT TRAVELS WITH ITS DENOMINATOR. `events_total` is the denominator and `denominator_subject` says what it counts, because that differs per relation: matches for odds, events for player props, player identities for availability. `events_with_series` and `events_with_pre_event_obs` are reported SEPARATELY, so a window holding a series that was only ever observed after kickoff reports 1 and 0 rather than collapsing to \"covered\".\n\nAN ABSENT WINDOW IS `\"coverage\": \"none\"`, NEVER `0`. A window that was never measured and a window with genuinely nothing in it are different facts, so only a measured window carries counts and a `computed_at`. A sport with no coverage at all is listed with an empty `windows` array rather than omitted: an omitted sport reads as an oversight, an absent one reads as a fact.",
        "parameters": [
          {
            "in": "query",
            "name": "relation",
            "schema": {
              "type": "string",
              "enum": [
                "odds",
                "player_props",
                "player_availability"
              ]
            },
            "description": "Narrow to one relation. Omit for all three."
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            },
            "description": "Narrow to one sport. Omit for every sport we report on, including those with no coverage."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`. Each `data` entry is one `(relation, sport)` with `as_of_floor`, `as_of_ceiling`, `windows`, `gaps` and `denominator_subject`."
          },
          "400": {
            "description": "unknown relation or sport"
          },
          "503": {
            "description": "the coverage windows are still being built"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/injuries": {
      "get": {
        "tags": [
          "injuries"
        ],
        "summary": "Active injury reports",
        "description": "Injury reports by sport. What a row MEANS differs by league, because\ninjury data is a reporting artifact and leagues differ in what they\ncompel teams to disclose:\n\n- `american_football`: the NFL's **mandated** weekly injury report\n  `status` is a league-defined game designation:\n  Out / Doubtful / Questionable. `comment` carries practice\n  participation. `player.position` is always present. Published per\n  week; `meta.as_of` is the date of the games that week's report covers.\n  **No `return_date`**: the NFL report is designation-based and states\n  no expected return, so the field is omitted rather than sent as a\n  permanent null. Only rows carrying a game designation are returned;\n  a player logging a full practice with no designation is not injured,\n  and is not listed.\n- `basketball`, `ice_hockey`: NBA / NHL reports from a paid aggregator\n  (`aggregator-paid`). Note the NHL requires no disclosure at all, so\n  NHL detail is vague at the league (\"upper-body\"), not at this API.\n- `football`: soccer absences from a paid aggregator\n  (`aggregator-paid`). Mostly\n  **retrospective**: a record that a player missed a fixture that has\n  been played, which includes non-injury absences such as suspension.\n  `meta.as_of` is the newest matchday held.\n- `baseball`, and everything else: not served; returns 400 naming the\n  sports that are.\n\nEndpoints that serve a stored snapshot always carry `meta.as_of` (the\ncurrency of the DATA, never the time we last ingested it) and set\n`meta.stale: true` with a plain-language `meta.message` when no fresher\ndata exists yet (e.g. between seasons).\n",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string"
            },
            "description": "Team NAME, short name / abbreviation, FIFA code or Big Balls team id (UUID): whichever spelling you read back off a previous response. Until 2026-09-20 only the UUID matched, so `?team=ARI` returned 0 of the 7 ARI rows the unfiltered call returns, with a 200. NOT APPLIED on sport=basketball and sport=ice_hockey: those rows are keyed on the player and the board is league-wide, so the response returns every team AND discloses the parameter in `meta.ignored_params` rather than answering silently. Applying it there is a pending decision, not an accident."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`; `meta.as_of` is the currency of the DATA. Declared no `content` until 2026-09-25.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "sport": {
                              "$ref": "#/components/schemas/Sport"
                            },
                            "count": {
                              "type": "integer",
                              "description": "Rows in `injuries` AFTER any ?team= filter, so it is the count of what you were given."
                            },
                            "injuries": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "description": "Row shape read off `?sport=american_football` live on 2026-09-25 (12 rows). WHAT A ROW MEANS DIFFERS BY LEAGUE; see the route description. and the other served sports were not sampled in that read, so no property is declared required and extra keys are permitted. Notably `return_date` is OMITTED on american_football rather than sent as a permanent null.",
                                "properties": {
                                  "player": {
                                    "type": "object"
                                  },
                                  "status": {
                                    "type": "string",
                                    "description": "On american_football this is the NFL's league-defined game designation (Out / Doubtful / Questionable), not a medical status."
                                  },
                                  "injury_type": {
                                    "type": [
                                      "string",
                                      "null"
                                    ]
                                  },
                                  "comment": {
                                    "type": [
                                      "string",
                                      "null"
                                    ],
                                    "description": "Practice participation on american_football."
                                  },
                                  "updated_at": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "injuries",
        "x-bbs-not-available-for-sports": [
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/webhooks": {
      "get": {
        "tags": [
          "webhooks"
        ],
        "summary": "List your registered webhooks",
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`; `data` is the list of webhooks registered to the CALLING key. Declared no `content` until 2026-09-25.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "description": "The key used for the 2026-09-25 read had NO webhooks registered, so the live response was an empty array and the ITEM shape could not be derived from it. It is left as a bare object rather than bound to `WebhookRegistration`, because that schema describes the POST request body and binding it here would be a guess presented as a contract. The item shape is the one open question left on this operation.",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      },
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Register a webhook subscription (Edge+)",
        "description": "Creates an HTTPS webhook on an Edge-or-higher API key with webhook:write scope. The HMAC secret is returned exactly once. Delivery is at least once and uses X-BBS-Signature: t=<unix>,v1=<hex>, where the HMAC-SHA256 input is \"t.raw_body\". Non-2xx responses are retried after 30s, 2m, 10m, and 1h (five attempts total).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookRegistration"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "created; secret is present only in this response"
          },
          "400": {
            "description": "invalid event list or unsafe destination URL"
          },
          "403": {
            "description": "Edge-or-higher plan and webhook:write scope required"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/webhooks/{id}": {
      "delete": {
        "tags": [
          "webhooks"
        ],
        "summary": "Deregister a webhook",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "deleted"
          },
          "404": {
            "description": "not found"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/webhooks/{id}/unpark": {
      "post": {
        "tags": [
          "webhooks"
        ],
        "summary": "Re-enable a parked webhook after fixing its endpoint",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "re-enabled"
          },
          "400": {
            "description": "destination URL is no longer safe"
          },
          "403": {
            "description": "Edge-or-higher plan and webhook:write scope required"
          },
          "404": {
            "description": "not found"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "List players (filterable)",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "in": "query",
            "name": "leagueId",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "description": "Canonical league key, brand alias or exact display name: `nba`, `NBA` and `epl` all resolve, same as on /v1/stored/players, /v1/standings and /v1/stored/matches. A league we do not hold returns 400 naming the value and suggesting the valid keys, rather than a 200 over every league. Accepted and SILENTLY DROPPED until 2026-10-07 (and merely disclosed in `meta.ignored_params` from 2026-09-20): `?league=nba`, `?league=ncaab` and no league at all each returned pagination.total 175889, the whole table, against 6,998 for the `?sport=basketball` control. ?leagueId= (a league UUID from GET /v1/leagues) still works and is unchanged.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "teamId",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "name",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/teams/{id}/form": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Recent finished matches with derived result (W/D/L from this team's perspective)",
        "description": "Recent finished matches with the result derived from this team's\nperspective (W/D/L), newest first.\n**Solo plan or higher**: derived team form starts at Solo; Free\nreceives a 403 naming the tier and linking the docs and pricing pages.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 1,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "home": {
                            "type": "string",
                            "nullable": true
                          },
                          "away": {
                            "type": "string",
                            "nullable": true
                          },
                          "home_score": {
                            "type": "integer",
                            "nullable": true
                          },
                          "away_score": {
                            "type": "integer",
                            "nullable": true
                          },
                          "result": {
                            "type": "string",
                            "enum": [
                              "W",
                              "D",
                              "L"
                            ],
                            "nullable": true
                          },
                          "competition": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid team id"
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/teams/{id}/stats": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Aggregated team stats (W/D/L, goals, BTTS, O/U, form string)",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "tournament",
            "schema": {
              "type": "string",
              "enum": [
                "wc2026"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "matches_played": {
                          "type": "integer"
                        },
                        "matches_with_scores": {
                          "type": "integer"
                        },
                        "wins": {
                          "type": "integer"
                        },
                        "draws": {
                          "type": "integer"
                        },
                        "losses": {
                          "type": "integer"
                        },
                        "goals_scored": {
                          "type": "integer"
                        },
                        "goals_conceded": {
                          "type": "integer"
                        },
                        "goal_difference": {
                          "type": "integer"
                        },
                        "clean_sheets": {
                          "type": "integer"
                        },
                        "points": {
                          "type": "integer"
                        },
                        "form_string": {
                          "type": "string",
                          "description": "Last 5 results, oldest→newest e.g. 'WWDLW'"
                        },
                        "btts_rate": {
                          "type": "number",
                          "description": "Fraction of scored matches where both teams scored (0–1)"
                        },
                        "over_2_5_rate": {
                          "type": "number",
                          "description": "Fraction of scored matches with 3+ total goals (0–1)"
                        },
                        "avg_goals_scored": {
                          "type": "number"
                        },
                        "avg_goals_conceded": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "team not found or not in requested tournament"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "team stats",
        "x-bbs-not-available-for-sports": [
          "basketball",
          "ice_hockey",
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/teams/{id}/elo": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Team Elo rating, league rank, and league context (all rated leagues)",
        "description": "Standalone Elo lookup for one team: current rating, the team rank within\nits own league (ROW_NUMBER by Elo descending), and league context (name,\nabbreviation, logo, league, sport). Works for any rated league: NCAAF,\nNCAAF FCS, NFL, NBA, NHL, MLB, the five European domestic football\nleagues, and WC2026 national teams. Always 200 when\nthe team exists; an unrated team (below the min-games guard or a\nnon-rated league) returns null rating fields with a pending note.\n5-minute Redis cache.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "team_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "name": {
                          "type": "string"
                        },
                        "abbreviation": {
                          "type": "string",
                          "nullable": true
                        },
                        "logo_url": {
                          "type": "string",
                          "nullable": true
                        },
                        "sport": {
                          "type": "string",
                          "nullable": true
                        },
                        "league": {
                          "type": "string",
                          "nullable": true
                        },
                        "elo_rating": {
                          "type": "number",
                          "nullable": true,
                          "description": "Rounded to 1 decimal; null when unrated"
                        },
                        "elo_rank": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Rank by Elo descending within the team's league"
                        },
                        "games_counted": {
                          "type": "integer",
                          "nullable": true
                        },
                        "last_computed": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "note": {
                          "type": "string",
                          "nullable": true,
                          "description": "Pending message when the team is unrated; null otherwise"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid team id (must be a UUID)"
          },
          "404": {
            "description": "team not found"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "Elo ratings",
        "x-bbs-not-available-for-sports": [
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nba/games": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA games list (legacy v1 shape, 1946-2025)",
        "deprecated": true,
        "x-sunset": "2027-04-01",
        "description": "Each game carries `match_id`, the canonical match UUID usable with\nevery /v1 UUID-keyed endpoint (availability, lineups, odds). It is\nnull for games our canonical coverage cannot reach: anything before\n2019-10-22, plus preseason and All-Star games. This response shape is\nstable through 2027-03-31; migrate list reads to\n`/v1/matches?league=nba`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1946,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 4
            },
            "description": "Team abbreviation (e.g. LAL)"
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string",
              "enum": [
                "REG",
                "POST",
                "PRE",
                "ALLSTAR",
                "Regular Season",
                "Playoffs",
                "Pre Season",
                "All Star"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "game_id": {
                            "type": "string"
                          },
                          "match_id": {
                            "type": "string",
                            "format": "uuid",
                            "nullable": true
                          },
                          "season_id": {
                            "type": "string"
                          },
                          "season": {
                            "type": "integer",
                            "description": "Season label: the year the season STARTED."
                          },
                          "season_type": {
                            "type": "string"
                          },
                          "game_date": {
                            "type": "string",
                            "format": "date"
                          },
                          "home": {
                            "$ref": "#/components/schemas/NbaGameTeamBox"
                          },
                          "away": {
                            "$ref": "#/components/schemas/NbaGameTeamBox"
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "rebound_split": {
                          "$ref": "#/components/schemas/NbaReboundSplitMeta"
                        },
                        "history_window": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present only when a plan or coverage floor clamped the default window."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/ncaab/tournament": {
      "get": {
        "tags": [
          "ncaab"
        ],
        "summary": "NCAA tournament bracket (men's + women's)",
        "description": "March Madness bracket games (seeds, rounds, scores and winners) for NCAAB (men's) and NCAAW (women's) (migration 278). `?season=` is required: the table is additive across tournament years but today holds season 2025 only (134 rows: 67 men's + 67 women's), which is the bracket PLAYED between 17 March and 7 April 2026. A season label is the year the season BEGAN, not the year it was played, so `?season=2026` is a tournament that has not been played yet; it returns an empty list with a `meta.note` explaining that rather than a bare empty 200. `round` is the feed's own observed numbering: 0 = First Four, 1 through 5 = Round of 64 through Elite Eight, 6 = Championship. `?limit=` defaults to 200 so that one request returns a whole bracket; any short page carries a `meta.note` saying how many games matched. Region labels are not populated on any row currently held, so `?region=` matches nothing; select a stage with `?round=` instead.",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 2000,
              "maximum": 2100
            },
            "description": "Year the tournament SEASON began, not the year it was played: season 2025 is the bracket played in March 2026. We currently hold 2025 only."
          },
          {
            "in": "query",
            "name": "gender",
            "schema": {
              "type": "string",
              "enum": [
                "men",
                "women"
              ]
            },
            "description": "Omit for both."
          },
          {
            "in": "query",
            "name": "round",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 7
            },
            "description": "The feed's observed round number: 0 = First Four ... 6 = Championship."
          },
          {
            "in": "query",
            "name": "region",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "description": "Case-insensitive substring match against the region label. Matches NOTHING today: region labels are empty on every bracket row currently held, so any value returns an empty list. Use ?round= to select a stage instead."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 200
            },
            "description": "Page size. Defaults to 200 rather than the usual 50, so a single request returns a complete bracket (67 games for one gender, 134 for both)."
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Rows to skip. Results are ordered by round, then tip-off time, then game id, so pages are stable across requests."
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "description": "Alias for ?offset=(page-1)*limit. An explicit ?offset= wins."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data` is an array of bracket games (empty when the filter genuinely matches nothing, or when the season has no data yet; `meta.note` says which). `meta` carries `count` (rows in this page), `total` (rows matching the filter), `limit` and `offset`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "gender": {
                                "type": "string",
                                "enum": [
                                  "men",
                                  "women"
                                ]
                              },
                              "season": {
                                "type": "integer"
                              },
                              "round": {
                                "type": "integer",
                                "minimum": 0,
                                "maximum": 7
                              },
                              "region_id": {
                                "type": "integer",
                                "nullable": true
                              },
                              "region_label": {
                                "type": "string",
                                "nullable": true
                              },
                              "bracket_location": {
                                "type": "integer",
                                "nullable": true
                              },
                              "scheduled_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "venue": {
                                "type": "string",
                                "nullable": true
                              },
                              "status": {
                                "type": "string",
                                "nullable": true
                              },
                              "status_state": {
                                "type": "string"
                              },
                              "home_team": {
                                "type": "object",
                                "properties": {
                                  "school": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "School name, e.g. \"NC State\"."
                                  },
                                  "name": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Mascot only, e.g. \"Wolfpack\"."
                                  },
                                  "full_name": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "School + mascot, e.g. \"NC State Wolfpack\". Use this to render a bracket."
                                  },
                                  "abbreviation": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "The school's own short form, e.g. \"NCSU\". Stable across seasons; not a joinable BBS team id; no bridge exists between bracket rows and /v1/teams."
                                  },
                                  "seed": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "score": {
                                    "type": "integer",
                                    "nullable": true
                                  },
                                  "winner": {
                                    "type": "boolean",
                                    "nullable": true
                                  }
                                }
                              },
                              "away_team": {
                                "type": "object",
                                "properties": {
                                  "school": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "School name, e.g. \"TCU\"."
                                  },
                                  "name": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "Mascot only, e.g. \"Horned Frogs\"."
                                  },
                                  "full_name": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "School + mascot, e.g. \"TCU Horned Frogs\". Use this to render a bracket."
                                  },
                                  "abbreviation": {
                                    "type": "string",
                                    "nullable": true,
                                    "description": "The school's own short form, e.g. \"TCU\". Stable across seasons; not a joinable BBS team id."
                                  },
                                  "seed": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "score": {
                                    "type": "integer",
                                    "nullable": true
                                  },
                                  "winner": {
                                    "type": "boolean",
                                    "nullable": true
                                  }
                                }
                              },
                              "updated_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        },
                        "meta": {
                          "type": "object",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "description": "Rows in this page."
                            },
                            "total": {
                              "type": "integer",
                              "description": "Rows matching the filter, before limit/offset."
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "offset": {
                              "type": "integer"
                            },
                            "note": {
                              "type": "string",
                              "description": "Present only when the result needs explaining: the page is shorter than `total` (says how to fetch the rest), the season is not one we hold (names the seasons that are), or the filter cannot match at all (`?region=`, which has no populated data)."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters: a missing ?season=, an out-of-range ?round=, or an out-of-range ?limit/?offset.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/nba/games/{id}": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "Single NBA game with embedded linescore",
        "description": "Includes `match_id` (canonical match UUID, nullable; see\n/v1/nba/games) bridging this game into the UUID-keyed endpoints.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "numeric game id (10-digit, e.g. \"0042200405\")",
            "schema": {
              "type": "string",
              "pattern": "^\\d{10}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "match_id": {
                          "type": "string",
                          "format": "uuid",
                          "nullable": true,
                          "description": "Canonical match UUID; null when the game predates canonical coverage (pre-2019) or is preseason/All-Star"
                        },
                        "home": {
                          "$ref": "#/components/schemas/NbaGameTeamBox"
                        },
                        "away": {
                          "$ref": "#/components/schemas/NbaGameTeamBox"
                        },
                        "linescore": {
                          "oneOf": [
                            {
                              "type": "object",
                              "properties": {
                                "home": {
                                  "type": "array",
                                  "items": {
                                    "type": "integer",
                                    "nullable": true
                                  }
                                },
                                "away": {
                                  "type": "array",
                                  "items": {
                                    "type": "integer",
                                    "nullable": true
                                  }
                                }
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "rebound_split": {
                          "$ref": "#/components/schemas/NbaReboundSplitMeta"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid game id format"
          },
          "404": {
            "description": "game not found"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/nba/games/{id}/plays": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "Play-by-play for an NBA game",
        "description": "Pre-1996 games have no PBP in the Kaggle dataset; those return 200 with\n`data: []` and `meta.pbp_available: false`. 404 only when the game\nitself doesn't exist.\n**Pro plan or higher**: full play-by-play logs are a Pro capability\nfor basketball, ice hockey and American football, NBA included, and\nNBA shot coordinates are part of it; Free / Solo / Trio / Edge receive\na 403 naming the tier and linking the docs and pricing pages. Soccer\nmatch events are a separate, free capability: see\n`/v1/matches/{id}/events`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{10}$"
            }
          },
          {
            "in": "query",
            "name": "period",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 14
            }
          },
          {
            "in": "query",
            "name": "player",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "2544"
            },
            "description": "numeric player id, filters plays where this player appears in player1/2/3 slots"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "pbp_available": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid game id or query"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          },
          "404": {
            "description": "game not found"
          }
        },
        "x-bbs-min-plan": "pro",
        "x-bbs-capability": "play-by-play",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "ice_hockey",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nba/games/{id}/availability": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "Who played in an NBA game (box-score derived availability)",
        "description": "Home/away player arrays with `min_played` and a derived `played`\nboolean, from post-game box scores. Self-teaching: every response's\n`meta` block carries these same semantics so the payload explains\nitself.\n\n- `data_type: post_game_participation`: this is \"who played\", a\n  strong historical availability signal for modeling. It is NOT a\n  pre-tip lineup (for true pre-tip lineups, 2025-26 onward, use\n  /v1/nba/games/{id}/lineups).\n- Semantics: arrays contain ONLY players who logged minutes. A\n  player who is absent did not play: DNP (coach decision),\n  inactive, or injured, and this endpoint cannot distinguish those\n  reasons. Absence never appears as `played: false`.\n- Coverage: seasons 2019-20 through 2025-26 (box-score era). Games\n  before 2019-10-22, preseason and All-Star games are not covered.\n- For true pre-tip availability on current and upcoming games,\n  cross-reference GET /v1/injuries?sport=nba (league-wide active\n  injury reports) or GET /v1/players/{id}/injury. Historical\n  pre-tip injury reports do not exist at our source.\n- `starter` is true/false only for games with lineup coverage\n  (2025-26 onward), null for earlier games; historical starter data\n  does not exist at our source and we will not fabricate it.\n\n`id` accepts a canonical match UUID or a 10-digit numeric game id\n(bridged via the games list's `match_id`). A real game outside\ncoverage returns 200 with empty arrays and\n`meta.availability_available: false`; 404 only when the id resolves\nto nothing.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "0042200405"
            },
            "description": "Match UUID or 10-digit numeric game id"
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "game": {
                          "type": "object",
                          "properties": {
                            "match_id": {
                              "type": "string",
                              "format": "uuid",
                              "nullable": true
                            },
                            "game_id": {
                              "type": "string",
                              "nullable": true,
                              "example": "0042200405",
                              "description": "10-digit numeric id when bridged"
                            },
                            "game_date": {
                              "type": "string",
                              "format": "date",
                              "nullable": true
                            },
                            "season_year": {
                              "type": "integer",
                              "nullable": true,
                              "description": "Season start year (2022 = 2022-23)"
                            },
                            "postseason": {
                              "type": "boolean",
                              "nullable": true
                            },
                            "home": {
                              "type": "object",
                              "properties": {
                                "team_id": {
                                  "type": "string",
                                  "format": "uuid",
                                  "nullable": true
                                },
                                "name": {
                                  "type": "string",
                                  "nullable": true
                                }
                              }
                            },
                            "away": {
                              "type": "object",
                              "properties": {
                                "team_id": {
                                  "type": "string",
                                  "format": "uuid",
                                  "nullable": true
                                },
                                "name": {
                                  "type": "string",
                                  "nullable": true
                                }
                              }
                            }
                          }
                        },
                        "home": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "player_id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              },
                              "min_played": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Whole minutes played"
                              },
                              "played": {
                                "type": "boolean",
                                "description": "min_played > 0. Always true in practice; zero-minute players have no box-score row and are absent"
                              },
                              "starter": {
                                "type": "boolean",
                                "nullable": true,
                                "description": "Only set for games with lineup coverage (2025-26 onward)"
                              }
                            }
                          }
                        },
                        "away": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "player_id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              },
                              "min_played": {
                                "type": "integer",
                                "nullable": true
                              },
                              "played": {
                                "type": "boolean"
                              },
                              "starter": {
                                "type": "boolean",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "availability_available": {
                          "type": "boolean"
                        },
                        "coverage": {
                          "type": "string",
                          "description": "Real season range: 2019-20 through 2025-26 (box-score era)"
                        },
                        "data_type": {
                          "type": "string",
                          "enum": [
                            "post_game_participation"
                          ],
                          "description": "Always post_game_participation, never a pre-tip lineup"
                        },
                        "semantics": {
                          "type": "string",
                          "description": "Plain-language contract: arrays hold only players who logged minutes; absence = DNP/inactive/injured, indistinguishable here"
                        },
                        "for_true_availability": {
                          "type": "string",
                          "description": "Where to get pre-tip availability for current/upcoming games (/v1/injuries?sport=nba, /v1/players/{id}/injury)"
                        },
                        "starter_coverage": {
                          "type": "string",
                          "description": "starter flags exist 2025-26 onward only; null for historical games"
                        },
                        "players_unassigned": {
                          "type": "integer"
                        },
                        "reason": {
                          "type": "string",
                          "description": "Present only on empty responses, explains why"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "id is neither a match UUID nor a 10-digit numeric game id"
          },
          "404": {
            "description": "game not found"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/nba/players/{id}/stats": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "Career stats aggregated from NBA PBP (1996-2023)",
        "description": "Approximate aggregates from `nba_plays`. Players whose entire career\nwas pre-1996 return 200 with `games_played=0`. Prefer the public BigBalls\nplayer UUID returned by `/v1/players`; legacy numeric player\nids remain accepted for backwards compatibility.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "BigBalls player UUID, or a legacy numeric player id (digits only, e.g. \"2544\")",
            "schema": {
              "oneOf": [
                {
                  "type": "string",
                  "format": "uuid"
                },
                {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "example": "2544"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "player_id": {
                          "type": "string",
                          "description": "BigBalls UUID for UUID requests; legacy numeric id is echoed for legacy requests"
                        },
                        "games_played": {
                          "type": "integer"
                        },
                        "total_points": {
                          "type": "integer",
                          "description": "Count of MADE FG events (eventmsgtype=1); not point-weighted"
                        },
                        "total_assists": {
                          "type": "integer",
                          "description": "Count of MADE FG events where player is player2_id (assister)"
                        },
                        "appearances_per_period": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "source": {
                          "type": "string",
                          "example": "kaggle-pbp"
                        },
                        "coverage": {
                          "type": "string"
                        },
                        "approximation_notes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid player id format"
          },
          "404": {
            "description": "no NBA player with that id"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nba/teams/{id}/season-averages": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA team season averages, by category",
        "description": "Team-level season averages as the vendor publishes them. The TEAM\ncounterpart of `/v1/nba/players/{id}/season-averages`, and deliberately\none route rather than seven: the relation is a cube, so `category` is a\nfilter and an ABSENT filter returns every slice.\n\nCoverage: 1,290 rows, 30 teams across 43 populated\n(category, stat_type) slices in 7 categories, season 2025, regular\nseason only. An impossible `category` or `stat_type` returns 200 with an\nempty `data` rather than being ignored.\n\nA well-formed id that is not an NBA team returns 404. A known NBA team\nwith no rows for the requested filter returns 200 with an empty `data`\nand a `meta.note` naming the coverage boundary.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "BigBalls team id (UUID from /v1/teams)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1946,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year the season STARTS in. 2025 means the 2025-26 campaign."
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ]
            }
          },
          {
            "in": "query",
            "name": "category",
            "schema": {
              "type": "string",
              "maxLength": 40,
              "pattern": "^[a-z0-9_]+$"
            },
            "description": "general, clutch, tracking, playtype, shooting, shotdashboard or hustle. Not an enum: the vendor adds a category when it adds a dashboard."
          },
          {
            "in": "query",
            "name": "stat_type",
            "schema": {
              "type": "string",
              "maxLength": 40,
              "pattern": "^[a-z0-9_]*$"
            },
            "description": "The slice inside the category. The empty string is a real value and is the one `hustle` uses."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Alias for offset: page=2 means offset=limit. An explicit offset wins."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NbaTeamSeasonAverage"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Newest row timestamp on THIS page: a fact about the response, not the feed."
                        },
                        "note": {
                          "type": "string",
                          "description": "Present only when data is empty; explains the two reasons it can be."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid team id format, or invalid query"
          },
          "403": {
            "description": "plan does not include basketball"
          },
          "404": {
            "description": "no NBA team with that id"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/nba/players/{id}/season-averages": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA per-game season averages",
        "description": "Per-game averages exactly as the vendor publishes them: one row per\nseason, regular season only. These are NOT the same numbers as\n`/v1/nba/players/{id}/season`, which DERIVES its averages from box\nscores; the two will differ in the last decimal.\n\nCoverage: seasons 2023-2025, 1,723 rows across 802 players. Every\ncolumn is non-NULL on every row; a `0` is a real zero (a bench player\nwho attempted no threes), not a placeholder.\n\nA well-formed id that is not an NBA player returns 404. A known NBA\nplayer with no rows in the measured season span returns 200 with an\nempty `data` and a `meta.note` explaining the coverage boundary.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "BigBalls player id (UUID from /v1/players)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1946,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year the season STARTS in. 2025 means the 2025-26 campaign."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Alias for offset: page=2 means offset=limit. An explicit offset wins."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NbaPlayerSeasonAverage"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Newest row timestamp on THIS page: a fact about the response, not the feed."
                        },
                        "note": {
                          "type": "string",
                          "description": "Present only when data is empty; explains the two reasons it can be."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid player id format, or invalid query"
          },
          "403": {
            "description": "plan does not include basketball"
          },
          "404": {
            "description": "no player with that id"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/nba/players/{id}/advanced-stats": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA advanced season stats and league ranks",
        "description": "Vendor-computed advanced season aggregates: shooting efficiency, usage,\non/off ratings, pace and PIE, plus the vendor's own league rank for\neach under `ranks`.\n\nSeparate from `/season-averages` because the grain differs: this\nrelation splits regular season from playoffs and the other does not.\nWithout `?season_type` both are returned, each labelled, newest season\nfirst with the regular season ahead of the playoffs within a season.\n\nCoverage: seasons 2023-2025 regular season plus 2023 playoffs, 1,937\nrows across 802 players.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "BigBalls player id (UUID from /v1/players)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1946,
              "maximum": 2100
            }
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Alias for offset: page=2 means offset=limit. An explicit offset wins."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NbaPlayerAdvancedSeason"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid player id format, or invalid query"
          },
          "403": {
            "description": "plan does not include basketball"
          },
          "404": {
            "description": "no player with that id"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nba/players/{id}/advanced-game-stats": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA advanced and tracking measures, per game",
        "description": "The PER-GAME grain of `/advanced-stats`: 116 advanced and tracking\nmeasures for one player in one game, and per quarter or overtime when\n`?period` asks for it.\n\nSeparate from `/advanced-stats` because the grain differs, exactly as\n`/advanced-stats` is separate from `/season-averages`.\n\n`?period` DEFAULTS TO 0, the whole game, so the bare path returns one\nrow per game. `?period=all` returns every period as well, and\n`?period=3` pins one. `meta.period` always echoes which was applied.\n\nCoverage: season 2025, 132,621 rows over 1,301 games and 594 players,\n2025-10-21 to 2026-06-13; 34,810 of those rows are the full game\n(`period=0`) and 8,565 are postseason.\n\nA well-formed id that is not an NBA player returns 404. A known NBA\nplayer with no rows returns 200 with an empty `data` and a `meta.note`\nexplaining the coverage boundary.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "BigBalls player id (UUID from /v1/players)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1946,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year the season STARTS in. 2025 means the 2025-26 campaign."
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ]
            }
          },
          {
            "in": "query",
            "name": "period",
            "description": "0 (default) is the whole game, 1-4 are quarters, 5+ are overtimes, and the literal `all` returns every period.",
            "schema": {
              "oneOf": [
                {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 10
                },
                {
                  "type": "string",
                  "enum": [
                    "all"
                  ]
                }
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Alias for offset: page=2 means offset=limit. An explicit offset wins."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NbaPlayerAdvancedGameStat"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Newest row timestamp on THIS page: a fact about the response, not the feed."
                        },
                        "period": {
                          "description": "The period filter actually applied: an integer, or `all`."
                        },
                        "note": {
                          "type": "string",
                          "description": "Present only when data is empty."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid player id format, or invalid query"
          },
          "403": {
            "description": "plan does not include basketball"
          },
          "404": {
            "description": "no player with that id"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nba/players/{id}/contracts": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA player career contract totals (Edge and above)",
        "description": "Career contract totals: term, type, status, total value, average\nsalary, guarantees, the exception the deal was signed using, and the\nfree-agency year it ends in. Money is whole US dollars.\n\nEDGE AND ABOVE. A key below Edge receives 403 with the tier, the price\nand a checkout link.\n\nShipped alongside `/contract-seasons`, which is the per-season\nbreakdown. The two are NOT nested: they are stored in separate id\nspaces upstream, so joining them would drop rows rather than relate\nthem. Ask both and relate them by `season` against\n`start_year`-`end_year` yourself if you need that.\n\n`?season` selects the deals IN FORCE in that season, not deals whose\nlabel equals it.\n\nCoverage: 1,533 contracts across 451 players, start years 2001 through\nend years 2031.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "BigBalls player id (UUID from /v1/players)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1946,
              "maximum": 2100
            },
            "description": "Returns the deals in force in this season LABEL."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Alias for offset: page=2 means offset=limit. An explicit offset wins."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NbaPlayerContract"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "currency": {
                          "type": "string",
                          "example": "USD"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid player id format, or invalid query"
          },
          "403": {
            "description": "plan is below Edge, or does not include basketball"
          },
          "404": {
            "description": "no player with that id"
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/nba/players/{id}/contract-seasons": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA player per-season cap hit and salary (Edge and above)",
        "description": "The per-season breakdown behind `/contracts`: cap hit, total cash, base\nsalary and the league salary rank for that season. Money is whole US\ndollars.\n\nEDGE AND ABOVE, same refusal as `/contracts`.\n\nMORE THAN ONE ROW PER SEASON IS REAL DATA. A player traded mid-deal, or\nsigned to a second contract inside one league year, has two rows for\nthat season and both figures are true, so nothing here is\ndeduplicated.\n\nCoverage: 3,586 contract-seasons across 456 players, seasons 2011\nthrough 2031.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "BigBalls player id (UUID from /v1/players)",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1946,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Alias for offset: page=2 means offset=limit. An explicit offset wins."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NbaPlayerContractSeason"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "currency": {
                          "type": "string",
                          "example": "USD"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid player id format, or invalid query"
          },
          "403": {
            "description": "plan is below Edge, or does not include basketball"
          },
          "404": {
            "description": "no player with that id"
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/nfl/games": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "NFL schedule rows (historical)",
        "description": "NFL schedule and final scores, 2020 onward. Omit `season` to get the newest season present; `meta.resolved` names the season actually served and whether it was defaulted, so the bare path always answers.",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1999,
              "maximum": 2100
            },
            "description": "Optional; defaults to the newest season present, echoed in meta.resolved. Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "week",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 22
            }
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 4
            }
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string",
              "enum": [
                "REG",
                "WC",
                "DIV",
                "CON",
                "SB",
                "POST"
              ]
            },
            "description": "Round filter. REG, WC, DIV, CON and SB are the values game_type actually holds; POST is an alias matching every playoff round (WC, DIV, CON, SB)."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/nfl/games/{id}": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "Single NFL game by league-standard game id",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "League-standard NFL game id (e.g. \"2024_11_KC_BUF\")",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}_\\d{2}_[A-Z0-9]{2,4}_[A-Z0-9]{2,4}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data` is the single game row. This response carries NO `meta` at all, which is legal; only `data` is guaranteed. Declared no `content` until 2026-09-25.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "description": "Read off `/v1/nfl/games/2026_01_NE_SEA` live on 2026-09-25. `game_id` is the league-standard id; the cross-sport uuid is `match_id`, and those two never join in the same direction.",
                          "properties": {
                            "game_id": {
                              "type": "string",
                              "example": "2026_01_NE_SEA"
                            },
                            "match_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "season": {
                              "type": "integer"
                            },
                            "week": {
                              "type": "integer"
                            },
                            "game_date": {
                              "type": "string"
                            },
                            "game_type": {
                              "type": "string"
                            },
                            "home_team": {
                              "type": "string"
                            },
                            "away_team": {
                              "type": "string"
                            },
                            "home_score": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "away_score": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "stadium": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "roof": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "surface": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "home_rest": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "away_rest": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid game id format"
          },
          "404": {
            "description": "game not found"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/nfl/games/{id}/plays": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "NFL play-by-play with EPA + Win Probability",
        "description": "**Pro plan or higher**: all NFL play-by-play (the full play/type/\ndown/distance/EPA/win-probability event log) requires Pro; Free /\nSolo / Trio / Edge receive a 403 naming the tier and linking the docs\nand pricing pages. NBA play-by-play carries the same Pro floor; it\nstarted at Edge until 2026-09-18.\n\nEach play also carries (migration 283): passer/receiver/rusher\nattribution; kicker/punter/punt-returner/kickoff-returner attribution\n(`kicker_player_id` is used on both field-goal and kickoff attempts);\nper-play `air_yards`/`yards_after_catch`; scoring, turnover and\npenalty attribution (`touchdown`, `interception`, `fumble`, `sack`,\n`penalty` and the player/team ids behind each); and situational\ncontext (`qtr`, `game_seconds_remaining`, `goal_to_go`). Boolean\nindicators are `null`, not `false`, when the play was not classified\nfor that indicator. `pass_attempt`/`complete_pass`/`field_goal_attempt`/\n`field_goal_result`/`kick_distance` (migration 255) are also included.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}_\\d{2}_[A-Z0-9]{2,4}_[A-Z0-9]{2,4}$"
            }
          },
          {
            "in": "query",
            "name": "play_type",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 30
            }
          },
          {
            "in": "query",
            "name": "min_epa",
            "schema": {
              "type": "number"
            }
          },
          {
            "in": "query",
            "name": "down",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 4
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "description": "Present ONLY when the request returned no plays AND the game itself exists, so an empty array always explains itself. `coverage` is false when we hold no play-by-play for the game yet, and true when the game does have plays but your play_type/down/min_epa filters matched none of them (the message then says how many it has). Paging past the end carries no meta, because that page is empty for a reason the caller already knows. An id that names no game is a 404, not an empty array.",
                      "properties": {
                        "source": {
                          "type": "string",
                          "enum": [
                            "stored"
                          ]
                        },
                        "coverage": {
                          "type": "boolean"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid game id format. Carries a suggested_fix naming the SEASON_WEEK_AWAY_HOME shape and an id that works today."
          },
          "403": {
            "description": "plan_required (Pro or higher)"
          },
          "404": {
            "description": "game not found. The id is well formed but names no game we hold, which is what swapping the away and home team produces. An empty plays array is NOT a 404, see the 200 above."
          }
        },
        "x-bbs-min-plan": "pro",
        "x-bbs-capability": "play-by-play",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "ice_hockey",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nfl/injuries": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "NFL injuries - weekly report, practice participation and roster status",
        "description": "Three facts about a player, never collapsed into one. `report` is the league's mandated weekly game designation (Out, Doubtful, Questionable), `practice` is participation (Did Not Participate, Limited, Full), and `roster` is reserve-list state (IR, IR-R, PUP-R, NFI-R, Reserve-Sus, Reserve-DNR) with a human comment. A player can legitimately hold all three at once with different values, and each row's `sources` array names which feeds contributed it. 34,810 weekly-report rows cover 2020-2026, all 32 teams and every regular-season week plus the postseason rounds. Omit season to get the newest week available; meta.resolved names the season and week actually served, and meta.data_as_of is the newest updated_at in the response, which sits in the past between seasons because nothing new is published then. ROSTER STATUS IS CURRENT-INSTANT ONLY - it is attached to the newest published week and nothing else. Ask for an older season or week and every row's `roster` is null and meta.sources.roster_status.in_scope is false with a reason, rather than the field being quietly absent. Rows carrying roster status but no weekly report entry have player_id null, because player_id on this endpoint is an NFL gsis-format id and those rows have none; their identity is roster.bbs_player_id when we hold one, and otherwise their name and team. Each row's `sources` array names the feeds by WHAT THEY ANSWER - weekly_report, roster_status - never by supplier.",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1999,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "week",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 22
            }
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 4
            }
          },
          {
            "in": "query",
            "name": "player_id",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 20
            }
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "REG",
                "POST",
                "WC",
                "DIV",
                "CON",
                "SB"
              ]
            }
          },
          {
            "in": "query",
            "name": "status",
            "description": "GAME DESIGNATION, matched case-insensitively (e.g. Out, Questionable). Filters the weekly report only, so rows that carry roster status and no report entry are excluded.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            }
          },
          {
            "in": "query",
            "name": "roster_status",
            "description": "ROSTER / RESERVE-LIST status, matched case-insensitively and EXACTLY (e.g. IR, IR-R, PUP-R, NFI-R, Reserve-Sus, Reserve-DNR, Questionable, Out). A separate axis from status: IR does not match IR-R. Only meaningful on the newest published week.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            }
          },
          {
            "in": "query",
            "name": "position",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 6
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "resolved": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "season": {
                              "type": "integer"
                            },
                            "week": {
                              "type": "integer",
                              "nullable": true
                            },
                            "defaulted": {
                              "type": "boolean"
                            }
                          }
                        },
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "sources": {
                          "type": "object",
                          "description": "Per-feed row count and currency, so a caller can see which half of the response is stale rather than inferring it. `roster_status.in_scope` is false, with a reason, whenever the requested window is not the newest published week.",
                          "properties": {
                            "weekly_report": {
                              "type": "object",
                              "properties": {
                                "rows": {
                                  "type": "integer"
                                },
                                "as_of": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                }
                              }
                            },
                            "roster_status": {
                              "type": "object",
                              "properties": {
                                "rows": {
                                  "type": "integer"
                                },
                                "as_of": {
                                  "type": "string",
                                  "format": "date-time",
                                  "nullable": true
                                },
                                "in_scope": {
                                  "type": "boolean"
                                },
                                "reason": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        },
                        "merge": {
                          "type": "object",
                          "description": "How many rows each combination of feeds produced. Present only when roster status is in scope.",
                          "properties": {
                            "both": {
                              "type": "integer"
                            },
                            "report_only": {
                              "type": "integer"
                            },
                            "roster_only": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "injuries",
        "x-bbs-not-available-for-sports": [
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nfl/depth-charts": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "NFL depth charts, current or as at a past instant",
        "description": "Interval-shaped depth chart - one row per player per position per contiguous stretch of state. Defaults to the open intervals; pass as_of to read the chart as it stood at that instant. An open interval is one that has not been closed, which is not the same as \"today's chart\" - no 2026 charts have been published yet, so the open intervals are currently the final 2025 chart. meta.season_covered names the season the rows actually describe and meta.data_as_of their age. Depth charts are available from 2025 only, so this covers one season, not the six carried by the other NFL endpoints.",
        "parameters": [
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 4
            }
          },
          {
            "in": "query",
            "name": "position",
            "description": "position abbreviation (e.g. QB, WR)",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 6
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1999,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "player_id",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 20
            }
          },
          {
            "in": "query",
            "name": "as_of",
            "description": "RFC3339 instant; omit for the current chart",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "open_intervals_only": {
                          "type": "boolean"
                        },
                        "season_covered": {
                          "type": "integer",
                          "nullable": true
                        },
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "depth charts",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "ice_hockey",
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nfl/team-stats": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "NFL team-week totals - offence, defence, kicking, returns and fumbles",
        "description": "Passing, rushing and receiving totals per team per week, with EPA and CPOE, plus kicking, return and fumble blocks. 3,386 rows covering 2020-2025 - 544 per 17-game season, which is every team in every game. Omit season to get the newest available; meta.resolved names what was served.\nNO DERIVED `turnovers` FIELD. Sum passing.interceptions with whichever offensive fumbles-lost columns your scoring rules use. fumbles.lost_total is NOT that number - it also counts return fumbles, which the offence is not charged with.\nNULL AND 0 DIFFER. kicking.fg_long is null when every attempt missed, because the stat records the longest MADE kick - absent, not zero. A null on a season that predates a load means not-loaded, never zero.\n`fg_pct` is deliberately not published - it is derivable from fg_made and fg_att, and the stored value is a fraction in [0,1] despite the name.\nDEFENSE. Each row carries a `defense` object: what this team's defence did in the game, derived from the OPPONENT's row in the same game, this team's summed defender credits and the final score. It is not a separate dataset, so it is available exactly where the row is. Sacks, sack yards, interceptions and yards allowed come from the opponent's offensive totals rather than summed defender credits, because a sack with no defender named is still a sack. Every defense field is null, not 0, when its input has not loaded yet (for example the opponent's row for a game still being processed).\npassing_yards_allowed is NET: the opponent's passing.yards is gross, and the yards its passers lost to sacks are subtracted. total_yards_allowed is passing_yards_allowed plus rushing_yards_allowed.\nTHREE DEFENSIVE FIELDS ARE NOT PUBLISHED. fumble_recoveries: no stored data records who recovered a fumble. safeties: about a third of safeties (33 of 92 in 2020-2025) are team safeties from penalties enforced in the end zone, which no defender is credited with, so a credited count would silently miss them. defensive_tds: the stored defender touchdown count includes interception returns only, never fumble returns, so it is published as interception_return_tds and not as a defensive touchdown total.",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1999,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "week",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 22
            }
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 4
            }
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "REG",
                "POST",
                "WC",
                "DIV",
                "CON",
                "SB"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "team": {
                            "type": "string",
                            "example": "ARI"
                          },
                          "opponent_team": {
                            "type": "string",
                            "nullable": true,
                            "example": "LAC"
                          },
                          "defense": {
                            "type": "object",
                            "description": "This team's defence in the game, derived from the opponent's row. Every field is null when its input has not loaded.",
                            "properties": {
                              "sacks": {
                                "type": "number",
                                "nullable": true,
                                "example": 3,
                                "description": "Opponent's sacks suffered. A number because a sack can be shared."
                              },
                              "sack_yards": {
                                "type": "number",
                                "nullable": true,
                                "example": 22,
                                "description": "Yards the opponent's passers lost to sacks, as a positive number."
                              },
                              "interceptions": {
                                "type": "integer",
                                "nullable": true,
                                "example": 1,
                                "description": "Opponent's interceptions thrown."
                              },
                              "forced_fumbles": {
                                "type": "integer",
                                "nullable": true,
                                "example": 1,
                                "description": "Fumbles forced, summed over this team's defenders. Forcing is not recovering."
                              },
                              "interception_return_tds": {
                                "type": "integer",
                                "nullable": true,
                                "example": 0,
                                "description": "Interception-return touchdowns by this team's defenders. Fumble-return touchdowns are not included."
                              },
                              "blocked_kicks": {
                                "type": "integer",
                                "nullable": true,
                                "example": 1,
                                "description": "blocked_punts plus blocked_field_goals plus blocked_extra_points; null if any part is null."
                              },
                              "blocked_punts": {
                                "type": "integer",
                                "nullable": true,
                                "example": 1,
                                "description": "Punts blocked, summed over this team's defenders."
                              },
                              "blocked_field_goals": {
                                "type": "integer",
                                "nullable": true,
                                "example": 0,
                                "description": "The opponent's field-goal attempts that were blocked."
                              },
                              "blocked_extra_points": {
                                "type": "integer",
                                "nullable": true,
                                "example": 0,
                                "description": "The opponent's extra-point attempts that were blocked."
                              },
                              "points_allowed": {
                                "type": "integer",
                                "nullable": true,
                                "example": 14,
                                "description": "The opponent's final score, all phases. Null until the game has a final score."
                              },
                              "passing_yards_allowed": {
                                "type": "integer",
                                "nullable": true,
                                "example": 187,
                                "description": "Net of sack yardage."
                              },
                              "rushing_yards_allowed": {
                                "type": "integer",
                                "nullable": true,
                                "example": 81
                              },
                              "total_yards_allowed": {
                                "type": "integer",
                                "nullable": true,
                                "example": 268,
                                "description": "passing_yards_allowed plus rushing_yards_allowed."
                              }
                            }
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "resolved": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "season": {
                              "type": "integer"
                            },
                            "defaulted": {
                              "type": "boolean"
                            }
                          }
                        },
                        "data_as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "team stats",
        "x-bbs-not-available-for-sports": [
          "basketball",
          "ice_hockey",
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nfl/rosters": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "NFL season rosters",
        "description": "Season rosters for all 32 teams, 2020 through 2026: 21,795 rows.\nKeyed on the same gsis-format `player_id` as\n/v1/nfl/players/{id}/stats, so joining weekly statistics to a season\nroster needs no id reconciliation.\n\nSCOPE: a row is the published snapshot for that SEASON, not a\npoint-in-time roster for a given week. `status` carries the roster\ndesignation as published (ACT, RES, DEV, CUT, ...). 2026 rows exist and\ndescribe the upcoming season.\n\nOmitting `season` resolves to the newest season present; the resolved\nvalue and whether it was defaulted are echoed in `meta.resolved`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1999,
              "maximum": 2100
            },
            "description": "Season LABEL: the year the season starts in. Defaults to the newest season present."
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 4
            },
            "description": "Team abbreviation, case-insensitive (e.g. DET)."
          },
          {
            "in": "query",
            "name": "position",
            "schema": {
              "type": "string",
              "maxLength": 4
            },
            "description": "Position code, case-insensitive (e.g. QB)."
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "maxLength": 40
            },
            "description": "Roster status as published (ACT, RES, DEV, ...)."
          },
          {
            "in": "query",
            "name": "player_id",
            "schema": {
              "type": "string"
            },
            "description": "NFL player id (gsis format), e.g. \"00-0033106\"."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "season": {
                            "type": "integer",
                            "example": 2025
                          },
                          "team": {
                            "type": "string",
                            "example": "DET"
                          },
                          "player_id": {
                            "type": "string",
                            "example": "00-0033106"
                          },
                          "player_name": {
                            "type": "string",
                            "example": "Jared Goff"
                          },
                          "position": {
                            "type": "string",
                            "nullable": true,
                            "example": "QB"
                          },
                          "depth_chart_position": {
                            "type": "string",
                            "nullable": true
                          },
                          "jersey_number": {
                            "type": "integer",
                            "nullable": true
                          },
                          "status": {
                            "type": "string",
                            "nullable": true,
                            "example": "ACT"
                          },
                          "years_exp": {
                            "type": "integer",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "resolved": {
                          "type": "object",
                          "properties": {
                            "season": {
                              "type": "integer",
                              "nullable": true
                            },
                            "defaulted": {
                              "type": "boolean"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid query"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "rosters",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "ice_hockey",
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nfl/players/{id}/stats": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "Weekly NFL player stats",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "NFL player id, gsis format (e.g. \"00-0019596\")",
            "schema": {
              "type": "string",
              "pattern": "^\\d{2}-\\d{7}$"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1999,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NflPlayerWeekStat"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid player id format"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/live-stats/{sport}/{matchId}/players": {
      "get": {
        "tags": [
          "live-stats"
        ],
        "summary": "Live per-player boxscore",
        "description": "Per-player stat groups for one match, during the game and after it\nfinishes. One response shape for every sport listed below; cached 15s.\n\nCOVERAGE, PER SPORT, STATED RATHER THAN IMPLIED. These sports are not\nat parity and this endpoint does not pretend they are.\n\n- `baseball`: broad demonstrated coverage.\n- `basketball`, `ice_hockey`: covered for the primary professional\n  league only (NBA, NHL). Other leagues return a 400 naming the league.\n- `football`: soccer, and only the mapped competitions (EPL, MLS,\n  FIFA World Cup 2026). Anything else returns a 400 naming the league.\n- `american_football`: **NFL and NCAAF (FBS) only**, from the 2026\n  season opener. NCAAF FCS matches are a valid request that will keep\n  returning an empty box score: the live feed does not poll that\n  division, and naming it here is cheaper than letting a caller\n  discover it by waiting. Before kickoff of any covered game the\n  request also succeeds with `data.teams: []`, `meta.stale: true` and\n  a `meta.note` saying so. It is NOT a 404: a match with no rows yet\n  is a correct request whose data has not been written, and the two\n  are different answers.\n\nFRESHNESS. `meta.as_of` is the timestamp of the most recent stat cell\nin the response, and `meta.stale` says whether we would expect to have\nlearned something more recently than that. A box score that has stopped\nupdating mid-game and one that is simply quiet are indistinguishable\nwithout those two fields, which is why they are here rather than left\nfor the caller to infer from `fetched_at`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "sport",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "basketball",
                "baseball",
                "ice_hockey",
                "football",
                "american_football"
              ]
            }
          },
          {
            "in": "path",
            "name": "matchId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "match_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "sport": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "in_progress",
                            "finished"
                          ]
                        },
                        "teams": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "team_name": {
                                "type": "string",
                                "nullable": true
                              },
                              "stat_groups": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "type": {
                                      "type": "string",
                                      "nullable": true
                                    },
                                    "keys": {
                                      "type": "array",
                                      "items": {
                                        "type": "string"
                                      }
                                    },
                                    "players": {
                                      "type": "array",
                                      "items": {
                                        "type": "object",
                                        "properties": {
                                          "player_id": {
                                            "type": "string",
                                            "nullable": true,
                                            "format": "uuid",
                                            "example": "0905f056-26ed-48d0-b6b2-ca851a8836ca",
                                            "description": "Big Balls player UUID, resolvable against /v1/players/{id}. Null when the player is not yet linked to a player record; `name` is still populated."
                                          },
                                          "name": {
                                            "type": "string",
                                            "nullable": true
                                          },
                                          "starter": {
                                            "type": "boolean",
                                            "nullable": true,
                                            "description": "Null on `american_football`; that path does not carry it, and a guessed `false` would be worse than an honest null."
                                          },
                                          "position": {
                                            "oneOf": [
                                              {
                                                "type": "null"
                                              },
                                              {
                                                "type": "object",
                                                "properties": {
                                                  "id": {
                                                    "type": "string"
                                                  },
                                                  "name": {
                                                    "type": "string",
                                                    "example": "Second Base"
                                                  },
                                                  "displayName": {
                                                    "type": "string",
                                                    "example": "Second Baseman"
                                                  },
                                                  "abbreviation": {
                                                    "type": "string",
                                                    "example": "2B"
                                                  }
                                                }
                                              }
                                            ],
                                            "description": "Null on `american_football`, for the same reason as `starter`. Elsewhere an object carrying the position's id, name, display name and abbreviation. Measured 2026-09-25 over the two sports that had any players on the day: 67 of 67 `american_football` rows null, 31 of 31 `baseball` rows the four-field object. `basketball`, `ice_hockey` and `football` returned 0 players, so they are UNMEASURED here, not confirmed; no `required` is declared and extra keys are permitted, because this value is passed through from the upstream boxscore."
                                          },
                                          "stats": {
                                            "type": "array",
                                            "items": {
                                              "type": "string"
                                            },
                                            "description": "Positionally aligned to the group's `keys`, one entry per key, with an empty string where this player has no value for that key. Index into it by the position of the key you want; never assume a shorter array is truncated rather than shifted."
                                          }
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "source": {
                          "type": "string",
                          "example": "aggregator-free"
                        },
                        "fetched_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        },
                        "as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "Timestamp of the freshest stat cell in this response, or null when the response carries none. Distinct from `fetched_at`, which is when WE answered."
                        },
                        "stale": {
                          "type": "boolean",
                          "description": "True when `as_of` is null or older than 120 seconds. A stale response is still served; this is a disclosure, not a refusal."
                        },
                        "note": {
                          "type": "string",
                          "description": "Present only when `teams` is empty, explaining in plain English that no per-player rows have been written for this match yet and when coverage begins."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "unsupported sport, sport/match mismatch, or an uncovered league"
          },
          "404": {
            "description": "match not found, or no live-stats coverage for it. An empty box score is NOT a 404; see the 200 above."
          },
          "502": {
            "description": "upstream failure"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/live-stats/american_football/{matchId}/plays": {
      "get": {
        "tags": [
          "live-stats"
        ],
        "summary": "Live NFL/NCAAF play-by-play",
        "description": "Descriptive play-by-play for one match, during the game and after it\nfinishes. NFL, NCAAF and NCAAF FCS. **Pro plan or higher**. The shared\nNFL play-by-play gate protects this route alongside the\n`american_football` sport entitlement.\n\nSOURCE, STATED RATHER THAN LEFT TO AN ORDERING. `football_plays`\ncarries rows from two vendors; this endpoint serves ONLY the rows\nfrom our licensed paid aggregator. The other vendor's rows exist in\nstorage as derivation input and are never served; that vendor's\nlicence is `redistribute: no`, while the paid aggregator's\nall-sports redistribution grant (2026-09-09) covers this table's\nrows outright.\n\nDIFFERENT TABLE FROM `GET /v1/nfl/games/{gameId}/plays`. That endpoint\nreads `public_nfl_plays` (EPA/WP, NFL only, rebuilt nightly, keyed\non the league-standard text game id). This one reads\n`football_plays` (keyed on the same match UUID every other\n`/v1/live-stats/*` and `/v1/matches/*` endpoint uses), sourced from\nthe paid aggregator and updated while a game is in progress. The two\nare never blended in one response; ask for both if you want both.\n\nFRESHNESS. `meta.as_of` is the timestamp the freshest play last\nchanged; `meta.stale` says whether we would expect to have learned\nsomething more recently than that. A feed that has stopped updating\nmid-game and one that is simply quiet are indistinguishable without\nthose two fields.\n",
        "parameters": [
          {
            "in": "path",
            "name": "matchId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "scoring_only",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Return only plays where `scoring_play` is true. Accepted: true|1|yes|on and false|0|no|off; anything else is a 400 naming the parameter. false means OFF. Until 2026-09-20 every spelling of false was read as true, so opting OUT of the filter turned it ON."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "match_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "sport": {
                          "type": "string",
                          "example": "american_football"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "scheduled",
                            "in_progress",
                            "finished"
                          ]
                        },
                        "plays": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "sequence_number": {
                                "type": "integer",
                                "nullable": true
                              },
                              "period": {
                                "type": "integer",
                                "nullable": true
                              },
                              "clock": {
                                "type": "string",
                                "nullable": true
                              },
                              "play_type": {
                                "type": "string",
                                "nullable": true
                              },
                              "description": {
                                "type": "string",
                                "nullable": true
                              },
                              "down": {
                                "type": "integer",
                                "nullable": true
                              },
                              "distance": {
                                "type": "integer",
                                "nullable": true
                              },
                              "yards_to_endzone": {
                                "type": "integer",
                                "nullable": true
                              },
                              "yards_gained": {
                                "type": "integer",
                                "nullable": true
                              },
                              "end_down": {
                                "type": "integer",
                                "nullable": true
                              },
                              "end_distance": {
                                "type": "integer",
                                "nullable": true
                              },
                              "end_yards_to_endzone": {
                                "type": "integer",
                                "nullable": true
                              },
                              "kick_distance": {
                                "type": "integer",
                                "nullable": true,
                                "description": "Attempted field-goal distance in yards, on field-goal plays only. Null on every other play type, and null on a field goal whose text does not state a distance: an absence, never 0. Where a play describes a blocked kick returned by the defence, the returned yardage is NOT reported here: that number is the return, not the kick."
                              },
                              "field_goal_result": {
                                "type": "string",
                                "nullable": true,
                                "enum": [
                                  "made",
                                  "missed",
                                  "blocked",
                                  null
                                ],
                                "description": "Outcome of a field-goal attempt. `blocked` is kept distinct from `missed` because a blocked kick is not a miss the kicker owns. Null on every play that is not a field-goal attempt. `null` is listed in the enum as well as in `nullable`: an enum without it forbids exactly the value the sentence above promises, and that is the common case (176 of 179 plays on the 2026-09-25 sample)."
                              },
                              "kicker_player_id": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true,
                                "description": "BigBalls player id of the kicker on a field-goal or extra-point attempt, the same id `/v1/players/{id}` and the post-game surfaces use, so the live feed and the box score join on an id instead of on a name.\nNULL IS COMMON AND EXPECTED; DO NOT ASSUME 100% COVERAGE. Participant data is written on roughly 84% of plays (measured 2026-09-13: 81.5-85.8% per match across 11 matches), so this is null on the remaining ~16% even for plays whose text plainly describes a kick. It is also null when the play names no kicker, when the individual resolves to no player we carry, and when the id would be ambiguous; in that last case we return null deliberately rather than choose between two players. Treat null as \"not stated\", never as \"no kicker\", and do not fall back to parsing the name out of `description`: name matching is precisely the failure this field exists to remove.\nPrecedence when both roles appear on one play: `field_goal_kicker` wins over `xp_kicker`. Punters and kickoff specialists are NOT folded in here; they appear in `participants` under their own roles."
                              },
                              "participants": {
                                "type": "array",
                                "description": "Everyone the play names, each as a role plus a BigBalls player id: passer, rusher, receiver, tackler and the rest, on the same terms as `kicker_player_id`.\nAlways present, but EMPTY on the ~16% of plays that carry no participant data, so `[]` means \"not stated\", not \"nobody was involved\". A member's `player_id` is null when that individual resolves to no player we carry, or would be ambiguous.\nRoles observed to date: passer, rusher, receiver, tackler, sacker, fumbler, recoverer, interception_returner, penalized, field_goal_kicker, xp_kicker, kickoff_kicker, kickoff_returner, punter, punt_returner. Treat the set as open; new roles may appear without a version bump.",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "role": {
                                      "type": "string",
                                      "example": "field_goal_kicker"
                                    },
                                    "player_id": {
                                      "type": "string",
                                      "format": "uuid",
                                      "nullable": true
                                    }
                                  }
                                }
                              },
                              "scoring_play": {
                                "type": "boolean"
                              },
                              "home_score": {
                                "type": "integer",
                                "nullable": true
                              },
                              "away_score": {
                                "type": "integer",
                                "nullable": true
                              },
                              "home_win_probability": {
                                "type": "number",
                                "nullable": true,
                                "description": "NFL/paid-aggregator only, and only from the 2018 season. Null is absence, never 0.5."
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "source": {
                          "type": "string",
                          "example": "live-stats"
                        },
                        "fetched_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        },
                        "as_of": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "stale": {
                          "type": "boolean"
                        },
                        "count": {
                          "type": "integer"
                        },
                        "note": {
                          "type": "string",
                          "description": "Present only when `plays` is empty, explaining that no play-by-play has been written for this match yet."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid matchId, or sport mismatch (this feed only serves american_football)"
          },
          "403": {
            "description": "plan does not include NFL play-by-play, or sport not entitled"
          },
          "404": {
            "description": "match not found. An empty plays array is NOT a 404; see the 200 above."
          }
        },
        "x-bbs-min-plan": "pro",
        "x-bbs-capability": "play-by-play",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "ice_hockey",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/f1/sessions/{id}/results": {
      "get": {
        "tags": [
          "formula1"
        ],
        "summary": "Finishing positions for a Formula 1 session",
        "description": "Returns per-driver finishing positions for a single session (practice,\nqualifying, sprint, or race). The underlying `f1_session_results` table\nis populated by a periodic worker that fetches the upstream\n`/sessions` + `/drivers` + `/position` + `/laps` chain (cron:\na scheduled results cron).\n\nSessions queried before the worker has processed them return a 200\nwith an empty `data` array, distinguish \"scheduled or freshly\nfinished\" from \"doesn't exist\" by checking `session_type` /\n`status` via `GET /v1/sessions` (GraphQL `Query.sessions`).\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "Session canonical id (`bb_session_…`).",
            "schema": {
              "$ref": "#/components/schemas/CanonicalId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "maxItems": 40,
                          "items": {
                            "$ref": "#/components/schemas/SessionResult"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid session id (must match `bb_session_<…>`)."
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/games": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Cross-sport games strip with live scores + odds",
        "description": "Intelligence endpoint for the top-of-page game ribbon. Sport-scoped\nwith hard caps (~15-20 games per sport per day). 30s Redis cache.\nLegacy alias: `GET /v1/nerd/games` (deprecated, sunset 2026-09-01).\n\n**One of `sport` or `league` is required** (unlike `/v1/matches`, which\ndefaults to an unfiltered list). `sport` accepts canonical sport slugs\n(`american_football`, `basketball`, `football`, ...) and brand aliases\n(`nfl`, `nba`, `nhl`, ...). A brand alias narrows to ONE league; a\ncanonical slug covers every league of that sport, so the two return\ndifferent row counts by design. `filters_applied.league` says which\nhappened: the league name for a brand alias, `null` for a whole sport.\n\nEach game's `sport` field carries the row's canonical sport slug, in\nthe same vocabulary `?sport=` accepts, so a value taken from a\nresponse can be fed straight back into a request.\n\n`?season=` is **not** supported and returns 400; filter by `?date=`\n(a single day or a `:`-separated range) instead.\n",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string",
              "example": "american_football"
            },
            "description": "Canonical sport slug or brand alias. Required unless ?league= is given. Unknown values return 400 listing the accepted forms."
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string",
              "example": "nfl"
            },
            "description": "League code or canonical league name. Required unless ?sport= is given. Passing both returns 400 if they are inconsistent.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string"
            },
            "description": "YYYY-MM-DD, an inclusive YYYY-MM-DD:YYYY-MM-DD range (max 31 days), 'today', 'upcoming' (next 7 days) or 'recent' (last 7 days, finished only). Default today. Calendar days are UTC days: an evening game in the Americas often kicks off after 00:00Z and is listed under the NEXT date. For calendar-day values the exact window applied is echoed in meta.date_window."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. America/New_York). Calendar-day values (a date, a range, today, and the default) then use local days in that zone: local midnight at both ends, converted to UTC. 'upcoming' and 'recent' are unaffected. Offsets such as -05:00 are rejected with 400. Example: ?sport=nba&date=today&tz=America/New_York."
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "live",
                "final"
              ]
            }
          },
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query; also returned for ?season=, which this endpoint does not support, and when neither ?sport= nor ?league= is given"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/games/{id}/books": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Multi-book line shopping for one match (sport-agnostic)",
        "description": "Every sportsbook with current lines on this match across moneyline +\nspread + total, joined to books_reference for display name and logo.\nBooks sorted by display_name. 30s Redis cache. Empty `books: []` (not\n404) when the match exists but has no odds rows.\nLegacy alias: `GET /v1/nerd/games/{id}/books` (sunset 2026-09-01).\n**Edge plan or higher**: odds access starts at Edge; Free / Solo / Trio\nreceive a 403 naming the tier and linking the docs and pricing pages.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid game_id"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          },
          "404": {
            "description": "match not found"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/games/{id}/line-movement": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "5-minute snapshot timeline of moneyline + spread + total",
        "description": "Line-movement chart data attributed to the freshest single sportsbook\n(switching mid-window would produce visually noisy jumps). 5-min\nbuckets, denormalized for one-response three-chart rendering.\nLegacy alias: `GET /v1/nerd/games/{id}/line-movement` (sunset 2026-09-01).\n**Edge plan or higher**: odds access starts at Edge; Free / Solo / Trio\nreceive a 403 naming the tier and linking the docs and pricing pages.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid game_id"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          },
          "404": {
            "description": "match not found"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "odds",
        "x-bbs-not-available-for-sports": [
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/wc2026/standings": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 group-stage standings with Elo + advancement probability",
        "description": "Live 12-group table augmented with Elo + cached Monte Carlo advancement\nprobability. Form is last two finished group-stage results. 5-min cache.\nLegacy alias: `GET /v1/nerd/wc2026/standings` (sunset 2026-09-01).\n",
        "parameters": [
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "standings",
        "x-bbs-not-available-for-sports": [
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/wc2026/matches": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "All 72 WC2026 group-stage matches with Elo prediction + odds",
        "description": "Filterable by group (A-L) and status (upcoming/live/final). Odds null\nuntil the upstream odds feed activates the tournament. 5-min cache.\nLegacy alias: `GET /v1/nerd/wc2026/matches` (sunset 2026-09-01).\n\n`elo_prediction` carries its own freshness: `valid_until`, `expired`\n(true / false / null when no expiry is stated) and `expired_days`.\nMeasured 2026-09-28, all 72 `match_result` predictions are past their\nvalid_until, the newest by 92 days, and these routes previously carried\nno expiry at all; so the block read as a forecast for a match already\nplayed. Nothing is filtered; the block is disclosed, not withheld.\n",
        "parameters": [
          {
            "in": "query",
            "name": "group",
            "schema": {
              "type": "string",
              "pattern": "^[A-L]$"
            }
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "live",
                "final"
              ]
            }
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "description": "ISO date (YYYY-MM-DD) of kickoff, as a UTC calendar day unless ?tz= is set. The exact window applied is echoed in meta.date_window."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. America/Mexico_City). ?date= then selects the local calendar day in that zone. Offsets such as -05:00 are rejected with 400. Example: ?date=2026-06-11&tz=America/Mexico_City."
          },
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/wc2026/matches/{id}": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 match-detail intelligence endpoint (lineups, shots+xG, momentum, etc.)",
        "description": "Match meta + Elo prediction + odds + every ingested detail section.\nSections are null when that section has not been ingested for this\nmatch yet, or came back empty. 60s cache.\nEvery `player_id`, `team_id` and `match_id` inside the sections\n(lineups, player_match_stats, team_match_stats, best_players,\navg_positions, pre_match_form, momentum) is a Big Balls UUID. The\n`recent_match_history` and `ranked_form_metrics` sections are also\nserved: `recent_match_history` contains each side's recent\nfinished-match sequence, `ranked_form_metrics` contains each side's\nranked, form-derived indicators, and `pre_match_form` remains the v1\ncompatibility alias (ranked metrics when present, otherwise\nrecent-match history); their ids are Big Balls UUIDs too. A\n`player_id` is null when the player is not yet linked to a player\nrecord; `player_name` is still populated on lineups.\nLegacy alias: `GET /v1/nerd/wc2026/matches/{id}` (sunset 2026-09-01).\n\n`elo_prediction` carries its own freshness: `valid_until`, `expired`\n(true / false / null when no expiry is stated) and `expired_days`.\nMeasured 2026-09-28, all 72 `match_result` predictions are past their\nvalid_until, the newest by 92 days, and these routes previously carried\nno expiry at all; so the block read as a forecast for a match already\nplayed. Nothing is filtered; the block is disclosed, not withheld.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "additionalProperties": true,
                      "properties": {
                        "recent_match_history": {
                          "type": "object",
                          "nullable": true,
                          "description": "Home and away recent finished-match history."
                        },
                        "ranked_form_metrics": {
                          "type": "object",
                          "nullable": true,
                          "description": "Home and away ranked form-derived metrics."
                        },
                        "pre_match_form": {
                          "type": "object",
                          "nullable": true,
                          "description": "Compatibility alias. Ranked form metrics when available, otherwise recent match history."
                        }
                      }
                    }
                  },
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "invalid game_id"
          },
          "404": {
            "description": "match not found"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/wc2026/teams/{id}": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 nation profile (Elo, rank, recent form, group matches, squad)",
        "description": "Team meta + Elo + global rank + advancement probability + last 10\ninternationals + 3 group matches + squad. 5-min cache.\nLegacy alias: `GET /v1/nerd/wc2026/teams/{id}` (sunset 2026-09-01).\n\nEach `group_matches[].elo_prediction` carries its own freshness:\n`valid_until`, `expired` (true / false / null when no expiry is stated)\nand `expired_days`. Measured 2026-09-28, all 72 `match_result`\npredictions are past their valid_until, the newest by 92 days, and this\nroute previously carried no expiry at all. Nothing is filtered.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid team_id"
          },
          "404": {
            "description": "team not found"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/nba/games/{id}/matchup": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "NBA matchup intelligence endpoint (season aggregates, last-5, rosters, H2H)",
        "description": "Match meta + live state + per-team season aggregates + last-5 +\nrest days + active-roster averages + H2H. 60s cache.\nLegacy alias: `GET /v1/nerd/nba/games/{id}/matchup` (sunset 2026-09-01).\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid game_id"
          },
          "404": {
            "description": "NBA match not found"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/nba/games/{id}/lineups": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "NBA game lineups (home/away players with position + starter flag)",
        "description": "Home and away lineups for one NBA game from the lineups feed.\nEach side is a flat array ordered starters-first; split starters from\nbench on the `starter` boolean (there is no separate team object or\nstarters/bench split). `id` accepts our match UUID or the numeric NBA\ngame id. Coverage fills in as the backfill runs: a real game with no ingested\nlineups yet returns empty arrays, not 404. Lineups exist 2025-26 onward\nonly; for historical \"who played\" data (2019-20 onward) use\n/v1/nba/games/{id}/availability.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Match UUID or numeric NBA game id"
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "home": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "player_id": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true,
                                "description": "Null when the upstream player is not yet bridged to a BigBalls player"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true,
                                "description": "Bridged player name, else the raw first + last as ingested"
                              },
                              "position": {
                                "type": "string",
                                "nullable": true
                              },
                              "starter": {
                                "type": "boolean"
                              }
                            }
                          }
                        },
                        "away": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "player_id": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              },
                              "position": {
                                "type": "string",
                                "nullable": true
                              },
                              "starter": {
                                "type": "boolean"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid game id (must be a match uuid or numeric NBA game id)"
          },
          "404": {
            "description": "NBA game not found"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "lineups",
        "x-bbs-not-available-for-sports": [
          "american_football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nba/players/{id}/props": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "NBA player prop hit-rate + current line across books",
        "description": "**Requires the Edge plan or higher.** Player props are part of the\nodds and model layer; Free, Solo and Trio are refused with `403\nforbidden`. Moved to Edge on 2026-09-25; previously free.\n\nFor one market (points/rebounds/assists/points_rebounds_assists/threes),\nreturns the current line picked by tightest juice plus rolling-last-N\nhit rate from nba_player_box_scores. 60s cache. current_line null when\nno nba_player_props row exists; hit_rate null when no box-score history.\nLegacy alias: `GET /v1/nerd/nba/players/{id}/props` (sunset 2026-09-01).\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "market",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "points",
                "rebounds",
                "assists",
                "points_rebounds_assists",
                "threes"
              ]
            }
          },
          {
            "in": "query",
            "name": "lookback",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 5,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "book",
            "schema": {
              "type": "string",
              "description": "Sportsbook short-name (e.g. betrivers). Omit for tightest-juice pick."
            }
          },
          {
            "in": "query",
            "name": "view",
            "deprecated": true,
            "schema": {
              "type": "string",
              "enum": [
                "nerd"
              ]
            },
            "description": "Accepted and IGNORED. No handler reads `view`; the response is byte-identical with and without it (verified 2026-09-17 against production on /v1/games, /v1/wc2026/matches and /v1/wc2026/standings, modulo `meta.cache_age_ms`). Kept in the contract, and still tolerated by the routes, only so existing callers that send it are not broken, but do not build on it."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query or player_id"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          },
          "404": {
            "description": "player not found"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "player props",
        "x-bbs-not-available-for-sports": [
          "football",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nhl/players/{player_id}/props": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "NHL player prop hit-rate + latest line across books",
        "description": "**Requires the Edge plan or higher.** Player props are part of the\nodds and model layer; Free, Solo and Trio are refused with `403\nforbidden`. Moved to Edge on 2026-09-25; previously free.\n\nFor one market (shots_on_goal/goals/assists/points; saves and\npower_play_points have no game-log field, so they return a real\ncurrent_line with a null hit_rate), returns the latest line we hold picked by tightest\njuice plus rolling-last-N hit rate from player_stats (box\nstats). 60s cache. current_line null when no nhl_player_props row\nexists; hit_rate null when the player has no stats bridge or no\nhistory.\n\n**Check `freshness` before treating a line as a current price.** Every\nresponse carries a `freshness` object DERIVED from the returned line's\nown `fetched_at`, never from the clock: `data_as_of` (the line's real\ntimestamp), `age_days`, and `status` (`live` ≤2d, `recent` ≤14d,\n`stale` >14d, or `no_data` when no line exists). The top-level `as_of`\nis when the RESPONSE was built and says nothing about the line's age.\nBetween seasons the newest lines can be months old and will report\n`stale`; the historical lines and the hit rate computed over them\nremain valid.\n\n**`milestones`: yes/no prop prices, separate from `current_line`.**\nA milestone prop asks whether a player reaches a threshold at all, so it\nis priced as ONE American `odds` integer, not an over/under pair. Each\nentry is `{ sportsbook, display, line, odds, fetched_at }`, one per\n(book, threshold): `goals` with `line: 0.5` is the Anytime Goal Scorer\nprice, `1.5` is 2+ goals, and so on.\n\nThese are strictly ADDITIVE. `current_line`, `current_line.all_books`\nand the `hit_rate` line are computed from over/under rows ALONE, so a\nmilestone price can never appear as a line: the query keys on\n`market_type` rather than leaving it to a tiebreak. The array is empty\nuntil the milestone half of the board is being stored.\n",
        "parameters": [
          {
            "in": "path",
            "name": "player_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "market",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "shots_on_goal",
                "goals",
                "assists",
                "points",
                "saves",
                "power_play_points"
              ]
            }
          },
          {
            "in": "query",
            "name": "lookback",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 5,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "book",
            "schema": {
              "type": "string",
              "description": "Sportsbook short-name (e.g. betrivers). Omit for tightest-juice pick. Selects the over/under line only; `milestones` always returns every book."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query or player_id"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          },
          "404": {
            "description": "player not found"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "player props",
        "x-bbs-not-available-for-sports": [
          "football",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nhl/props/freshness": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Dataset-wide freshness for NHL player props",
        "description": "Answers \"is the NHL props dataset live right now\" with no player or\nmarket, for a build-time reader (e.g. marketing copy) that cannot\nsupply either. Returns the SAME `freshness` shape documented on\n`GET /v1/nhl/players/{player_id}/props`, derived from\n`MAX(fetched_at)` across every held NHL prop line rather than one\nplayer's line: `data_as_of`, `age_days`, `status`\n(`live` ≤2d, `recent` ≤14d, `stale` >14d, or `no_data` when the table\nis empty), and `note`. A page reading this can never claim `live`\nwhile the per-player endpoint would call the same data `stale`.\n",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player props",
        "x-bbs-not-available-for-sports": [
          "football",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/intelligence/value-props": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Player-prop value finder (model hit-rate vs market) with backtest",
        "description": "Player-prop value finder: flags props where a player's empirical\nover/under hit rate (model probability) beats the de-vigged over/under\njuice (market probability). Edge = model minus market. Two windows:\n`upcoming` (default) flags props on games in the next 24h using a\ntrailing `lookback`-game model; `all` runs a point-in-time historical\nbacktest and populates a `backtest` block (record, ROI, average edge).\nThe crude empirical model flags generously, so treat the backtest as a\ncalibration check, not an edge guarantee. Response object keys: model,\nwindow, min_edge_pct, lookback, min_games, evaluated, value_found,\nbacktest (null for upcoming), props[]. 5-minute Redis cache.\n**Solo plan or higher**: prop value signals start at Solo; Free\nreceives a 403 naming the tier and linking the docs and pricing pages.\n(The `Edge` in \"Edge = model minus market\" above is the statistical\nquantity, not the plan.)\n",
        "parameters": [
          {
            "in": "query",
            "name": "window",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "all"
              ],
              "default": "upcoming"
            },
            "description": "upcoming: next-24h props, live model. all: point-in-time historical backtest."
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string",
              "maxLength": 40
            },
            "description": "Sport the request is about: a sport slug or alias (basketball, cricket, ...) or a league token (nba, wnba, ncaab). Defaults to basketball. Player-prop edges are modelled for basketball only, so another recognised sport returns 200 with an empty `props` list and a `meta.coverage_note`, never basketball rows. An unrecognised value returns 400 `bad_request`. Your key must be entitled to the sport you name."
          },
          {
            "in": "query",
            "name": "min_edge",
            "schema": {
              "type": "number",
              "default": 3,
              "minimum": 0,
              "maximum": 100
            },
            "description": "Minimum model-minus-market edge in percentage points."
          },
          {
            "in": "query",
            "name": "market",
            "schema": {
              "type": "string",
              "enum": [
                "points",
                "rebounds",
                "assists",
                "points_rebounds_assists",
                "threes"
              ]
            },
            "description": "Restrict to one prop market. Omit for all markets."
          },
          {
            "in": "query",
            "name": "lookback",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 5,
              "maximum": 100
            },
            "description": "upcoming: trailing games in the model sample. all: scoring window."
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "default": 10,
              "minimum": 3,
              "maximum": 82
            },
            "description": "Minimum games of history required to flag a prop."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query"
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/intelligence/value-finder": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Match-level value finder (Elo win prob vs implied odds)",
        "description": "Flags upcoming games where the Elo-implied win probability beats the\nde-vigged market price by at least `min_edge` percentage points. WC2026\nplays on neutral sites, so the Elo home-court bump is suppressed and the\nnote is echoed in `neutral_site_note`. Filter by sport/league using brand\naliases (nba, nhl, ...) or canonical slugs (basketball, soccer, ...);\npassing both with a conflict returns 400; omit both for all sports.\nRequires a 2-way American moneyline plus an Elo rating for both teams;\na candidate whose only odds are decimal-format or a 3-way market (with a\ndraw) is reported in `odds_unresolved` rather than force-fit, and an\nempty or partial result explains itself in `coverage_note` (null when\nvalue games were found). Response object keys: model, window,\nmin_edge_pct, sport, evaluated, value_found, odds_unresolved,\nneutral_site_note, coverage_note, games[], generated_at.\n5-minute Redis cache.\n**Edge plan or higher**: model-ready value signals start at Edge; Free\n/ Solo / Trio receive a 403 naming the tier and linking the docs and\npricing pages.\nA sport or league we cannot resolve returns 400 naming the value and\nsuggesting the valid keys, rather than widening to every sport. Until\n2026-10-07 it widened: measured in production that day,\n`?league=ZZZ_NOT_A_LEAGUE` and no parameter at all both returned\nevaluated 217, against 3 for `?league=nba` and 17 for `?league=nhl`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            },
            "description": "Brand alias (nba, nhl, mlb, ...) or canonical slug (basketball, soccer, ...). Omit for all sports. A value we cannot resolve returns 400 naming it, rather than widening to every sport."
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            },
            "description": "Optional league filter; a sport/league conflict returns 400. Canonical league key, brand alias or exact display name, resolved the same way as on /v1/players and /v1/standings. A league we do not carry also returns 400 naming it, rather than widening to every sport, which is what it did until 2026-10-07.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "window",
            "schema": {
              "type": "string",
              "enum": [
                "upcoming",
                "all"
              ],
              "default": "upcoming"
            }
          },
          {
            "in": "query",
            "name": "min_edge",
            "schema": {
              "type": "number",
              "default": 3,
              "minimum": 0,
              "maximum": 100
            },
            "description": "Minimum Elo-minus-market edge in percentage points."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/intelligence/trending": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Hot/cold player movers (last-5 vs season average)",
        "description": "Players whose last-5-game average is at least 15% above (hot) or below\n(cold) their season average for the chosen stat. NBA and NHL today\n(default nba). `stat` must belong to the chosen sport: NBA pts/reb/ast,\nNHL goals/assists/points/shots_on_goal; a cross-sport stat returns 400.\n`min_season_avg` floors the baseline to drop small-denominator noise\n(per-stat default when omitted; pass 0 to opt out). Response object keys:\nstat, direction, threshold_pct, min_season_avg, season, players[],\nas_of. 1-hour Redis cache.\n**Solo plan or higher**: player-movement intelligence starts at Solo;\nFree receives a 403 naming the tier and linking the docs and pricing\npages.\nThe nba default applies only when neither `sport` nor `league` is\nsupplied, so a covered league passed on its own (`?league=nhl`) is\nenough. A league we carry but do not compute trending for, such as\nNCAAB or WNBA, returns 400 naming the league.\n",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "Brand alias nba|nhl or canonical basketball|ice_hockey. Default nba. The default applies only when neither sport nor league is supplied."
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "description": "Optional; a sport/league conflict returns 400. Usable on its own: ?league=nhl alone selects the NHL feed. A league we carry but do not compute trending for (NCAAB, WNBA) returns 400 naming the league. Until 2026-10-07 sport defaulted to nba before the league resolved, so ?league=nhl returned 400 blaming a sport the caller never sent.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "stat",
            "schema": {
              "type": "string",
              "enum": [
                "pts",
                "reb",
                "ast",
                "goals",
                "assists",
                "points",
                "shots_on_goal"
              ]
            },
            "description": "NBA: pts/reb/ast. NHL: goals/assists/points/shots_on_goal. Must match the chosen sport."
          },
          {
            "in": "query",
            "name": "direction",
            "schema": {
              "type": "string",
              "enum": [
                "up",
                "down",
                "both"
              ],
              "default": "both"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "default": 15,
              "minimum": 5,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "min_season_avg",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 100
            },
            "description": "Baseline floor in the chosen stat; pass 0 to opt out."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query"
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/odds": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Tournament-wide odds list (WC2026)",
        "description": "Latest odds snapshot per (match, sportsbook, market, participant) for\nevery match in a tournament, grouped per match. `tournament` is required\n(wc2026 today). Returns 200 with `matches: []` when no odds exist yet, so\nit self-fills as books open their markets. Response object keys:\ntournament, matches[] (match_id, kickoff, status, home, away, odds[]),\ngenerated_at. Each odds[] entry carries sportsbook, market, line (a\nmarket-shaped object), and fetched_at. 60-second Redis cache.\n\n`line.price_format` (`american` | `decimal`) states which number system\n`line.price` is in. The two are stored in one column with no per-row\nflag, so a consumer reading `price` alone had to guess: a whole-number\ndecimal such as `2` or `19` is indistinguishable from an American `+2`\n/ `+19` by inspection, and 900 decimal rows sit at |price| >= 100 where\nmagnitude cannot separate them either. The field is a FORMAT, never a\nprovider.\n**Edge plan or higher**: odds access starts at Edge; Free / Solo / Trio\nreceive a 403 naming the tier and linking the docs and pricing pages.\n",
        "parameters": [
          {
            "in": "query",
            "name": "tournament",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "wc2026"
              ]
            },
            "description": "Tournament key. Required; wc2026 today."
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Filter to a single UTC calendar day (YYYY-MM-DD): kickoffs at or after 00:00Z that date and before 00:00Z the next. The exact window applied is echoed in meta.date_window."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. America/Mexico_City). ?date= then selects the local calendar day in that zone. Offsets such as -05:00 are rejected with 400. Example: ?tournament=wc2026&date=2026-06-11&tz=America/Mexico_City."
          },
          {
            "in": "query",
            "name": "market",
            "schema": {
              "type": "string"
            },
            "description": "Filter to one market (moneyline, handicap, total, ...). Omit for all."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query (tournament required)"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "odds",
        "x-bbs-not-available-for-sports": [
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/predictions": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Match predictions (Elo win/draw/loss + value bet)",
        "description": "**COVERAGE, stated plainly: this is not a per-sport prediction feed.**\nRe-measured 2026-09-28 against production: 276 rows in total, and every\nrow is tied to one match, so the sport of a row is the sport of its\nmatch. By sport, against that denominator of 276; football 176 (72\n`match_result` for **FIFA World Cup 2026** plus 104 `value_prop`),\namerican_football 67, ice_hockey 29, baseball 2, basketball 2. Those\nfive sports are exactly what `meta.covered_sports` names. A plan scoped\nto any OTHER sport receives 403 with the coverage named; a plan scoped\nto one of these five is admitted and sees only its own sports' rows.\nPredictions are written per batch against matches that already carry\nmoneyline odds, not as a continuous per-sport feed, so which of the\nfive sports has rows on a given day follows the odds board. (The\n2026-09-02 figure of 129 rows, and its \"there is no live writer\" note,\nare superseded: the value_prop writer resumed and added 147 rows.)\n\n**FRESHNESS, AND WHY IT IS ON THE RESPONSE.** 129 of those 276 rows\n(46.7%) are past their own `valid_until`; all 72 `match_result` rows\nand 57 of the 204 `value_prop` rows, the oldest by 92 days. They are\n**still returned**; nothing is filtered. Each row carries `valid_until`,\n`expired` (true / false / **null** when the row states no expiry, which\nis not the same as fresh) and `expired_days`, and `meta` carries\n`returned`, `expired`, `expiry_unknown`, `as_of` and; only when at\nleast one row is expired; `stale: true` and a human `message`. An\nexpired row describes a match already played and must not be read as a\nforecast: check `valid_until` per row before using it.\n\nPre-computed Elo-based win / draw / loss probabilities per match, with\nvalue-bet analysis (Elo-implied vs de-vigged market probability) when\nmoneyline odds exist. Filter by `tournament` (e.g. wc2026), `match_id`,\nor `type`. Requires Solo plan or higher (Free receives 403); results are\nadditionally scoped to the sports your key is entitled to.\nResponse array item keys: match_id, prediction_type, home_team,\naway_team, home_win_probability, draw_probability, away_win_probability,\nhome_elo, away_elo, model_version, valid_until, expired, expired_days,\ngenerated_at, kickoff_utc, plus value_bet / best_side / elo_prob /\nmarket_prob / edge (null until moneyline odds land).\n\n**UPDATE 2026-10-09 (correcting forward; the 276-row and 46.7% figures\nabove are a 2026-09-28 snapshot and are left as the record of that\ndate).** Re-measured against the whole table, 642 rows: football 259\n(72 `match_result` plus 187 `value_prop`), american_football 258,\nice_hockey 85, baseball 24, basketball 16. 414 of 642 (64.5%) are past\ntheir own `valid_until`.\n\nTwo corrections to the paragraphs above. First, \"null until moneyline\nodds land\" was the wrong explanation for `value_bet` / `best_side` /\n`elo_prob` / `market_prob` / `edge` being null on a `value_prop` row: a\n`value_prop` row is only written for a match that ALREADY carries\nmoneyline odds, and the row stores its own model probability, de-vigged\nmarket probability and edge. 570 of 642 rows (88.8%) were returning\nthose five fields null, plus `home_team`, `away_team` and the three\nwin/draw/loss probabilities, because the read path extracted only the\nkey names a `match_result` row uses. `value_prop` rows now serve\n`best_side`, `elo_prob`, `market_prob`, `edge` and `value_bet` from\ntheir own stored analysis, and `home_team` / `away_team` from the match.\n`home_win_probability`, `draw_probability` and `away_win_probability`\nremain null on a `value_prop` row deliberately: that model is two-way\nand states no draw, so the number is reported only where it is\nside-scoped, in `elo_prob` against `best_side`.\n\nSecond, ORDERING. The list was ordered by kickoff ascending, i.e.\noldest first, so `?limit=5` returned five expired rows, the first 124\ndays past expiry, while 228 live rows sat further down. Live rows (and\nrows whose expiry is unknown) now sort ahead of expired ones; within\nthe live group soonest kickoff first, within the expired group most\nrecent kickoff first. This is an ordering change and not a filter:\nnothing is withheld, `meta.total` is unchanged, and every expired row\nis still returned on a later page.\n",
        "parameters": [
          {
            "in": "query",
            "name": "tournament",
            "schema": {
              "type": "string",
              "enum": [
                "wc2026"
              ]
            },
            "description": "Tournament filter, e.g. wc2026."
          },
          {
            "in": "query",
            "name": "match_id",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Single match."
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string"
            },
            "description": "Prediction type filter."
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string",
              "enum": [
                "football",
                "basketball",
                "baseball",
                "ice_hockey",
                "cricket",
                "mma",
                "boxing",
                "formula1",
                "american_football"
              ]
            },
            "description": "Narrow to one sport, WITHIN your key's entitlement; it never widens\naccess. Predictions cover football, american_football, basketball,\nbaseball and ice_hockey (2026-09-28). Asking for one of the other\nfour sports returns 200 with an empty array plus `meta.coverage`,\n`meta.covered_sports` and a `meta.note` saying nothing has ever been\nwritten for it; that is the true answer to a well-formed request,\nnot an error. A value that is not a sport at all is a 400.\n\nUntil 2026-09-04 this parameter was accepted and silently discarded,\nso every value returned the same rows regardless of sport (#1376).\n"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. When `?sport=` named a sport with no predictions, `data` is empty\nand `meta` carries `coverage`, `covered_sports` and `note`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query, or a ?sport= value that is not a known sport"
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/odds/historical": {
      "get": {
        "tags": [
          "odds"
        ],
        "summary": "Historical odds snapshots for a match (CLV)",
        "description": "Every historical odds snapshot (opening / t24h / closing, never the live\n`current` series) for one match, newest snapshot first, with sportsbook\nnames resolved. Powers closing-line value (CLV) and line-movement analysis.\nRequires an Edge plan or higher (Free / Solo / Trio receive 403).\nEach row carries an EVIDENCE-BASED `verification` field from two-source\ncross-checking: a \"verified\" status is only ever returned when we\nactually compared two providers, never inferred from the absence of a\ncheck: `two_source_agreed` (both providers priced this book/market/side\nand agreed; a positive comparison is on record), `two_source_diverged`\n(both priced it and disagreed; `divergence_categories` then lists the\ncategories, e.g. `[\"PRICE_DIFF\",\"STALE\"]`), `single_source` (only one\nprovider carries this book/side, or the book has no cross-provider match;\nNOT verified), or `unverified` (we have not compared this line; NOT\nverified). The default when we lack evidence is `single_source` or\n`unverified`, never a badge. `verified_at` dates the comparison the status\ncame from (null when nothing was compared); a verification you cannot\ndate is a claim you cannot check. A comparison only describes the snapshot\nwhose line it actually compared and only within 48h of it, so a snapshot\nwhose price has since moved reads `unverified` rather than inheriting an\nolder badge. Response: `{ data: [{ match_id, snapshot_type,\nsnapshot_at, market, book: { id, name, logo_url }, line, source,\nfetched_at, verification, verified_at, divergence_categories? }], meta: {\ntotal, limit, offset, snapshot_types_present, generated_at } }`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "match_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Match to read snapshots for."
          },
          {
            "in": "query",
            "name": "snapshot_type",
            "schema": {
              "type": "string",
              "enum": [
                "opening",
                "t24h",
                "closing"
              ]
            },
            "description": "Filter to one snapshot type (default: all historical)."
          },
          {
            "in": "query",
            "name": "sportsbook",
            "schema": {
              "type": "string"
            },
            "description": "Filter to one book by the name returned in `book.name`, e.g. DraftKings. Case-insensitive."
          },
          {
            "in": "query",
            "name": "market",
            "schema": {
              "type": "string"
            },
            "description": "Filter to one market, e.g. moneyline."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query (suggested_fix lists valid snapshot_type values)"
          },
          "403": {
            "description": "plan_required (Pro or higher)"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "odds",
        "x-bbs-not-available-for-sports": [
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/odds/player-props/historical": {
      "get": {
        "tags": [
          "odds"
        ],
        "summary": "NHL player-prop archive: every stored observation for a game or a player",
        "description": "Every stored NHL player-prop observation for one game or one player,\nnewest snapshot first. **16,807,052 observations across 4,231 events,\n2023-05-03 to 2026-06-14**, 18 vendor markets over 9 canonical stats.\nRequires an Edge plan or higher (Free / Solo / Trio receive 403).\n\nThis is the ARCHIVE. It is a different product from\n`/v1/nhl/players/{player_id}/props`, which serves the CURRENT line:\none row per book and market type, picked by a stated precedence. Here\nnothing is collapsed: the response grain is one observation, and season,\nsnapshot minute, book, market, market type, line and outcome are each\ncarried on their own row and each addressable as a filter. That is what\nlets you reconstruct line movement yourself.\n\n**One of `match_id` or `player_id` is required.** An unanchored window\nquery cannot be answered from the archive's indexes, so it is refused\nrather than accepted and timed out.\n\n**Unattributed rows are served, not dropped.** Roughly 4% of\nobservations name a player the bridge could not pin to exactly one of\nour players. Those rows are returned with `player_id: null`, the\nvendor's `player_name` intact, and `attribution` set to `ambiguous` or\n`unmatched`, never guessed onto a player id. `meta.attribution_counts`\ngives the split for YOUR selection. Note that a `player_id`-addressed\nquery cannot return them at all, by construction; `meta.\nunattributed_rows_reachable` says so on every response.\n\n**There is no `opening` or `closing` label, deliberately.** The board\nopens roughly 48h before puck drop, so no stored row is provably an\nopening price. Each row carries `snapshot_at` and the game's\n`commence_time`, plus `before_kickoff`, which is exactly the\ncomparison `snapshot_at < commence_time`, made at read time. Derive\n\"closing\" as the last observation strictly before `commence_time`.\n\nResponse: `{ data: [{ match_id, home_team, away_team, commence_time,\nplayer_id, player_name, attribution, market, market_type, outcome, line,\nprice_american, book: { id, name, logo_url }, source, snapshot_at,\nbefore_kickoff }], meta: { total, limit, offset, addressed_by,\nattribution_counts, unattributed_rows_reachable, markets_selected,\nsnapshot_labels, generated_at } }`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "match_id",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Game to read the archive for. Required unless player_id is given."
          },
          {
            "in": "query",
            "name": "player_id",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Player to read the archive for, across every archived game. Required unless match_id is given. Cannot return unattributed rows."
          },
          {
            "in": "query",
            "name": "market",
            "schema": {
              "type": "string",
              "enum": [
                "shots_on_goal",
                "goals",
                "assists",
                "points",
                "saves",
                "power_play_points",
                "blocked_shots",
                "goals_against",
                "shutouts"
              ]
            },
            "description": "Filter to one canonical stat."
          },
          {
            "in": "query",
            "name": "market_type",
            "schema": {
              "type": "string",
              "enum": [
                "over_under",
                "alternate",
                "scorer_anytime",
                "scorer_first",
                "scorer_last"
              ]
            },
            "description": "Filter to one price shape. `alternate` is the Over-only alt-line ladder; the three scorer types are Yes-priced and carry no line."
          },
          {
            "in": "query",
            "name": "sportsbook",
            "schema": {
              "type": "string"
            },
            "description": "Filter to one book by the name returned in `book.name`, e.g. DraftKings. Case-insensitive."
          },
          {
            "in": "query",
            "name": "snapshot_from",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only observations at or after this instant."
          },
          {
            "in": "query",
            "name": "snapshot_to",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only observations at or before this instant."
          },
          {
            "in": "query",
            "name": "before_kickoff",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "true keeps only observations made strictly before commence_time."
          },
          {
            "in": "query",
            "name": "attribution",
            "schema": {
              "type": "string",
              "enum": [
                "resolved",
                "ambiguous",
                "unmatched",
                "unresolved"
              ]
            },
            "description": "Filter to one attribution state."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "400": {
            "description": "invalid query: neither match_id nor player_id given, or a market/market_type pair no market has"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          },
          "404": {
            "description": "no archived event for this match, or more than one and it cannot be disambiguated"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "player props",
        "x-bbs-not-available-for-sports": [
          "football",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/odds/player-props/coverage": {
      "get": {
        "tags": [
          "odds"
        ],
        "summary": "What the NHL player-prop archive holds, per season",
        "description": "Per-season depth of the NHL player-prop archive: events held, events\ncarrying props, and observation counts. Edge plan or higher.\n\n`observations` is the per-event insert count recorded at capture time,\nsummed; stated in `meta.observations_source` rather than left for you\nto assume, because it is not a live `COUNT(*)` over the observation\ntable.\n\nResponse: `{ data: { sport, league, seasons: [{ season, events,\nevents_with_props, observations, first_event, last_event }], totals,\nmarkets_swept, canonical_markets, market_types }, meta: {\nobservations_source, snapshot_labels, generated_at } }`.\n",
        "responses": {
          "200": {
            "description": "ok"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "player props",
        "x-bbs-not-available-for-sports": [
          "football",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/odds/closing-lines": {
      "get": {
        "tags": [
          "odds"
        ],
        "summary": "Closing lines across a sport and date range",
        "description": "Closing lines (`snapshot_type='closing'`) for every match of a sport in a\nkickoff date range, newest kickoff first, with sportsbook names resolved.\nRequires an Edge plan or higher (Free / Solo / Trio receive 403). Each row\ncarries an evidence-based `verification` field from two-source\ncross-checking: `two_source_agreed`, `two_source_diverged` (with\n`divergence_categories`), `single_source`, or `unverified`; a \"verified\"\nstatus requires a real comparison, never row-absence, and `verified_at`\ndates it (null when nothing was compared); a comparison describes only the\nsnapshot whose line it compared, within 48h. See /v1/odds/historical.\nResponse:\n`{ data: [{ match_id, sport, league, home, away, kickoff_utc, snapshot_at,\nmarket, book: { id, name, logo_url }, line, source, fetched_at,\nverification, verified_at, divergence_categories? }], meta: { total,\nlimit, offset, snapshot_types_present, generated_at } }`.\n",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Sport, e.g. basketball, football, baseball."
          },
          {
            "in": "query",
            "name": "date_from",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Inclusive kickoff lower bound (YYYY-MM-DD)."
          },
          {
            "in": "query",
            "name": "date_to",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Inclusive kickoff upper bound (YYYY-MM-DD)."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "odds",
        "x-bbs-not-available-for-sports": [
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/odds/divergences": {
      "get": {
        "tags": [
          "odds"
        ],
        "summary": "Cross-provider odds divergences (two-source verification)",
        "description": "Per (match, market, canonical book) disagreements between our two odds\nproviders, as detected by the divergence\nworker. Categories: `PRICE_DIFF` (same line, juice differs > threshold),\n`LINE_DIFF` (different line value), `MISSING_FROM_ONE` (a book one\nprovider doesn't carry), `STALE` (one feed lagging). Requires an Edge\nplan or higher (Free / Solo / Trio receive 403). Provide `match_id`, or\nelse `date_from` + `date_to` to bound the kickoff window. Response: `{\ndata: [{ match_id, sport_type, league, kickoff_utc, market, canonical_id,\nbook_display_name, category, secondary_categories, magnitude, detected_at\n}], meta: { total, limit, offset, categories_present, generated_at } }`.\n`magnitude` is category-specific (price_diff_pct for PRICE_DIFF, line_diff\nfor LINE_DIFF, else null) and is an ABSOLUTE difference; it carries no\ndirection and cannot be used to reconstruct either provider's line. The\nper-provider line and price values are deliberately not returned: the\nproduct here is the computed disagreement, not the upstream feeds.\nReturns 503 while the divergence worker is still provisioning (its table\nnot yet created).\n",
        "parameters": [
          {
            "in": "query",
            "name": "match_id",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Filter to one match. If omitted, date_from + date_to are required."
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string",
              "enum": [
                "football",
                "basketball",
                "baseball",
                "ice_hockey",
                "cricket",
                "mma",
                "boxing",
                "formula1",
                "american_football"
              ]
            },
            "description": "Canonical sport slug, e.g. football or basketball. Common aliases are accepted and normalized (`soccer` to `football`)."
          },
          {
            "in": "query",
            "name": "category",
            "schema": {
              "type": "string",
              "enum": [
                "PRICE_DIFF",
                "LINE_DIFF",
                "MISSING_FROM_ONE",
                "STALE"
              ]
            },
            "description": "Filter to one divergence category."
          },
          {
            "in": "query",
            "name": "canonical_id",
            "schema": {
              "type": "string"
            },
            "description": "Filter to one canonical book, e.g. betmgm, draftkings."
          },
          {
            "in": "query",
            "name": "date_from",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Inclusive kickoff lower bound (required if no match_id)."
          },
          {
            "in": "query",
            "name": "date_to",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Inclusive kickoff upper bound (required if no match_id)."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid query (suggested_fix lists valid category values; or date range required)"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          },
          "503": {
            "description": "divergence worker still provisioning (table not yet created)"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "odds",
        "x-bbs-not-available-for-sports": [
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/wc2026/golden-boot": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 Golden Boot leaderboard",
        "description": "Tournament-wide top 20 scorers, aggregated from per-match fixture player\nstats across the 48 qualified nations. Ranked by goals with DENSE_RANK\n(players tied on goals share a rank), then ordered by the FIFA tiebreaker\n(goals, then assists, then fewest minutes). Explicit lifecycle state:\n`pre_tournament` (empty leaders[], null updated_at) before June 11,\n`in_progress` during, `finished` after. Response object keys:\ntournament_state, leaders[] (rank, name, country, team_id, team_name,\nplayer_id, goals, assists, minutes), updated_at. `country` is the\nnational team's full name (not an ISO code); player_id is null until the\nupstream player is bridged. Daily refresh, 1-hour Redis cache.\n",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/wc2026/leaders": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 top scorers (with games played)",
        "description": "Lighter sibling of golden-boot: rank, goals, assists, and games_played\nper player with a flat per-player team object, no squad filter or FIFA\ntiebreaker sort. Explicit tournament_state (pre_tournament until the\nfirst match finishes). Response object keys: tournament_state, leaders[]\n(rank, player_id, player_name, team {id, name}, goals, assists,\ngames_played), updated_at. meta.source is always \"aggregator-paid\". 60-second\nRedis cache.\n",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/wc2026/stat-leaders": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 stat leaders (aggregated from our match stats)",
        "description": "Canonical-for-us tournament leaderboard, aggregated from our own\nper-fixture wc2026_player_match_stats (not the upstream topscorers\nblob behind /v1/wc2026/leaders). Sums each player across the tournament\nand supports a ?category sort, including minutes and matches which the\nblob route does not carry. Flat data[] shape: rank, player_name, team,\ngoals, assists, minutes, matches. Returns { data: [], updated_at: null }\nbefore the first post-match ingest. 5-minute Redis cache.\n",
        "parameters": [
          {
            "in": "query",
            "name": "category",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "goals",
                "assists",
                "minutes",
                "matches"
              ],
              "default": "goals"
            },
            "description": "Sort metric. Defaults to goals (golden boot)."
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Number of leaders to return."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/wc2026/players/{id}/tournament": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "One player's World Cup so far (aggregate + event timeline)",
        "description": "A single player's tournament: headline aggregate (matches_played,\nminutes, goals, assists, yellow_cards, red_cards, substituted_off) plus\na per-event timeline (each with match_id, kickoff_utc, opponent, minute,\nevent_type, event_detail, assist_name). :id is the BBS player UUID.\nAggregates come from wc2026_player_match_stats (authoritative, matches\n/v1/wc2026/stat-leaders); the timeline from wc2026_match_events, joined\non a shared player key. A player who exists but has not played yet returns 200\nwith zeroed stats and an empty events array (not 404). 60s Redis cache.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "BBS player UUID."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "malformed player id (not a UUID)"
          },
          "404": {
            "description": "no player with that id"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/wc2026/group/{group}/advancement": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 group advancement probabilities (Monte Carlo)",
        "description": "Per-team probability of finishing 1st/2nd/3rd/4th in one group, from a\n10,000-iteration Monte Carlo over the remaining fixtures anchored on the\ncurrent standings. p_advance = p_1st + p_2nd; p_third_contention is the\nraw P(3rd) (cross-group best-third resolution lives in\n/v1/formulas/wc2026/progression). Response object keys: group,\niterations, matches_played, matches_remaining, teams[] (team {id, name,\nelo}, current {played, points, gd}, p_1st, p_2nd, p_3rd, p_4th,\np_advance, p_third_contention, p_eliminated), as_of. 1-hour Redis cache.\n",
        "parameters": [
          {
            "in": "path",
            "name": "group",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-La-l]$"
            },
            "description": "Group letter A through L."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid group letter"
          },
          "404": {
            "description": "group does not have 4 rated teams"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/wc2026/teams/{id}/squad": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 team squad (26 players)",
        "description": "Full World Cup squad for one nation, ordered by position group\n(Goalkeeper, Defender, Midfielder, Attacker) then shirt number.\nResponse object keys: team {id, name, short_name}, squad[]\n(player_id, name, shirt_number, position, age, photo_url).\nplayer_id is null for the few players not yet bridged; those are\nidentified by name. 1-hour Redis cache.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "x-bbs-field-entitlements": [
              {
                "path": "data.squad[].photo_url",
                "min_plan": "solo",
                "below_floor": "PRESENT-NULL"
              }
            ],
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "team": {
                          "type": "object"
                        },
                        "squad": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "player_id": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true
                              },
                              "name": {
                                "type": "string"
                              },
                              "shirt_number": {
                                "type": "integer",
                                "nullable": true
                              },
                              "position": {
                                "type": "string",
                                "nullable": true
                              },
                              "age": {
                                "type": "integer",
                                "nullable": true
                              },
                              "photo_url": {
                                "type": "string",
                                "format": "uri",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid team id"
          },
          "404": {
            "description": "not a WC2026 team"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/teams/{id}/world-cup-history": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "National team World Cup history (2002-2022)",
        "description": "A nation's World Cup record across past tournaments, most recent first.\nResponse array item keys: year, tournament, finish, games_played, wins,\ndraws, losses, goals_for, goals_against. Covers the 48 WC2026-qualified\nnations, so non-qualified former champions (e.g. Italy, the 2006 champion)\nare absent; and for a nation present, a penalty-shootout final is not\nlabelled \"champion\" in finish (the seeder derives it from the regulation\nfinal score). 24-hour cache.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid team id"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/teams/{id}/elo/history": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Team Elo rating history (trend line)",
        "description": "Elo rating trend for a team: a baseline seed (match_id null) plus one\npoint per settled match, ordered oldest first. The trend sibling of\n/v1/teams/{id}/elo. Response array item keys: date, elo_rating, match_id,\nopponent. source selects the rating series; when omitted it is derived\nfrom the team's own league, so an NCAAF team gets source=ncaaf and an\nNFL team source=nfl without asking. Pass source explicitly to override.\nA league carrying no mapping falls back to wc2026 and returns an empty\ndata: [], which reads as \"no history\" when it actually means \"wrong\nseries\"; MLB is in that position today, so pass source=mlb for a\nbaseball team. Optional from / to date filters. 1-hour Redis cache.\n**Solo plan or higher**: Elo trend history starts at Solo; Free\nreceives a 403 naming the tier and linking the docs and pricing pages.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "source",
            "schema": {
              "type": "string",
              "enum": [
                "ncaaf",
                "ncaaf-fcs",
                "nfl",
                "mlb",
                "epl",
                "laliga",
                "seriea",
                "bundesliga",
                "ligue1",
                "wc2026"
              ]
            },
            "description": "Rating series to read. Defaults to the series for the team's own league; pass it explicitly to override. ncaaf = NCAAF FBS college football; ncaaf-fcs = NCAAF FCS; nfl = NFL club Elo since 2020; mlb = MLB, which has no league mapping yet and so must be passed explicitly; epl/laliga/seriea/bundesliga/ligue1 = the five European domestic football leagues, each calibrated independently and NOT comparable across leagues; wc2026 = FIFA World Cup 2026 national teams, the only series carrying a baseline seed row. The others start at the team's first game on record."
          },
          {
            "in": "query",
            "name": "from",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Clip to dates >= from (YYYY-MM-DD)."
          },
          {
            "in": "query",
            "name": "to",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Clip to dates <= to (YYYY-MM-DD)."
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid team id or query"
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          }
        },
        "x-bbs-min-plan": "solo",
        "x-bbs-capability": "Elo ratings",
        "x-bbs-not-available-for-sports": [
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/players/{id}/career": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player career averages (season-by-season)",
        "description": "Season-by-season career averages, most recent first, sport-dispatched on\nthe player's sport (check meta.sport). NBA: one row per (season, team)\nwith season, team, abbr, gp, ppg, rpg, apg, mpg, had_postseason. NHL:\nrows carry game_type plus goals/assists/points/plus_minus/pim/sog/hits/\nblk/toi (skaters) or saves/shots_against/goals_against/shutouts (goalies).\nTwin-safe. 1-hour Redis cache.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid player id"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}/transfers": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player transfer history (football)",
        "description": "A football player's club transfer history, newest first. Football-only,\nfetched live from the upstream feed, 24-hour cache. Response array item keys:\ndate, from_club, to_club, type (raw label e.g. \"€ 180M\" / \"Free\" /\n\"Loan\"), fee (euros as a number, null when type is not a euro amount).\nEmpty list for a player with no football id.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid player id"
          },
          "404": {
            "description": "player not found"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}/trophies": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player trophy history (football)",
        "description": "A football player's career honours, with a top-level total_trophies count\nof wins. Football-only, fetched live from the upstream feed, 24-hour cache.\nResponse keys: data[] (season, competition, country, place, won),\ntotal_trophies. Empty list (total_trophies 0) for a player with no\nfootball id.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid player id"
          },
          "404": {
            "description": "player not found"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/teams/{id}/playoffs": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "NBA team playoff record (per season)",
        "description": "An NBA team's playoff record by season, most recent first. Derived from\npostseason box scores (NBA linescore is null for most games, so each\ngame's score is the box-score point sum per side). Response array item\nkeys: season, games_played, wins (losses = games_played - wins).\nNBA-only. 5-minute Redis cache.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid team id"
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/wc2026/groups": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 group tables (all 12 groups)",
        "description": "All 12 World Cup groups (A through L), each with its 4 teams ordered by\nwithin-group rank. Response shape: data.groups keyed by group letter,\neach value { teams: [{ rank, team_id, name, played, wins, draws, losses,\ngf, ga, gd, points, advancement_prob, p_top2, p_win_group,\nexpected_points }] }. rank / team_id / name and the advancement fields\n(Monte Carlo model output, probabilities 0..1) may be null pre-matchday\nor when unbridged. No parameters. 5-minute Redis cache.\n",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/wc2026/matches/{game_id}/line-movement": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "WC2026 match line movement (odds history)",
        "description": "Chronological odds history for one WC2026 match, per sportsbook and\nmarket, oldest first. Only books with 2+ distinct timestamps per market\nappear. Response object keys: match_id, home, away, kickoff, markets\n{ moneyline[] (sportsbook, timestamp, home_price, away_price, draw_price),\ntotal[] (sportsbook, timestamp, line, over_price, under_price), spread[]\n(sportsbook, timestamp, line, home_price, away_price) }. 60-second cache.\n\nEvery `*_price` is a canonical AMERICAN moneyline integer. Books quoting\nin decimal odds are converted, not omitted: before\nfix/nerd-odds-price-format this route filtered them out of the history\nentirely (89.7% of non-American-source rows), and published the\nwhole-number ones as small American prices.\n",
        "parameters": [
          {
            "in": "path",
            "name": "game_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "invalid game_id"
          },
          "403": {
            "description": "plan_required (Edge or higher)"
          },
          "404": {
            "description": "not a WC2026 match"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-capability": "odds",
        "x-bbs-not-available-for-sports": [
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/cricket/matches": {
      "get": {
        "tags": [
          "cricket"
        ],
        "summary": "List cricket matches",
        "description": "Cricket match list, served from stored rows. Returns a JSON ARRAY in `data`, never a `{scores: ...}` object. Narrow with ?series= (matched case-insensitively as a substring of the competition name) and ?date= (one day, or an inclusive START:END range of days), and page with ?limit= + ?offset=.",
        "parameters": [
          {
            "in": "query",
            "name": "series",
            "schema": {
              "type": "string"
            },
            "description": "Competition name or fragment, matched case-insensitively against the league name."
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}(:\\d{4}-\\d{2}-\\d{2})?$",
              "example": "2026-09-10:2026-09-24"
            },
            "description": "Filters on the match's KICKOFF instant. Two spellings, the same grammar as ?date= on /v1/games: (a) YYYY-MM-DD, one UTC calendar day: kickoff at or after 00:00Z that date and before 00:00Z the next. (b) START:END, an INCLUSIVE range of calendar days, so 2026-09-10:2026-09-24 covers the whole of the 24th, from 00:00Z on the 10th up to (but not including) 00:00Z on the 25th. START must be on or before END; a backwards range is a 400, not an empty 200. There is no cap on the span; this endpoint is paged, so ?limit= and ?total bound the response. Multi-day formats (a four-day County Championship match) are matched on their kickoff day ALONE: a match that starts on the 9th and runs into the 11th does NOT appear under ?date=2026-09-10:2026-09-24. The exact half-open instant window applied is echoed in meta.date_window, so the bucketing is never something you have to infer."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. Asia/Kolkata). ?date= then selects local calendar days in that zone, both ends, instead of UTC ones. Offsets such as +05:30 are rejected with 400. Example: ?date=2026-09-10:2026-09-24&tz=Asia/Kolkata."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            },
            "description": "Page size."
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Rows to skip. Results are ordered by kickoff descending with a unique tiebreak, so pages are stable across requests."
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "description": "Alias for ?offset=(page-1)*limit. An explicit ?offset= wins."
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "scheduled",
                "live",
                "finished",
                "postponed",
                "cancelled"
              ]
            },
            "description": "NOT a supported filter on this endpoint; sending it returns 400 rather than being accepted and ignored."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data` is an array of matches (empty when the filter genuinely matches nothing). `meta` carries `count` (rows in this page), `total` (rows matching the filter), `limit` and `offset`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "meta": {
                          "type": "object",
                          "properties": {
                            "count": {
                              "type": "integer",
                              "description": "Rows in this page."
                            },
                            "total": {
                              "type": "integer",
                              "description": "Rows matching the filter, before limit/offset."
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "offset": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters: a bad ?date format, a ?date range whose start is after its end, an out-of-range ?limit or ?offset, or an unsupported ?status filter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/cricket/matches/{id}": {
      "get": {
        "tags": [
          "cricket"
        ],
        "summary": "Single cricket match",
        "description": "Resolves the `stats` field for one cricket match.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data.stats` is a FieldResult, or null when unavailable (still 200).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "stats": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/FieldResult"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/cricket/matches/{id}/deliveries": {
      "get": {
        "tags": [
          "cricket"
        ],
        "summary": "Cricket ball-by-ball deliveries",
        "description": "Stored ball-by-ball for one cricket match. Requires Pro or higher. This is a new capability; the innings scorecard and existing cricket match, series, player and tutorial surfaces remain available on Free. Wides and no-balls are delivery records and remain present as rows. Same-day double-headers can share a canonical match id; when that happens, retry with one of the opaque stream numbers returned in meta.available_streams. Internal source references are never exposed.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "stream",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Opaque one-based stream selector, needed only when the match id represents more than one same-day match."
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string",
              "maxLength": 256
            },
            "description": "Exact meta.next_cursor from the previous page. It carries innings, super-over, over and delivery position."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A collision-safe page of stored deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "required": [
                            "match_id",
                            "deliveries"
                          ],
                          "properties": {
                            "match_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "deliveries": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "required": [
                                  "innings_number",
                                  "super_over",
                                  "over_number",
                                  "ball_seq",
                                  "batter",
                                  "bowler",
                                  "runs",
                                  "extras",
                                  "wicket"
                                ],
                                "properties": {
                                  "innings_number": {
                                    "type": "integer",
                                    "minimum": 1
                                  },
                                  "super_over": {
                                    "type": "boolean"
                                  },
                                  "over_number": {
                                    "type": "integer",
                                    "minimum": 0
                                  },
                                  "ball_seq": {
                                    "type": "integer",
                                    "minimum": 1
                                  },
                                  "batting_team": {
                                    "type": [
                                      "object",
                                      "null"
                                    ],
                                    "required": [
                                      "id",
                                      "name"
                                    ],
                                    "properties": {
                                      "id": {
                                        "type": "string",
                                        "format": "uuid"
                                      },
                                      "name": {
                                        "type": [
                                          "string",
                                          "null"
                                        ]
                                      }
                                    }
                                  },
                                  "batter": {
                                    "type": "object",
                                    "required": [
                                      "id",
                                      "name"
                                    ],
                                    "properties": {
                                      "id": {
                                        "type": "string",
                                        "format": "uuid"
                                      },
                                      "name": {
                                        "type": [
                                          "string",
                                          "null"
                                        ]
                                      }
                                    }
                                  },
                                  "bowler": {
                                    "type": "object",
                                    "required": [
                                      "id",
                                      "name"
                                    ],
                                    "properties": {
                                      "id": {
                                        "type": "string",
                                        "format": "uuid"
                                      },
                                      "name": {
                                        "type": [
                                          "string",
                                          "null"
                                        ]
                                      }
                                    }
                                  },
                                  "non_striker": {
                                    "type": [
                                      "object",
                                      "null"
                                    ],
                                    "required": [
                                      "id",
                                      "name"
                                    ],
                                    "properties": {
                                      "id": {
                                        "type": "string",
                                        "format": "uuid"
                                      },
                                      "name": {
                                        "type": [
                                          "string",
                                          "null"
                                        ]
                                      }
                                    }
                                  },
                                  "runs": {
                                    "type": "object",
                                    "required": [
                                      "batter",
                                      "extras",
                                      "total"
                                    ],
                                    "properties": {
                                      "batter": {
                                        "type": "integer",
                                        "minimum": 0
                                      },
                                      "extras": {
                                        "type": "integer",
                                        "minimum": 0
                                      },
                                      "total": {
                                        "type": "integer",
                                        "minimum": 0
                                      }
                                    }
                                  },
                                  "extras": {
                                    "type": "object",
                                    "required": [
                                      "wides",
                                      "no_balls",
                                      "byes",
                                      "leg_byes",
                                      "penalty"
                                    ],
                                    "properties": {
                                      "wides": {
                                        "type": "integer",
                                        "minimum": 0
                                      },
                                      "no_balls": {
                                        "type": "integer",
                                        "minimum": 0
                                      },
                                      "byes": {
                                        "type": "integer",
                                        "minimum": 0
                                      },
                                      "leg_byes": {
                                        "type": "integer",
                                        "minimum": 0
                                      },
                                      "penalty": {
                                        "type": "integer",
                                        "minimum": 0
                                      }
                                    }
                                  },
                                  "wicket": {
                                    "type": [
                                      "object",
                                      "null"
                                    ],
                                    "required": [
                                      "kind",
                                      "player_out",
                                      "fielder_ids"
                                    ],
                                    "properties": {
                                      "kind": {
                                        "type": "string"
                                      },
                                      "player_out": {
                                        "type": [
                                          "object",
                                          "null"
                                        ],
                                        "required": [
                                          "id",
                                          "name"
                                        ],
                                        "properties": {
                                          "id": {
                                            "type": "string",
                                            "format": "uuid"
                                          },
                                          "name": {
                                            "type": [
                                              "string",
                                              "null"
                                            ]
                                          }
                                        }
                                      },
                                      "fielder_ids": {
                                        "type": "array",
                                        "items": {
                                          "type": "string",
                                          "format": "uuid"
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        },
                        "meta": {
                          "type": "object",
                          "required": [
                            "available",
                            "count",
                            "limit",
                            "stream",
                            "available_streams",
                            "next_cursor"
                          ],
                          "properties": {
                            "available": {
                              "type": "boolean"
                            },
                            "count": {
                              "type": "integer"
                            },
                            "limit": {
                              "type": "integer"
                            },
                            "stream": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "available_streams": {
                              "type": "array",
                              "items": {
                                "type": "integer",
                                "minimum": 1
                              }
                            },
                            "next_cursor": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id, stream, cursor or limit."
          },
          "403": {
            "description": "Pro plan or higher is required."
          },
          "404": {
            "description": "Match or selected delivery stream not found."
          },
          "409": {
            "description": "The canonical match id represents multiple streams; retry with meta.available_streams."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "x-bbs-min-plan": "pro",
        "x-bbs-min-plan-by-sport": {
          "cricket": "pro"
        },
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/cricket/matches/{id}/scorecard": {
      "get": {
        "tags": [
          "cricket"
        ],
        "summary": "Cricket match scorecard",
        "description": "Resolves the `stats` field for one cricket match. NOTE: this currently returns the SAME payload as GET /v1/cricket/matches/{id}; both handlers resolve the identical `stats` field. Documented as-is rather than implying a distinct scorecard shape.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data.stats` is a FieldResult, or null when unavailable (still 200).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "stats": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/FieldResult"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/cricket/matches/{id}/state": {
      "get": {
        "tags": [
          "cricket"
        ],
        "summary": "Cricket live match state",
        "description": "Current innings state for one cricket match (overs bowled, runs, wickets, target and run rates), read from `cricket_match_state`, which the hourly cricket ingest appends to ONLY while a match is in progress.\n\n`meta.available` is false, and `data.state` null, whenever no snapshot was ever recorded: a scheduled match, a match between innings before the first hourly capture, or any match that finished before this write path was wired (2026-08-12). That is the common answer and it is never padded with a zeroed-out innings.\n\n`meta.live` describes the PAYLOAD, not the match: true only when the match is in progress AND a snapshot exists. For a finished match the last recorded snapshot is returned with `live: false` and a note, so a closing reading is never mistaken for a current one. `recorded_at` carries the observation time in every case.\n\n`required_run_rate` is present only where it can be computed honestly (T20-format chases); it is omitted for ODIs and Tests rather than derived from an assumed innings length.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "innings",
            "required": false,
            "description": "Return the newest snapshot for THIS innings instead of the newest snapshot for the match. `cricket_match_state` is append-only and per-innings, so a multi-innings match holds state for each innings it was captured during; without this parameter only the last one is reachable. Ordering within the innings is `recorded_at DESC, id DESC`, the same tiebreak the unfiltered default uses, narrowed rather than replaced.\n\nOmit it for the previous behaviour, which is unchanged: newest snapshot for the match, and a 200 with `data.state: null` / `meta.available: false` when none exists.\n\nAn innings with no snapshot is a **404**, not a 200 with `available: false`. You asked a specific question, and the empty 200 cannot distinguish \"this match has no state at all\" from \"we hold innings 1 and 3, not 7\". The 404 names the innings we do hold in `meta.available_innings` and in `suggested_fix`. A non-integer or non-positive value (`?innings=abc`, `?innings=0`) is a 400.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "example": 2
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data.state` is null when no state was recorded (still 200); this is the no-parameter case. `meta.filters_applied.innings` echoes the innings actually applied, or null when none was requested.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "match_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "state": {
                              "oneOf": [
                                {
                                  "type": "null"
                                },
                                {
                                  "type": "object",
                                  "properties": {
                                    "innings_number": {
                                      "type": "integer",
                                      "example": 2
                                    },
                                    "batting_team": {
                                      "oneOf": [
                                        {
                                          "type": "null"
                                        },
                                        {
                                          "type": "object",
                                          "properties": {
                                            "id": {
                                              "type": "string",
                                              "format": "uuid"
                                            },
                                            "name": {
                                              "type": "string",
                                              "nullable": true
                                            }
                                          }
                                        }
                                      ],
                                      "description": "Null when the upstream innings label matched neither side; the scoreline is still real and is kept."
                                    },
                                    "overs_bowled": {
                                      "type": "number",
                                      "example": 12
                                    },
                                    "runs_scored": {
                                      "type": "integer",
                                      "example": 88
                                    },
                                    "wickets_fallen": {
                                      "type": "integer",
                                      "example": 3
                                    },
                                    "current_run_rate": {
                                      "type": "number",
                                      "nullable": true,
                                      "example": 7.33
                                    },
                                    "target": {
                                      "type": "integer",
                                      "nullable": true,
                                      "example": 161
                                    },
                                    "required_run_rate": {
                                      "type": "number",
                                      "nullable": true,
                                      "example": 9.13
                                    },
                                    "dls_target": {
                                      "type": "integer",
                                      "nullable": true
                                    },
                                    "result_method": {
                                      "type": "string",
                                      "nullable": true,
                                      "example": "DLS"
                                    },
                                    "recorded_at": {
                                      "type": "string",
                                      "format": "date-time"
                                    }
                                  }
                                }
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "malformed match id, or `innings` was not a positive whole number. Unknown query parameters are still ignored, as on every other gateway route."
          },
          "404": {
            "description": "no such cricket match (or the id names a match in another sport); or `innings` was given and we hold no snapshot for that innings, and `meta.available_innings` lists the ones we do hold."
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/cricket/series": {
      "get": {
        "tags": [
          "cricket"
        ],
        "summary": "List cricket series",
        "description": "The competitions we actually hold cricket matches for, with a match count and first/latest kickoff each, ordered by match count descending. Returns a JSON ARRAY in `data`. This endpoint returns a SINGLE PAGE (by default the complete list), so ?page= and ?offset= are rejected with a 400 rather than accepted and ignored. Use ?limit= to size it.",
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            },
            "description": "Size the single page returned. Omit for the COMPLETE list, which is the default and has always been the behaviour of this endpoint."
          },
          {
            "in": "query",
            "name": "country",
            "schema": {
              "type": "string"
            },
            "description": "NOT a supported filter on this endpoint; there is no column to filter on. Sending it returns 400 rather than being ignored."
          },
          {
            "in": "query",
            "name": "format",
            "schema": {
              "type": "string",
              "enum": [
                "test",
                "odi",
                "t20",
                "t20i",
                "list_a",
                "ipl",
                "bbl",
                "psl"
              ]
            },
            "description": "NOT a supported filter on this endpoint; there is no column to filter on. Sending it returns 400 rather than being ignored."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data` is an array of competitions, each with `id`, `name`, `match_count`, `first_match` and `latest_match`. `meta.count` is the number returned.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "name": {
                                "type": "string"
                              },
                              "match_count": {
                                "type": "integer"
                              },
                              "first_match": {
                                "type": "string",
                                "nullable": true
                              },
                              "latest_match": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters: an out-of-range ?limit, an unsupported ?country / ?format filter, or ?page= / ?offset= on an endpoint that returns a single page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/cricket/players/{id}": {
      "get": {
        "tags": [
          "cricket"
        ],
        "summary": "Cricket player career profile",
        "description": "Career batting and bowling for one cricket player, aggregated from the stored scorecards (12,559 players; 421,654 batting and 293,284 bowling innings). `batting` / `bowling` are the career totals; `by_format` breaks the same numbers down by match format (Test, ODI, T20, IT20, MDM, ODM) and always sums back to the career line. Derived rates are null rather than 0 when undefined: no dismissals means no batting average, no wickets means no bowling average or best figures. An id with no cricket innings is a 404, not an empty profile.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. Career profile aggregated from stored scorecard rows.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "$ref": "#/components/schemas/StoredId"
                            },
                            "name": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "sport": {
                              "type": "string",
                              "enum": [
                                "cricket"
                              ]
                            },
                            "nationality": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "dob": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "format": "date"
                            },
                            "team": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "properties": {
                                "id": {
                                  "$ref": "#/components/schemas/StoredId"
                                },
                                "name": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                }
                              }
                            },
                            "career": {
                              "type": "object",
                              "properties": {
                                "matches": {
                                  "type": "integer",
                                  "description": "Distinct matches batted or bowled in."
                                },
                                "first_match": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "format": "date-time"
                                },
                                "last_match": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "format": "date-time"
                                },
                                "formats": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              }
                            },
                            "batting": {
                              "$ref": "#/components/schemas/CricketBattingCareer"
                            },
                            "bowling": {
                              "$ref": "#/components/schemas/CricketBowlingCareer"
                            },
                            "by_format": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "format": {
                                    "type": "string",
                                    "description": "Match format: Test, ODI, T20, IT20, MDM, ODM."
                                  },
                                  "batting": {
                                    "$ref": "#/components/schemas/CricketBattingCareer"
                                  },
                                  "bowling": {
                                    "$ref": "#/components/schemas/CricketBowlingCareer"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The path id is not a Big Balls player UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No cricket innings for this id. Distinguishes an id we hold in another sport (fetch it from GET /v1/players/{id}) from one we have never seen.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/fight-cards": {
      "get": {
        "tags": [
          "combat"
        ],
        "summary": "List fight cards (MMA / boxing)",
        "description": "Fight-card list, served from stored data. `data` is an ARRAY of cards, most recent first, each carrying its bouts.\n\n**MMA** cards are grouped by stored event identity, never by calendar date. Use `promotion=ufc` or `promotion=pfl` to select one promotion. Stored cards carry their own `id`; legacy UFC cards continue to accept a bout id for backward compatibility. Completed bouts may carry `winner`, `winner_name`, `method`, `round`, and `time`. A null result means those facts have not been loaded and is never interpreted as a draw.\n\n**Boxing** cards are real `fight_card` rows (Nevada commission results) and DO carry results: `winner` as a corner (`a`/`b`/`draw`/`nc`) mapped to the fighter names, `method`, `round`, `time` and the judges' scorecards. A `winner` of `null` means undetermined; it is accompanied by `winner_status: \"undetermined\"` and is NOT a draw. Scorecards are served per judge and are never summed into a score. A null `weight_class`, `event_name`, `promoter`, `scheduled_rounds` or `knockdowns` means the source did not report it, never zero.",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "mma",
                "boxing"
              ]
            },
            "description": "Required: disambiguates MMA from boxing."
          },
          {
            "in": "query",
            "name": "promotion",
            "schema": {
              "type": "string",
              "enum": [
                "ufc",
                "pfl"
              ]
            },
            "description": "MMA only. Restricts cards to one promotion so same-day events remain separate."
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "description": "ISO date (YYYY-MM-DD). UFC cards: a UTC calendar day of kickoff, echoed in meta.date_window. Boxing cards: the card's published event date, so meta.date_window carries null bounds."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. America/New_York). UFC: ?date= then selects the local calendar day in that zone. Boxing: validated but not applied, because a published event date is already the local date where the card takes place. Offsets such as -05:00 are rejected with 400. Example: ?sport=mma&date=2026-09-12&tz=America/New_York."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. An array of cards; empty when none match the filter (still 200).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid ?sport= (must be mma or boxing), or a bad ?date format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/fight-cards/{id}": {
      "get": {
        "tags": [
          "combat"
        ],
        "summary": "Single fight card",
        "description": "One card with its bouts, sorted by `bout_order` ascending. `{id}` may be a stored card id or the id of a bout on that card. Legacy UFC cards have no stored card row, so a bout id remains their stable lookup key.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "mma",
                "boxing"
              ]
            },
            "description": "Required: disambiguates MMA from boxing."
          },
          {
            "in": "query",
            "name": "promotion",
            "schema": {
              "type": "string",
              "enum": [
                "ufc",
                "pfl"
              ]
            },
            "description": "MMA only. Requires the resolved card to belong to this promotion."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. The card object.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid ?sport= (must be mma or boxing).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No card matches that id (or none are loaded for that sport).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/bouts/{id}": {
      "get": {
        "tags": [
          "combat"
        ],
        "summary": "Single bout",
        "description": "One bout. Boxing bouts carry the full result (corner winner, method, round, time, per-judge scorecards). MMA bouts carry those result fields when loaded; otherwise `result` is null. The id is the canonical match id, the same one `/v1/matches/{id}` resolves.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "mma",
                "boxing"
              ]
            },
            "description": "Required: disambiguates MMA from boxing."
          },
          {
            "in": "query",
            "name": "promotion",
            "schema": {
              "type": "string",
              "enum": [
                "ufc",
                "pfl"
              ]
            },
            "description": "MMA only. Requires the bout to belong to this promotion."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. The bout object.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid ?sport= (must be mma or boxing).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No bout matches that id (or none are loaded for that sport).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/athletes/{id}": {
      "get": {
        "tags": [
          "combat"
        ],
        "summary": "Single athlete (fighter / boxer)",
        "description": "Resolves the `players` field for one athlete. MMA and boxing share this namespace.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "mma",
                "boxing"
              ]
            },
            "description": "Required: disambiguates MMA from boxing."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data.players` is a FieldResult, or null when unavailable (still 200).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "players": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/FieldResult"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid ?sport= (must be mma or boxing).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/athletes/{id}/record": {
      "get": {
        "tags": [
          "combat"
        ],
        "summary": "Athlete fight record",
        "description": "Resolves the `historical` field for one athlete: the win/loss/draw record and past bouts.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "mma",
                "boxing"
              ]
            },
            "description": "Required: disambiguates MMA from boxing."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data.historical` is a FieldResult, or null when unavailable (still 200).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "historical": {
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/FieldResult"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid ?sport= (must be mma or boxing).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/stored/matches": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "List stored matches (DB-backed)",
        "description": "Paginated match list read directly from Postgres.",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "scheduled",
                "live",
                "finished",
                "cancelled"
              ]
            }
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "description": "ISO date (YYYY-MM-DD) as a UTC calendar day: kickoffs at or after 00:00Z that date and before 00:00Z the next. An evening game in the Americas often kicks off after 00:00Z and is listed under the NEXT date. The exact window applied is echoed in meta.date_window."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. America/New_York). ?date= then selects the local calendar day in that zone. Offsets such as -05:00 are rejected with 400. Example: ?sport=american_football&date=2026-09-13&tz=America/New_York."
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Case-insensitive (ILIKE) match against leagues.name: \"nba\", \"NBA\", \"Premier League\" all work.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month. CHANGED 2026-08-31. This previously returned the calendar year and said so here - \"?season=2025 returns only the 2025 half. Documented as-is.\" Documenting it did not make it usable: an NBA caller got 767 of 1,310 rows from another season, and an NHL caller got a complete, internally consistent, half-length season with no way to tell."
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "asc = oldest first (default); desc = most recent first."
          },
          {
            "in": "query",
            "name": "stage",
            "schema": {
              "type": "string",
              "enum": [
                "playoffs",
                "regular",
                "preseason"
              ]
            },
            "description": "Reads the derived `stage` column. Only `playoffs` is populated today (NBA/NHL via the odds ingest); regular-season rows carry NULL, so **?stage=regular returns nothing** until an ingestor writes the explicit value. Documented as-is so callers don't read empty as a bug."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. Note the non-standard shape (`{ data, pagination }`, no meta/error wrapper).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "required": [
                        "total",
                        "limit",
                        "offset"
                      ],
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/stored/matches/{id}": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "Single stored match with latest odds",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "x-bbs-field-entitlements": [
              {
                "path": "data.odds",
                "min_plan": "edge",
                "below_floor": "PRESENT-EMPTY"
              }
            ],
            "description": "ok. `data` is the match plus aggregated latest odds. `odds_available` is false for ~99% of matches today; when false an `odds_note` explains that empty odds is coverage, not a route bug.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "odds": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "odds_available": {
                          "type": "boolean"
                        },
                        "odds_note": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id (must be a UUID).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No stored match with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/stored/matches/{id}/h2h": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "Head-to-head history for a match's team pair",
        "description": "The last N finished matches between the same two teams (either home/away order). Each entry carries both scores, the home-perspective spread (from handicap odds where available), and which side covered.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data }`, an array of past meetings; no meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "spread": {
                            "oneOf": [
                              {
                                "type": "number"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "covered_by": {
                            "oneOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id or limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/stored/matches/{id}/special-teams-toi": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "Per-player special-teams time on ice for a match",
        "description": "Per-player power-play, short-handed, even-strength and overtime ice time for one match, in whole seconds, plus total ice time and shift count. Ice hockey requires the Edge plan or higher. Access for every other sport on this shared route is unchanged. UNITS AND ARITHMETIC: every value is whole seconds, and `even_strength_time_on_ice_seconds + power_play_time_on_ice_seconds + short_handed_time_on_ice_seconds` equals `time_on_ice_seconds`. `overtime_time_on_ice_seconds` is an OVERLAY that is already counted inside those three and must not be added to them. `player_id` is null when we have not yet bridged that player to a Big Balls player id; `player_name` is always present. Coverage is partial and is being extended by season, so a match we do not yet hold returns 200 with an empty `players` array and a `meta.coverage` note rather than a 404.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data: { match_id, players }, meta: { count, field_notes, coverage? } }`, a route-specific meta, not the standard ResponseMeta (no error wrapper).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "match_id",
                        "players"
                      ],
                      "properties": {
                        "match_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "players": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "player_id": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true
                              },
                              "player_name": {
                                "type": "string"
                              },
                              "position": {
                                "type": "string",
                                "nullable": true
                              },
                              "team_id": {
                                "type": "string",
                                "format": "uuid",
                                "nullable": true
                              },
                              "side": {
                                "type": "string",
                                "enum": [
                                  "home",
                                  "away"
                                ],
                                "nullable": true
                              },
                              "time_on_ice_seconds": {
                                "type": "integer",
                                "nullable": true
                              },
                              "even_strength_time_on_ice_seconds": {
                                "type": "integer",
                                "nullable": true
                              },
                              "power_play_time_on_ice_seconds": {
                                "type": "integer",
                                "nullable": true
                              },
                              "short_handed_time_on_ice_seconds": {
                                "type": "integer",
                                "nullable": true
                              },
                              "overtime_time_on_ice_seconds": {
                                "type": "integer",
                                "nullable": true
                              },
                              "shifts": {
                                "type": "integer",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "count"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "field_notes": {
                          "type": "object",
                          "properties": {
                            "units": {
                              "type": "string"
                            },
                            "components": {
                              "type": "string"
                            },
                            "player_id": {
                              "type": "string"
                            }
                          }
                        },
                        "coverage": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "description": "forbidden. The plan on the key is below the Edge floor for this sport. The message names the plan held and the plan required, and links the docs and pricing pages."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-bbs-min-plan": "edge",
        "x-bbs-min-plan-by-sport": {
          "ice_hockey": "edge"
        },
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/stored/matches/{id}/lineups": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "Stored lineups for a match",
        "description": "Home and away lineup entries from the best stored lineup data available for the match. MLB responses include player names, positions, jersey numbers and starter flags, including starting pitcher identification. MLB lineups (baseball) and NHL lineups (ice hockey) require the Edge plan or higher. Access for every other sport on this shared route is unchanged, including the soccer and cricket lineups served on Free. Coverage is partial, so matches without lineup data return empty arrays with `meta.available=false` and a coverage note.\nNHL (ice hockey) entries are a POST-GAME record and are never a pre-game projection. A match gains entries only after it has been played: they are the dressed skaters, both dressed goalies and the scratches, and `starter` is true for the one goalie per side who actually started the game. That is not a confirmed or projected pre-game starting goalie, and no tier returns one before puck drop. NHL entries also carry no forward-line number, no defence pairing and no power-play unit membership, because the upstream record does not express them.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data: { home, away }, meta: { available, coverage_note? } }`, a route-specific meta, not the standard ResponseMeta (no error wrapper).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "home",
                        "away"
                      ],
                      "properties": {
                        "home": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "player_id": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "position": {
                                "type": "string"
                              },
                              "jersey_number": {
                                "type": "integer"
                              },
                              "starter": {
                                "type": "boolean"
                              },
                              "field": {
                                "type": "string"
                              },
                              "value": {}
                            }
                          }
                        },
                        "away": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "player_id": {
                                "type": "string"
                              },
                              "name": {
                                "type": "string"
                              },
                              "position": {
                                "type": "string"
                              },
                              "jersey_number": {
                                "type": "integer"
                              },
                              "starter": {
                                "type": "boolean"
                              },
                              "field": {
                                "type": "string"
                              },
                              "value": {}
                            }
                          }
                        },
                        "bench": {
                          "type": "object",
                          "description": "Non-starters from the same stored observation, on the `lineup_shape=players` baseball shape only. Added 2026-10-05: these rows used to be returned INSIDE `home`/`away` carrying `starter: false`, so a lineup included players who did not start. They are not removed from the response, only moved here.",
                          "properties": {
                            "home": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "player_id": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "position": {
                                    "type": "string"
                                  },
                                  "jersey_number": {
                                    "type": "integer"
                                  },
                                  "starter": {
                                    "type": "boolean"
                                  }
                                }
                              }
                            },
                            "away": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "player_id": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "position": {
                                    "type": "string"
                                  },
                                  "jersey_number": {
                                    "type": "integer"
                                  },
                                  "starter": {
                                    "type": "boolean"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "available"
                      ],
                      "properties": {
                        "available": {
                          "type": "boolean"
                        },
                        "lineup_shape": {
                          "type": "string",
                          "enum": [
                            "players",
                            "raw_stats"
                          ]
                        },
                        "coverage_note": {
                          "type": "string"
                        },
                        "team_attribution": {
                          "type": "object",
                          "description": "How each player was attributed to home or away, on the `lineup_shape=players` baseball shape. `team_at_match` means every row carried the team the ingested box score put the player on for THIS match; `current_team` means every row fell back to the player's present roster team, which cannot distinguish a player who has since moved; `mixed` means both. `side_uncertain` is true whenever any row used the fallback.",
                          "properties": {
                            "basis": {
                              "type": "string",
                              "enum": [
                                "team_at_match",
                                "current_team",
                                "mixed"
                              ]
                            },
                            "team_at_match_rows": {
                              "type": "integer"
                            },
                            "current_team_fallback_rows": {
                              "type": "integer"
                            },
                            "side_uncertain": {
                              "type": "boolean"
                            }
                          }
                        },
                        "partial": {
                          "type": "boolean",
                          "description": "Present and true only when stored starter rows existed for this match but could not be served, so the lineup returned is known to be incomplete. Absent otherwise."
                        },
                        "dropped": {
                          "type": "object",
                          "description": "Per-reason counts of the rows behind `partial`. Present only alongside `partial`.",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "unparseable_starter_value": {
                              "type": "integer"
                            },
                            "no_team_attribution": {
                              "type": "integer"
                            },
                            "team_not_in_this_match": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "MLB starting lineups require the Edge plan or higher.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-min-plan-by-sport": {
          "ice_hockey": "edge",
          "baseball": "edge"
        },
        "x-bbs-capability": "lineups",
        "x-bbs-not-available-for-sports": [
          "american_football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/stored/matches/{id}/plays": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "Play-by-play for a stored match",
        "description": "Stored plays for any covered match. The full soccer minute-by-minute\nstream and NBA, NHL and NFL play-by-play all require Pro; lower tiers\nreceive a 403 naming the tier. A match whose sport cannot be resolved\nis treated as Pro. For Free soccer scoring events, use\n`GET /v1/matches/{id}/events` with its default `type=goals` view.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "scoring_only",
            "schema": {
              "type": "boolean"
            },
            "description": "Restrict to scoring plays. Accepted: true|1|yes|on and false|0|no|off; anything else is a 400 naming the parameter. false means OFF. Until 2026-09-20 every spelling of false was read as true and returned the filtered set."
          },
          {
            "in": "query",
            "name": "period",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Restrict to one period/quarter/inning."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data }`, an array of plays; no meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "sequence_number": {
                            "type": "integer"
                          },
                          "period": {
                            "type": "integer"
                          },
                          "period_display": {
                            "type": "string"
                          },
                          "clock": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "team_id": {
                            "oneOf": [
                              {
                                "type": "string",
                                "format": "uuid"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "player_id": {
                            "oneOf": [
                              {
                                "type": "string",
                                "format": "uuid"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "scoring_play": {
                            "type": "boolean"
                          },
                          "score_value": {
                            "oneOf": [
                              {
                                "type": "integer"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "home_score": {
                            "oneOf": [
                              {
                                "type": "integer"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "away_score": {
                            "oneOf": [
                              {
                                "type": "integer"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "coordinate_x": {
                            "oneOf": [
                              {
                                "type": "number"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "Shot/play location. For soccer shots this is the SHOOTER's position on a 0-100 normalised pitch, not where the ball crossed the goal line."
                          },
                          "coordinate_y": {
                            "oneOf": [
                              {
                                "type": "number"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "xg": {
                            "oneOf": [
                              {
                                "type": "number"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "Expected goals for this shot, 0-1. Populated on soccer shot rows; `null` on plays whose source does not model xG. A null means \"not supplied\", never \"zero chance\"."
                          },
                          "xgot": {
                            "oneOf": [
                              {
                                "type": "number"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "Expected goals on target. Populated only for shots that were on target, so it is `null` for misses and blocks by design."
                          },
                          "wallclock": {
                            "oneOf": [
                              {
                                "type": "string",
                                "format": "date-time"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id or query.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Pro or higher for covered play-by-play sports)"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-min-plan-by-sport": {
          "basketball": "pro",
          "ice_hockey": "pro",
          "american_football": "pro",
          "football": "pro",
          "baseball": "pro"
        },
        "x-bbs-capability": "play-by-play",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "ice_hockey",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/stored/matches/{id}/stats": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "Stored team + player stats for a match",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data: { team_stats, players }, meta: { available, team_stats_available, players_available, withheld, coverage_note? } }`, a route-specific meta, not the standard ResponseMeta (no error wrapper). The two `*_available` flags are separate deliberately: a soccer match commonly returns player stats and an empty team-stats array, and a single flag cannot express that. An empty `team_stats` is not a statement that we hold none; check `withheld`. `withheld` counts values we hold for this match that this response does not carry, so an empty array can be told apart from an absence; it is always present, including as zeroes. `coverage_note` is present only when at least one of the two arrays is empty; for soccer it points at `/v1/matches/{id}/statistics`, a second surface that also serves team-level soccer stats.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "meta": {
                      "type": "object",
                      "required": [
                        "available",
                        "team_stats_available",
                        "players_available",
                        "withheld"
                      ],
                      "properties": {
                        "available": {
                          "type": "boolean",
                          "description": "True when either array is non-empty. It describes THIS RESPONSE, not our holdings: see withheld."
                        },
                        "team_stats_available": {
                          "type": "boolean"
                        },
                        "players_available": {
                          "type": "boolean"
                        },
                        "withheld": {
                          "type": "object",
                          "required": [
                            "team_stats",
                            "players"
                          ],
                          "description": "Values we hold for this match that this response does not carry, because of a sourcing restriction on our side. Always present. `available: false` with a non-zero count here means we have the data and are not returning it, which is a different thing from having none.",
                          "properties": {
                            "team_stats": {
                              "type": "integer",
                              "description": "Distinct (team, stat) values held but not returned."
                            },
                            "players": {
                              "type": "integer",
                              "description": "Distinct (player, stat) values held but not returned."
                            }
                          }
                        },
                        "coverage_note": {
                          "type": "string",
                          "description": "Present only when at least one array is empty."
                        }
                      }
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "team_stats",
                        "players"
                      ],
                      "properties": {
                        "team_stats": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "team_id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "team_name": {
                                "type": "string"
                              },
                              "team_logo_url": {
                                "oneOf": [
                                  {
                                    "type": "string"
                                  },
                                  {
                                    "type": "null"
                                  }
                                ]
                              },
                              "field": {
                                "type": "string"
                              },
                              "label": {
                                "oneOf": [
                                  {
                                    "type": "string"
                                  },
                                  {
                                    "type": "null"
                                  }
                                ]
                              },
                              "display_value": {
                                "oneOf": [
                                  {
                                    "type": "string"
                                  },
                                  {
                                    "type": "null"
                                  }
                                ]
                              },
                              "fetched_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        },
                        "players": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/stored/players": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "List stored players (DB-backed)",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Team bbs_id (UUID)."
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string"
            },
            "description": "Canonical league key, brand alias or exact display name. `?league=` means the CURRENT league: since 2026-09-18 the filter is bounded to the clubs in that league's latest stored standings season, so it returns the clubs currently in the competition rather than every club whose `teams.league_id` still points at it. Measured in prod 2026-09-18, `?league=epl` went from 775 records across 23 clubs to 673 across 20; the 102 difference are players whose clubs are not in the current season. Send `?season=` to reach any earlier season. A league with no stored standings is not narrowed.",
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "The year the season STARTS, the same meaning `?season=` carries on every other endpoint. Bounds the directory to the clubs in that season's standings instead of the current one: `?league=epl&season=2025` returns the 2025-26 population (775 records across 23 clubs, measured 2026-09-18). Requires `?league=`, because without a league there is no standings table to bound by, so sending it alone is a 400 rather than a parameter that is accepted and ignored. A season we hold no standings for is a 400 naming the seasons we do hold."
          },
          {
            "in": "query",
            "name": "q",
            "schema": {
              "type": "string"
            },
            "description": "Name search."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, pagination, meta }`; no error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "required": [
                        "total",
                        "limit",
                        "offset"
                      ],
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "filters_applied"
                      ],
                      "properties": {
                        "filters_applied": {
                          "type": "object",
                          "description": "Echo of the scope this response was computed under, so the population is readable rather than inferred from a count.",
                          "required": [
                            "sport",
                            "team",
                            "league",
                            "season",
                            "q",
                            "league_scope"
                          ],
                          "properties": {
                            "sport": {
                              "type": "string",
                              "nullable": true
                            },
                            "team": {
                              "type": "string",
                              "nullable": true
                            },
                            "league": {
                              "type": "string",
                              "nullable": true
                            },
                            "season": {
                              "type": "integer",
                              "nullable": true
                            },
                            "q": {
                              "type": "string",
                              "nullable": true
                            },
                            "league_scope": {
                              "type": "object",
                              "nullable": true,
                              "description": "Present only when `?league=` was sent. `mode` names the population returned. `leagues` carries the standings season labels the rows were bounded to PER LEAGUE. An empty `seasons` array is a league with no standings under this scope, which under the default mode keeps the unnarrowed behaviour rather than returning nothing. `season` is a convenience that is populated only when exactly one league resolved to exactly one label, and is `null` otherwise, because one string cannot describe a multi-league filter or a season carrying two labels.",
                              "required": [
                                "mode",
                                "season",
                                "leagues"
                              ],
                              "properties": {
                                "mode": {
                                  "type": "string",
                                  "enum": [
                                    "latest_standings_season",
                                    "requested_season",
                                    "all_clubs_ever_attached"
                                  ]
                                },
                                "season": {
                                  "type": "string",
                                  "nullable": true
                                },
                                "leagues": {
                                  "type": "array",
                                  "nullable": true,
                                  "items": {
                                    "type": "object",
                                    "required": [
                                      "league_id",
                                      "seasons"
                                    ],
                                    "properties": {
                                      "league_id": {
                                        "type": "string",
                                        "format": "uuid"
                                      },
                                      "seasons": {
                                        "type": "array",
                                        "items": {
                                          "type": "string"
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        },
                        "current_participants": {
                          "type": "string",
                          "nullable": true,
                          "description": "Non-null only when a league-filtered response was NOT bounded to a season; names how to bound it."
                        },
                        "historical_directory": {
                          "type": "string",
                          "nullable": true,
                          "description": "Non-null on a season-bounded league response; names how to reach an earlier season, and the two-call path to every club ever attached to the league."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/stored/standings": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "Stored standings, grouped by league",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string"
            },
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "string"
            },
            "description": "Season LABEL, not a calendar year; the year a season STARTS in. Same parameter, same meaning and same matcher as /v1/standings. It was accepted by the route and undocumented here."
          },
          {
            "in": "query",
            "name": "conference",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "example": "Big Ten Conference",
            "description": "Narrow to one conference/division inside the league, case-insensitive. Same parameter and same matcher as /v1/standings."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data }`, standings groups; no meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "standings",
        "x-bbs-not-available-for-sports": [
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/stored/sports": {
      "get": {
        "tags": [
          "stored"
        ],
        "summary": "Sports present in the stored match table, with counts",
        "description": "Distinct sport_type values in public_matches with a match count each, most matches first. No parameters.",
        "responses": {
          "200": {
            "description": "ok. `{ data }`; no meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "sport",
                          "match_count"
                        ],
                        "properties": {
                          "sport": {
                            "type": "string"
                          },
                          "match_count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/assets/{id}": {
      "get": {
        "tags": [
          "assets"
        ],
        "summary": "Fetch an image we host",
        "description": "Serves a team crest or player headshot from our own storage. Every `logo_url`, `headshot_url`, `photo_url` and `flag_url` in any response points here; no response field ever points at a third-party host.\n\n`id` is opaque and is the only supported way to reach an asset. There is no parameter that accepts a URL, and an id this API did not issue is refused. Do not parse, construct, truncate or derive anything from it; treat it as a bearer string and store it whole. The same image always gets the same id, so the URL is stable and safe to cache or persist.\n\nNo API key is required, and an authenticated request returns byte-identical content to an unauthenticated one. Responses carry `cache-control: public, max-age=31536000, immutable`, so you may cache and store the bytes, including in your own infrastructure, for as long as that header allows.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "description": "The opaque id exactly as it appeared in a response URL.",
            "schema": {
              "type": "string",
              "minLength": 40,
              "maxLength": 2048
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The image.",
            "headers": {
              "Cache-Control": {
                "description": "Always `public, max-age=31536000, immutable`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/svg+xml": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "403": {
            "description": "The id is well-formed but names an asset this API does not serve.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No asset of ours is named by this id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "We do not yet hold a copy of this image and could not retrieve it. Retry; the copy is fetched on first request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "504": {
            "description": "The image could not be retrieved in time. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/teams": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "List teams",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "in": "query",
            "name": "league",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "x-bbs-known-callable-values": [
              "Africa Cup of Nations",
              "bbl",
              "bpl",
              "bundesliga",
              "CONCACAF Gold Cup",
              "Copa America",
              "cpl",
              "epl",
              "Euro Championship",
              "f1",
              "hundred-men",
              "hundred-women",
              "ipl",
              "laliga",
              "ligue-1",
              "ligue1",
              "mlb",
              "mls",
              "MLS",
              "nba",
              "ncaab",
              "ncaaf",
              "ncaaf-fcs",
              "nfl",
              "nhl",
              "odi",
              "psl",
              "sa20",
              "serie-a",
              "seriea",
              "t20i",
              "test",
              "ucl",
              "UEFA Champions League",
              "UEFA Nations League",
              "ufc",
              "wbbl",
              "wbc",
              "wc2026",
              "wnba",
              "womens-super-smash"
            ],
            "x-bbs-values-are-exhaustive": false
          },
          {
            "in": "query",
            "name": "tournament",
            "schema": {
              "type": "string",
              "enum": [
                "wc2026"
              ]
            },
            "description": "Shorthand for a tournament league, so callers need not memorise its UUID."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. Non-standard shape: `{ data, pagination }`, no meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Team"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "required": [
                        "total",
                        "limit",
                        "offset"
                      ],
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/teams/{id}/matches": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Matches for a team",
        "description": "Resolves the `historical` field for one team. Returns the standard Envelope with a FieldMap.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Sport"
            },
            "description": "Required."
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}(-\\d{2,4})?$"
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Alias for ?offset=(page-1)*limit. An explicit ?offset= wins."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `data.historical` is a FieldResult, or null when unavailable (still 200).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "oneOf": [
                            {
                              "type": "object",
                              "properties": {
                                "historical": {
                                  "oneOf": [
                                    {
                                      "$ref": "#/components/schemas/FieldResult"
                                    },
                                    {
                                      "type": "null"
                                    }
                                  ]
                                }
                              }
                            },
                            {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/Match"
                              }
                            }
                          ]
                        },
                        "meta": {
                          "type": "object"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid ?sport=, or an invalid ?season=/?limit=.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/teams/{id}/ats": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Against-the-spread record",
        "description": "ATS record with home/away and favourite/underdog splits, plus a per-sport breakdown. **Edge plan or higher**: these are model-ready betting features; Free / Solo / Trio receive a 403 naming the tier and linking the docs and pricing pages.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data }` with ats_record, ats_win_pct, sample_size, sport_breakdown and the home/away + favourite/underdog splits. No meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "ats_record": {
                          "type": "object"
                        },
                        "ats_win_pct": {
                          "oneOf": [
                            {
                              "type": "number"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "sample_size": {
                          "type": "integer"
                        },
                        "sport_breakdown": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid team id or season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/teams/{id}/ats-record": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Against-the-spread record (alias of /v1/teams/{id}/ats)",
        "description": "ALIAS. This route is bound to the exact same handler as GET /v1/teams/{id}/ats and returns an identical payload. Kept for back-compat; prefer /ats. **Edge plan or higher**: the same gate applies here, since it is the same handler.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. Identical to GET /v1/teams/{id}/ats.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid team id or season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/teams/{id}/trends": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Betting + performance trends for a team",
        "description": "Recent-form trend rates, computed from finished matches with odds: ATS cover rates over the last 5 and 10, over/under rates, home vs away ATS, and favourite vs underdog ATS. **Edge plan or higher**: the same ATS product as /v1/teams/{id}/ats in a rolling window; Free / Solo / Trio receive a 403 naming the tier and linking the docs and pricing pages.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "sport",
            "schema": {
              "$ref": "#/components/schemas/Sport"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data }` with sample_size and the trend rates. Rates are null when the sample is empty. No meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "sample_size": {
                          "type": "integer"
                        },
                        "ats_last_5": {
                          "oneOf": [
                            {
                              "type": "number"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "ats_last_10": {
                          "oneOf": [
                            {
                              "type": "number"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "ou_last_5": {
                          "oneOf": [
                            {
                              "type": "number"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "ou_last_10": {
                          "oneOf": [
                            {
                              "type": "number"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "home_ats": {
                          "oneOf": [
                            {
                              "type": "number"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "away_ats": {
                          "oneOf": [
                            {
                              "type": "number"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "as_favorite_ats": {
                          "oneOf": [
                            {
                              "type": "number"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid team id or query.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/teams/{id}/schedule-context": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Rest + schedule context for a team",
        "description": "Fatigue/rest context as of a date: days of rest, the last 5 games, games in the trailing 7 days, whether the team is on a back-to-back, and the current home/away streak. **Edge plan or higher**: a model-ready rest/fatigue feature vector; Free / Solo / Trio receive a 403 naming the tier and linking the docs and pricing pages.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            },
            "description": "Reference date (YYYY-MM-DD), a UTC calendar day. Defaults to today (UTC)."
          },
          {
            "in": "query",
            "name": "tz",
            "schema": {
              "type": "string",
              "example": "America/New_York"
            },
            "description": "Optional IANA time zone name (e.g. America/New_York). The reference date, today, and the calendar days behind days_rest, back_to_back and games_last_7_days then use local days in that zone. Offsets such as -05:00 are rejected with 400. Example: ?date=2026-09-13&tz=America/New_York."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data }` with team_id, as_of, days_rest, last_5_games, games_last_7_days, back_to_back and home_away_streak. No meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "team_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "as_of": {
                          "type": "string"
                        },
                        "days_rest": {
                          "oneOf": [
                            {
                              "type": "integer"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "games_last_7_days": {
                          "type": "integer"
                        },
                        "back_to_back": {
                          "type": "boolean"
                        },
                        "home_away_streak": {
                          "type": "object",
                          "properties": {
                            "type": {
                              "oneOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "count": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid team id or date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/teams/{id}/h2h-intelligence": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Head-to-head intelligence vs one opponent",
        "description": "Head-to-head record and betting context against a specific opponent, including a trailing-365-day record, revenge context, cover/over rates and the last 3 meetings. **Solo plan or higher**: head-to-head intelligence starts at Solo; Free receives a 403 naming the tier and linking the docs and pricing pages.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/StoredId"
            }
          },
          {
            "in": "query",
            "name": "opponent_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Required. The other team's UUID. Must differ from the path id, or the request returns 400 with failing_field=opponent_id."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data }` with h2h_record, h2h_last_365_days, revenge_factor, revenge_context, h2h_cover_rate, h2h_over_rate and last_3_meetings. No meta/error wrapper.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "team_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "opponent_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "h2h_record": {
                          "type": "object"
                        },
                        "h2h_last_365_days": {
                          "type": "object"
                        },
                        "last_3_meetings": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid team id, missing ?opponent_id=, or opponent_id equal to the team id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/teams/{id}/season": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Team season summary",
        "description": "Season summary for a team; sport is auto-detected from the team record. HONEST COVERAGE: NBA and NHL are complete. WC2026 returns meta and standings only, with splits/last_10/top_players/season_stats empty until tournament data lands. STANDINGS GAP: the standings table carries no conference or division columns, so conference_rank and division_rank return null. `overall_rank` and the rank-1 leader that `games_back` is computed against use the SAME ordering and tiebreak as the `rank` field on GET /v1/standings; see the StandingRow.rank description. Before 2026-09-24 this route ran its own wins/(wins+losses) comparator, which ignored draws and so ordered association-football leagues wrongly.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "string"
            },
            "description": "Accepted, but ONLY the current season is loaded. Any other value is rejected with a 400 naming the available seasons, rather than returning data for the wrong season."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` where meta carries cache info (a route-local meta, not the standard ResponseMeta).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid team id (must be UUID), or a ?season= other than the current season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/teams/{id}/depth-chart": {
      "get": {
        "tags": [
          "teams"
        ],
        "summary": "Depth chart inferred from playing time",
        "description": "Depth chart derived from playing time (NBA by minutes, NHL goalies by games played). Sports other than NBA/NHL return `supported: false` rather than an invented chart.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 84,
              "default": 5
            },
            "description": "Minimum games played for a player to appear (NHL seasons contain 84 games beginning in 2026-27)."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`. `data.supported` is false for sports without a depth-chart rule (everything outside NBA/NHL today).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "sport": {
                          "type": "string"
                        },
                        "supported": {
                          "type": "boolean"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid team id or query.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No team with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}/profile": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Extended player profile",
        "description": "Player profile with team + biographical context. No query parameters. Cached.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` where meta is a route-local cache stamp (cached, cache_age_ms).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No player with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}/game-log": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Filterable per-game log for a player",
        "description": "Game-by-game log with filters. DISTINCT from GET /v1/players/{id}/gamelog, which is a separate route with its own query schema. This one supports opponent/venue/result filtering and pagination.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "opponent",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            }
          },
          {
            "in": "query",
            "name": "venue",
            "schema": {
              "type": "string",
              "enum": [
                "home",
                "away"
              ]
            }
          },
          {
            "in": "query",
            "name": "result",
            "schema": {
              "type": "string",
              "enum": [
                "W",
                "L"
              ]
            }
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": [
                "date_asc",
                "date_desc"
              ],
              "default": "date_desc"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id or query (e.g. limit above 100).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No player with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}/rolling-stats": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Rolling averages over a window",
        "description": "Rolling per-game averages for a player over the chosen window. **Solo plan or higher**: rolling player form starts at Solo; Free receives a 403 naming the tier and linking the docs and pricing pages.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "window",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d",
                "365d",
                "season"
              ],
              "default": "30d"
            },
            "description": "Rolling window. Defaults to 30d."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`; data carries the player, the window, and the rolling per-game averages.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "window": {
                          "type": "string",
                          "enum": [
                            "7d",
                            "30d",
                            "365d",
                            "season"
                          ]
                        },
                        "player": {
                          "type": "object"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id or window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          },
          "404": {
            "description": "No player with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "solo",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/players/{id}/season-projection": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Season projection for a player",
        "description": "Projected season totals/rates from the player's season to date. No query parameters. Cached. **Solo plan or higher**: season projections start at Solo; Free receives a 403 naming the tier and linking the docs and pricing pages.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` with a route-local cache stamp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          },
          "404": {
            "description": "No player with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/players/{id}/streak": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Active streaks for a player",
        "description": "Current active streaks. No query parameters. Cached. **Solo plan or higher**: streak detection starts at Solo; Free receives a 403 naming the tier and linking the docs and pricing pages.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` with a route-local cache stamp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          },
          "404": {
            "description": "No player with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/players/{id}/birthday-splits": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Performance splits around a player's birthday",
        "description": "Per-game averages split three ways: games on the birthday, games inside the birthday window, and all other games.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "window_days",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 10,
              "default": 3
            },
            "description": "Days either side of the birthday that count as the window. Defaults to 3."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data }` with on_birthday, birthday_window and other_games, each carrying games_played and per_game averages.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "on_birthday": {
                          "type": "object",
                          "properties": {
                            "games_played": {
                              "type": "integer"
                            },
                            "per_game": {
                              "type": "object"
                            }
                          }
                        },
                        "birthday_window": {
                          "type": "object",
                          "properties": {
                            "games_played": {
                              "type": "integer"
                            },
                            "per_game": {
                              "type": "object"
                            }
                          }
                        },
                        "other_games": {
                          "type": "object",
                          "properties": {
                            "games_played": {
                              "type": "integer"
                            },
                            "per_game": {
                              "type": "object"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id or window_days.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No player with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/players/{id}/tier": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player quality tier",
        "description": "Quality tier with overall and positional percentile. HONEST COVERAGE: NBA and NHL only. A player outside those leagues, or one without enough data to tier, returns `tier: null` with a `reason` string explaining why (there is no `supported` flag on this route). No query parameters. **Solo plan or higher**: player tiering starts at Solo; Free receives a 403 naming the tier and linking the docs and pricing pages.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`. An untiered player returns tier null plus a reason, not an error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "tier": {
                          "oneOf": [
                            {
                              "type": "integer"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "tier_label": {
                          "oneOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "reason": {
                          "oneOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Solo or higher)"
          },
          "404": {
            "description": "No player with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/nba/leaders": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA statistical leaders",
        "description": "One row per player across the requested period. `period=season` uses the\nselected season; `period=month` and `period=week` use trailing windows\nwithin that season. `data.row_grain` is `player-period` in every case.\nThe team label is the latest non-null team in that window and does not\npartition the totals.\n",
        "parameters": [
          {
            "in": "query",
            "name": "stat",
            "schema": {
              "type": "string",
              "enum": [
                "ast",
                "blk",
                "dreb",
                "eff",
                "fg3a",
                "fg3m",
                "fg3_pct",
                "fga",
                "fgm",
                "fg_pct",
                "fta",
                "ftm",
                "ft_pct",
                "min",
                "oreb",
                "pts",
                "reb",
                "stl",
                "tov",
                "ts_pct",
                "per"
              ],
              "default": "pts"
            },
            "description": "Per-game leaderboard category. The first 19 mirror the upstream GOAT leaders vocabulary; ts_pct and per are BBS-derived additions."
          },
          {
            "in": "query",
            "name": "period",
            "schema": {
              "type": "string",
              "enum": [
                "season",
                "month",
                "week"
              ],
              "default": "season"
            },
            "description": "Requested aggregation period. month and week are trailing windows within the selected season."
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "regular",
                "playoffs"
              ],
              "default": "all"
            },
            "description": "Filter stored box scores by regular season or playoffs. all preserves this endpoint's historical whole-campaign behavior."
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "x-bbs-field-entitlements": [
              {
                "path": "data.leaders[].player.headshot_url",
                "min_plan": "solo",
                "below_floor": "PRESENT-NULL"
              }
            ],
            "description": "ok. `{ data, meta }` with a route-local cache stamp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "row_grain",
                        "team_attribution",
                        "stat",
                        "period",
                        "season_type",
                        "season",
                        "qualified_min_games",
                        "leaders"
                      ],
                      "properties": {
                        "row_grain": {
                          "type": "string",
                          "enum": [
                            "player-period"
                          ],
                          "description": "One row per player across the requested period."
                        },
                        "team_attribution": {
                          "type": "string",
                          "enum": [
                            "latest_non_null_team_in_window"
                          ],
                          "description": "The display team is the latest non-null team in the requested period and does not partition the totals."
                        },
                        "stat": {
                          "type": "string",
                          "enum": [
                            "ast",
                            "blk",
                            "dreb",
                            "eff",
                            "fg3a",
                            "fg3m",
                            "fg3_pct",
                            "fga",
                            "fgm",
                            "fg_pct",
                            "fta",
                            "ftm",
                            "ft_pct",
                            "min",
                            "oreb",
                            "pts",
                            "reb",
                            "stl",
                            "tov",
                            "ts_pct",
                            "per"
                          ]
                        },
                        "period": {
                          "type": "string",
                          "enum": [
                            "season",
                            "month",
                            "week"
                          ]
                        },
                        "season_type": {
                          "type": "string",
                          "enum": [
                            "all",
                            "regular",
                            "playoffs"
                          ]
                        },
                        "season": {
                          "type": "integer"
                        },
                        "qualified_min_games": {
                          "type": "integer"
                        },
                        "leaders": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "rank",
                              "player",
                              "team",
                              "value",
                              "games_played"
                            ],
                            "properties": {
                              "rank": {
                                "type": "integer"
                              },
                              "player": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "name",
                                  "headshot_url"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string"
                                  },
                                  "headshot_url": {
                                    "type": "string",
                                    "nullable": true
                                  }
                                }
                              },
                              "team": {
                                "type": "object",
                                "required": [
                                  "id",
                                  "abbreviation"
                                ],
                                "properties": {
                                  "id": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "abbreviation": {
                                    "type": "string",
                                    "nullable": true
                                  }
                                }
                              },
                              "value": {
                                "type": "number",
                                "nullable": true
                              },
                              "games_played": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nba/players/{id}/splits": {
      "get": {
        "tags": [
          "nba"
        ],
        "summary": "NBA player splits",
        "description": "Per-split blocks, each carrying games_played and a win/loss record.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1900,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id or season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No player with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/nhl/games/{game_id}/matchup": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL game matchup",
        "description": "Matchup context for one NHL game.",
        "parameters": [
          {
            "in": "path",
            "name": "game_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid game_id (must be UUID).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No NHL game with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/nhl/games/{game_id}/plays": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL play-by-play",
        "description": "**Pro plan or higher**: all NHL play-by-play requires Pro; Free /\nSolo / Trio / Edge receive a 403 naming the tier and linking the docs\nand pricing pages. Mirrors NFL and NBA play-by-play's tier (\"all play\nby play is pro\"); NBA's Edge-and-up gate was retired on 2026-09-18.\n\n`nhl_play_by_play` mirrors `nba_play_by_play`'s schema (migration\n057). Two columns the migration declares are 0% populated in\nproduction (verified against all 434,815 rows) and are never served:\n`text` and `score_value`. `team_id` (~84%) and `coordinate_x`/\n`coordinate_y` (~83%) are null on faceoffs/stoppages with no team or\nspatial attribution; `home_score`/`away_score` (~2%) are stamped only\non the goal event itself. `shooting_only=true` turns the response into\na shot-chart feed and adds our licensed play-derived xG, distance,\nangle, rebound, rush and empty-net fields where the xG model covers\nthe event. Coverage includes the complete 2025-26 season; the licensed\n2026-27 feed begins with the September 29 regular-season opener.\n",
        "parameters": [
          {
            "in": "path",
            "name": "game_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "play_type",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 30
            }
          },
          {
            "in": "query",
            "name": "period",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10
            }
          },
          {
            "in": "query",
            "name": "scoring_only",
            "schema": {
              "type": "boolean"
            },
            "description": "Return scoring plays only. Accepted: true|1|yes|on and false|0|no|off; anything else is a 400 naming the parameter."
          },
          {
            "in": "query",
            "name": "shooting_only",
            "schema": {
              "type": "boolean"
            },
            "description": "Return shot events only; rows include shot geometry and computed xG fields when available. Accepted: true|1|yes|on and false|0|no|off; anything else is a 400 naming the parameter. false means OFF. Measured 2026-09-20 on game 9aac43c2-...-d4127f1f7147, ?shooting_only=false returned 119 of 361 plays because Boolean(\"false\") is true."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, pagination, meta }`; `meta.pbp_available: false` when the match exists but this vendor never covered it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "plan_required (Pro or higher)"
          },
          "404": {
            "description": "NHL match not found"
          }
        },
        "x-bbs-min-plan": "pro",
        "x-bbs-capability": "play-by-play",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "ice_hockey",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nhl/games/{game_id}/box-scores": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL per-game player box score",
        "description": "Per-player, per-game box score (goals, assists, points, shots on\ngoal, plus/minus, penalty minutes, power-play goals, blocked shots,\nhits, giveaways, takeaways, faceoff win %, time on ice, shifts), plus\ngoalie saves, shots/goals against, save percentage, starter/decision,\nand even-strength/power-play/short-handed goaltending splits.\nSport-entitlement + Claim-9 history-depth gated (season resolved\nfrom the game itself, not a `?season=` parameter), NOT the\nplay-by-play paywall above; same product class as\n`/v1/players/{id}/gamelog`. Backed by `nhl_box_scores_raw`\n(migration 203), which carries no typed stat columns; every field\nis promoted from the vendor's raw JSONB blob at query time. Coverage\nis the complete, finished 2025-26 season.\n",
        "parameters": [
          {
            "in": "path",
            "name": "game_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`; `meta.box_score_available: false` when the match exists but this vendor never covered it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "sport_not_included or history_not_included"
          },
          "404": {
            "description": "NHL match not found"
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/nhl/leaders": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL statistical leaders",
        "description": "Goals, assists, points and shots are licensed all-situations box-score\ntotals. `xg` is the compatibility view of our own all-situations shot\nmodel over licensed play-by-play, joined to those season totals to\npreserve the legacy `min_games` filter and response envelope. Only\nseasons present in that computed model are available for `xg`; an\nunsupported historical season returns 404 rather than falling back to\nsource-restricted rows. The additional metrics are full-situation\nseason totals and are included only where the box-score aggregate does\nnot already hold the same field.\n",
        "parameters": [
          {
            "in": "query",
            "name": "stat",
            "schema": {
              "type": "string",
              "enum": [
                "goals",
                "assists",
                "points",
                "shots",
                "xg",
                "even_strength_goals",
                "even_strength_points",
                "faceoff_win_pct",
                "game_winning_goals",
                "games_started",
                "losses",
                "ot_losses",
                "overtime_goals",
                "power_play_points",
                "short_handed_goals",
                "short_handed_points",
                "wins"
              ],
              "default": "goals"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2014,
              "maximum": 2099
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ],
              "default": "regular"
            }
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 84,
              "default": 10
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` with a route-local cache stamp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nhl/team-leaders": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL team rate leaders",
        "description": "Team leaderboard for the five rate dimensions not already held by our\nstandings or skater-leader data. Percentages are returned as fractions (0..1).\n",
        "parameters": [
          {
            "in": "query",
            "name": "stat",
            "schema": {
              "type": "string",
              "enum": [
                "faceoff_win_percentage",
                "penalty_kill_percentage",
                "power_play_percentage",
                "shots_against_per_game",
                "shots_for_per_game"
              ],
              "default": "penalty_kill_percentage"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1917,
              "maximum": 2099
            }
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ],
              "default": "regular"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 32
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No NHL team season stats available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nhl/xg-leaders": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL skater expected-goals leaderboard (our own model)",
        "description": "Expected goals (xG) computed entirely by us from a paid aggregator's\nNHL play-by-play (nhl_play_by_play), which we hold clear commercial\nredistribution rights to. GET /v1/nhl/leaders?stat=xg uses this same\nmodel as a compatibility view with the legacy response envelope. The\nmodel is a\nhierarchical shot-distance/angle/type/rebound/rush lookup table with\nempty-net shots modeled separately; it deliberately does not model\nstrength state (power play vs even strength); see\npackages/orchestrator/scripts/compute-nhl-xg.ts for why. `source` in\nthe response is an opaque tier label (`aggregator-paid`), same as\nevery other paid-aggregator-derived surface.\n",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2023,
              "maximum": 2099
            },
            "description": "Season LABEL: the year the season starts. Only seasons with ingested paid-aggregator NHL play-by-play are available."
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ],
              "default": "regular"
            }
          },
          {
            "in": "query",
            "name": "min_shots",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 20
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` with a route-local cache stamp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No computed NHL xG data available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "expected goals",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nhl/goalies/gsax": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL goalie goals-saved-above-expected (our own model)",
        "description": "Goals saved above expected (GSAx = expected goals against minus\nactual goals against, over shots faced), the natural derivative of\nour own shot-level xG model (see GET /v1/nhl/xg-leaders and\npackages/orchestrator/scripts/compute-nhl-xg.ts). Empty-net goals\nagainst are excluded from a goalie's own shots-faced count. Higher\nGSAx is better (more goals prevented than an average defended shot\nof that quality would predict).\n",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2023,
              "maximum": 2099
            }
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ],
              "default": "regular"
            }
          },
          {
            "in": "query",
            "name": "min_shots",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 2000,
              "default": 100
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` with a route-local cache stamp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No computed NHL xG data available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "expected goals",
        "x-bbs-not-available-for-sports": [
          "football",
          "basketball",
          "baseball",
          "american_football",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nhl/skaters/xg-splits": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL skater expected goals by on-ice strength, 2014-2025 (Edge)",
        "description": "Season-aggregate skater expected goals broken out by ON-ICE STRENGTH\nSTATE - even strength, power play, short-handed and everything else -\nfor twelve complete seasons, 2014 through 2025.\n\nNOT a second answer to GET /v1/nhl/xg-leaders, which stays free and\nunchanged. That route is OUR OWN shot-level model: current, updating\ndaily, and deliberately strength-state agnostic. This one adds the two\nthings that model does not have - ten further seasons of history, and\nthe strength-state split itself.\n\nA FROZEN ARCHIVE, AND THE RESPONSE SAYS SO. These rows have no writer.\n`data.as_of` carries the real data date for the exact slice served,\nscoped to the same season and game_type, so staleness is readable from\nthe response rather than from a changelog. The 2026-27 season is not\npresent and will not be added. If you want the current season, use\nGET /v1/nhl/xg-leaders.\n\n`player.id` is a Big Balls player UUID where we have bridged the\nathlete and `null` where we have not; `player.name` is always present.\nUnbridged rows are returned rather than dropped, so filter on\n`player.id` yourself if you need only bridged athletes.\n\nRequires the Edge plan or higher.\n",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2014,
              "maximum": 2099
            },
            "description": "Season LABEL: the year the season starts. 2014 to 2025 are present; later seasons are not and will not be added. Defaults to the newest season held for the requested game_type."
          },
          {
            "in": "query",
            "name": "game_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ],
              "default": "regular"
            }
          },
          {
            "in": "query",
            "name": "situation",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "5on5",
                "5on4",
                "4on5",
                "other"
              ],
              "default": "all"
            },
            "description": "On-ice strength state. `5on4` is the player's team on the power play, `4on5` short-handed, `other` every remaining state."
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z.]{2,4}$"
            },
            "description": "Team abbreviation, case-insensitive (e.g. EDM)."
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` with a route-local cache stamp. `data.as_of` is the real data date for the slice served, taken from the rows themselves.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Plan does not include this capability. Requires Edge or higher; the floor is enforced before validation, so a 403 is returned even for an invalid query.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No NHL xG season splits available for the requested game_type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/nhl/goalies/xg-splits": {
      "get": {
        "tags": [
          "nhl"
        ],
        "summary": "NHL goalie expected goals by strength and shot danger, 2014-2025 (Edge)",
        "description": "Season-aggregate goalie expected goals against, broken out by ON-ICE\nSTRENGTH STATE and by SHOT DANGER (low, medium, high), for twelve\ncomplete seasons, 2014 through 2025. Also carries flurry-adjusted\nexpected goals, and expected versus actual rebounds and freezes.\n\nNOT a replacement for GET /v1/nhl/goalies/gsax, which stays free and\nunchanged. That route is our own current-season model and gives one\nGSAx number per goalie. This one decomposes the same idea across\nstrength states and danger zones, over a decade our own model does not\nreach, so the two answer different questions. `goals_saved_above_expected`\nappears in both and the two numbers are independent estimates; they will\nnot agree exactly, and comparing them is a legitimate use.\n\nA FROZEN ARCHIVE, as above: no writer, `data.as_of` carries the real\ndata date for the slice served, and the 2026-27 season is absent and\nwill stay absent. Ordered by goals saved above expected, descending.\n\n`goalie.id` is `null` where the athlete is not bridged; `goalie.name`\nis always present, and unbridged rows are returned rather than dropped.\n\nRequires the Edge plan or higher.\n",
        "parameters": [
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2014,
              "maximum": 2099
            },
            "description": "Season LABEL: the year the season starts. 2014 to 2025 are present; later seasons are not and will not be added. Defaults to the newest season held for the requested game_type."
          },
          {
            "in": "query",
            "name": "game_type",
            "schema": {
              "type": "string",
              "enum": [
                "regular",
                "playoffs"
              ],
              "default": "regular"
            }
          },
          {
            "in": "query",
            "name": "situation",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "5on5",
                "5on4",
                "4on5",
                "other"
              ],
              "default": "all"
            },
            "description": "On-ice strength state. `5on4` is the player's team on the power play, `4on5` short-handed, `other` every remaining state."
          },
          {
            "in": "query",
            "name": "team",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z.]{2,4}$"
            },
            "description": "Team abbreviation, case-insensitive (e.g. EDM)."
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` with a route-local cache stamp. `data.as_of` is the real data date for the slice served, taken from the rows themselves.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Plan does not include this capability. Requires Edge or higher; the floor is enforced before validation, so a 403 is returned even for an invalid query.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No NHL xG season splits available for the requested game_type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "edge"
      }
    },
    "/v1/mlb/leaders": {
      "get": {
        "tags": [
          "mlb"
        ],
        "summary": "MLB batting/pitching season leaderboard",
        "description": "Season batting/pitching leaderboard, computed at request time from\n`player_stats` (no pre-aggregated season table exists yet; see\ndocs/baseball-player-stats-gap-scoping-2026-08-05.md). Batting and\npitching are split per the same rule GET /v1/players/{id}/stats?\nsport=baseball uses: a player-game is pitching iff it carries\nera/earnedRuns/innings-pitched, otherwise batting.\nHONESTY: player_stats currently covers the 2026 season ONLY (no\nearlier seasons exist yet), and current-season coverage itself is\npartial (~76% of finished games at time of writing); every\nresponse's `meta` carries `season_note`, `finished_games`,\n`games_with_stats`, and `coverage_pct` so this is never presented as\ncomplete or historical. `stat=era` sorts ascending (lower is\nbetter); every other stat sorts descending. 5-minute cache.\n",
        "parameters": [
          {
            "in": "query",
            "name": "category",
            "schema": {
              "type": "string",
              "enum": [
                "batting",
                "pitching"
              ],
              "default": "batting"
            }
          },
          {
            "in": "query",
            "name": "stat",
            "schema": {
              "type": "string",
              "enum": [
                "home_runs",
                "avg",
                "hits",
                "rbi",
                "runs",
                "stolen_bases",
                "walks",
                "strikeouts",
                "era",
                "innings_pitched",
                "hits_allowed",
                "earned_runs",
                "home_runs_allowed"
              ]
            },
            "description": "Defaults to home_runs for batting, era for pitching. Must belong to the requested category (400 otherwise)."
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2020,
              "maximum": 2099
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 10
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`; meta carries the coverage-honesty fields described above.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        },
                        "season_note": {
                          "type": "string"
                        },
                        "finished_games": {
                          "type": "integer"
                        },
                        "games_with_stats": {
                          "type": "integer"
                        },
                        "coverage_pct": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters, or a stat that does not belong to the requested category.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No baseball player_stats available for any season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nfl/leaders": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "NFL season leaderboard",
        "description": "Season leaderboard over stored weekly NFL player statistics. The route\nsums the selected stat across the requested season and season type,\nthen ranks players in descending order. `season` defaults to the newest\npopulated season for that season type. `player_id` is the established\nNFL player identifier also used by `/v1/nfl/players/{id}/stats`, not a\nBigBalls player UUID. Responses are cached for five minutes.\n\nThis code-only stage deliberately makes no `data_as_of` claim. The base\ntable records real value-change timestamps, but production's public\nview does not expose that column until migration 285 is separately\napplied. A missing freshness field is more accurate than substituting a\nrow-creation timestamp or cron heartbeat.\n",
        "parameters": [
          {
            "in": "query",
            "name": "stat",
            "schema": {
              "type": "string",
              "enum": [
                "fantasy_points_ppr",
                "fantasy_points",
                "passing_yards",
                "passing_tds",
                "rushing_yards",
                "rushing_tds",
                "receiving_yards",
                "receiving_tds",
                "receptions",
                "targets",
                "interceptions_thrown",
                "sacks",
                "tackles_solo"
              ],
              "default": "fantasy_points_ppr"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 1999,
              "maximum": 2099
            },
            "description": "Defaults to the newest populated season for the requested season_type."
          },
          {
            "in": "query",
            "name": "season_type",
            "schema": {
              "type": "string",
              "enum": [
                "REG",
                "POST"
              ],
              "default": "REG"
            }
          },
          {
            "in": "query",
            "name": "min_games",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 22,
              "default": 1
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Season leaderboard and cache metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No NFL player statistics are available for any season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/nfl/surfaces": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "Catalogue of available NFL snapshot surfaces",
        "description": "Lists every NFL snapshot surface we store, with its row count, the\nseasons present, the plan needed to read it and when it last updated.\nCall this first: it is the discovery endpoint for\nGET /v1/nfl/surfaces/{surface}, and it tells you what a surface costs\nbefore you are refused by it.\n\nA \"surface\" is one upstream product family, not a playing surface:\nseason and advanced stats, schedules, standings, rosters, player\ndesignations, fantasy projections/rankings/ADP, and betting market\nsnapshots. 22 surfaces, 572,842 rows as of 2026-10-09.\n\n17 surfaces are available on Free. The 5 market surfaces\n(`player_props`, `opening_player_props`, `dfs_draftables`,\n`opening_odds`, `dfs_slates`) require Edge or above, the same band as\nevery other prop and odds endpoint. This catalogue is readable on every\nplan, including the rows for surfaces your plan cannot yet fetch.\n",
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`, with a 5-minute cache stamp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "surface": {
                            "type": "string"
                          },
                          "rows": {
                            "type": "integer"
                          },
                          "seasons": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            }
                          },
                          "min_plan": {
                            "type": "string",
                            "enum": [
                              "free",
                              "solo",
                              "multi",
                              "edge",
                              "pro",
                              "enterprise",
                              "max",
                              "starter",
                              "growth",
                              "business"
                            ],
                            "description": "The lowest plan that can read this surface. The enum is the full account set because every plan enum in this spec lists it (check:plan-enums); the route emits only `free` or `edge` today."
                          },
                          "last_updated": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/nfl/surfaces/{surface}": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "Rows of one NFL snapshot surface",
        "description": "Returns the stored rows of a single NFL snapshot surface, newest season\nfirst, paginated. Use GET /v1/nfl/surfaces to list the surfaces, their\nrow counts and their plan floors.\n\nEach row is `{ surface, season, week, updated_at, attributes }`.\n`attributes` is the stat object for that row, keyed by the stat names\nthe surface uses, so the field set differs per surface by design: this\nis one endpoint over 22 product families rather than 22 endpoints.\n`season` and `week` are null on the surfaces that are not season- or\nweek-scoped (`teams`, `players_active`).\n\n`attributes` carries NO identifiers. Our supplier's licence does not\npermit redistributing its own ids, so they are removed, as are any\nsupplier hostnames. Join rows on player and team NAMES together with\n`season` and `week`. Sportsbook names and their deep links ARE returned\non the market surfaces, under `sportsbook`, because those are the\nmarket being quoted and not a supplier identifier.\n\nPLAN. 17 surfaces are available on Free. `player_props`,\n`opening_player_props` and `dfs_draftables` need Edge or above (the\nplayer-props band); `opening_odds` and `dfs_slates` need Edge or above\n(the odds band). A refused call returns 403 naming your current plan,\nthe plan required and what it unlocks.\n\nHISTORY. `?season=` is subject to the same historical-depth\nentitlement as every other season-scoped endpoint: a plan with a\nlimited archive is refused a season beyond its ceiling. Most surfaces\nhold 2025 and 2026.\n",
        "parameters": [
          {
            "in": "path",
            "name": "surface",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "teams",
                "players_active",
                "games",
                "standings",
                "team_roster",
                "team_stats",
                "team_season_stats",
                "season_stats",
                "advanced_passing",
                "advanced_rushing",
                "advanced_receiving",
                "player_designations",
                "fantasy_projections",
                "fantasy_weekly_stats",
                "fantasy_rankings",
                "fantasy_adp",
                "fantasy_scoring_formats",
                "player_props",
                "opening_player_props",
                "dfs_draftables",
                "opening_odds",
                "dfs_slates"
              ]
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2000,
              "maximum": 2099
            },
            "description": "Season LABEL: the year the season starts. Omit for every season this surface holds, newest first."
          },
          {
            "in": "query",
            "name": "week",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 30
            },
            "description": "Only rows for this week. Surfaces that are not week-scoped hold null and match no week."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10000
            },
            "description": "Alias for offset, resolved against limit."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }`; `meta.total` is the full match count, not the page size.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "surface": {
                            "type": "string"
                          },
                          "season": {
                            "type": "integer",
                            "nullable": true
                          },
                          "week": {
                            "type": "integer",
                            "nullable": true
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "attributes": {
                            "type": "object",
                            "description": "Stat object for this row. Field set varies by surface; carries no supplier identifier."
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "This surface requires a higher plan, or your plan's historical depth does not reach the requested season.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Unknown surface. The response names the valid surfaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/ncaaf/leaders": {
      "get": {
        "tags": [
          "ncaaf"
        ],
        "summary": "NCAAF season fantasy/statistical leaderboard",
        "description": "Season player leaderboard from our licensed paid aggregator's NCAAF\nplayer-season stats (migration 266's `bdl_ncaaf_surface_snapshots`,\nsurface `player_season_stats`), covered by that aggregator's\nall-sports redistribution agreement (`vendor-licences.tsv`). NOT\nbacked by the other NCAAF stats feed we ingest: that source is\nquotable but not servable (see\ndocs/measurable-servable-quotable-2026-09-01.md and issue #1213), so\nit is never used here.\n`fantasy_points_ppr` is OUR OWN computation (standard PPR: 0.04/pass\nyd, 4/pass TD, -2/INT, 0.1/rush or rec yd, 6/rush or rec TD,\n1/reception) over the aggregator's raw box-score totals; the\nunderlying data carries no fantasy total itself, unlike the\ncolumn of the same name on GET /v1/nfl/leaders. There is no\n`min_games` parameter: the underlying payload carries no\ngames-played field at all (verified against every row). Leaders are\nidentified by name/position/jersey_number and team name/abbreviation;\nthe aggregator's own numeric player/team ids are never served (its\nlicence bars redistributing them).\n`player.id` is OUR canonical `players.bbs_id` uuid, resolved through\nour own bridge, and is the field to join on: prefer it to matching a\nleader by name, which is ambiguous for roughly a third of this\nsurface. It is `null` for an athlete we have not bridged yet: a null\nmeans \"not resolved\", never \"no such player\". There is no team id:\nour NCAAF teams carry no identifier from this aggregator, so only the\nplayer side is bridged.\nNo `headshot_url` is served for NCAAF. The only college headshots we\nhold come from a source whose terms permit personal, non-commercial\nuse only, so serving them is not licensed; the aggregator that IS\nlicensed publishes no player images at all.\n",
        "parameters": [
          {
            "in": "query",
            "name": "stat",
            "schema": {
              "type": "string",
              "enum": [
                "fantasy_points_ppr",
                "passing_yards",
                "passing_touchdowns",
                "rushing_yards",
                "rushing_touchdowns",
                "receiving_yards",
                "receiving_touchdowns",
                "receptions",
                "total_tackles",
                "sacks",
                "interceptions"
              ],
              "default": "fantasy_points_ppr"
            }
          },
          {
            "in": "query",
            "name": "season",
            "schema": {
              "type": "integer",
              "minimum": 2020,
              "maximum": 2099
            },
            "description": "Season LABEL: the year the season starts. Omit to use the newest season with real rows (currently 2026, the only season this feed has ever carried)."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok. `{ data, meta }` with a route-local 5-minute cache stamp.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No NCAAF player-season data available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "player stats",
        "x-bbs-not-available-for-sports": [
          "football",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/wc2026/matches/{game_id}/live-probability": {
      "get": {
        "tags": [
          "wc2026"
        ],
        "summary": "Live win probability for a World Cup match",
        "parameters": [
          {
            "in": "path",
            "name": "game_id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid game_id (must be UUID).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Not a WC2026 match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/formulas/wc2026/group-standings": {
      "get": {
        "tags": [
          "wc2026"
        ],
        "summary": "World Cup group-standings formula",
        "description": "The computed group-standings view for WC2026. No parameters.",
        "responses": {
          "200": {
            "description": "ok.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/formulas/wc2026/progression": {
      "get": {
        "tags": [
          "wc2026"
        ],
        "summary": "World Cup progression formula",
        "description": "The computed advancement/progression view for WC2026. No parameters.",
        "responses": {
          "200": {
            "description": "ok.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "oneOf": [
                        {
                          "type": "object"
                        },
                        {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/nfl/players/{id}/game-log": {
      "get": {
        "tags": [
          "nfl"
        ],
        "summary": "NFL player game log for a season",
        "description": "The same rows as /v1/nfl/players/{id}/stats, ordered by week ascending\nfor one required season and returned unpaginated. Offence and defence.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Player id (the route param is playerId)."
          },
          {
            "in": "query",
            "name": "season",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1999,
              "maximum": 2100
            },
            "description": "Season LABEL, not a calendar year: the year a season STARTS in. 2025 means the 2025-26 campaign for leagues that cross the new year (EPL, NBA, NHL) and the 2025 season for those that do not (MLB, MLS, WNBA). Filtering resolves each league's own start month, so a season never spans two calendar years' worth of rows."
          }
        ],
        "responses": {
          "200": {
            "description": "ok.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NflPlayerWeekStat"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid ?season=.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/matches/{id}/weather": {
      "get": {
        "tags": [
          "matches"
        ],
        "summary": "Conditions at kickoff",
        "description": "Conditions at a match's kickoff. For a kickoff in the past these are OBSERVED values, for one in the future they are a forecast; `forecast.observed` says which, so a caller never has to infer it by comparing the kickoff against the clock. The shape is identical either way.\n\n`forecast` is null, with a machine-readable `unavailable_reason` and a prose `note`, whenever conditions cannot honestly be attributed to the kickoff. Branch on `unavailable_reason`, never on `note`.\n\nFor an entitled caller, a valid match id returns 200. `404 not_found` means only that no match exists with that id. Missing venue data or coordinates are coverage gaps represented by a 200 response, not a claim that the caller supplied the wrong id.\n\nOutdoor weather at an enclosed stadium describes the car park, not the game, so it is withheld rather than served. `venue.roof` is returned so a consumer can render \"Indoor\" instead of a blank.\n\nA `retractable` roof is withheld on the same grounds: the roof's STATE at a given kickoff is a property of the match, we have no source for it, and a guess is indistinguishable from a fact in the response.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ok.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "match_id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "kickoff_utc": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "venue": {
                              "description": "The resolved venue, or null when none is known. Coordinates are null when the venue is known but has not been geocoded.",
                              "oneOf": [
                                {
                                  "type": "null"
                                },
                                {
                                  "type": "object",
                                  "properties": {
                                    "name": {
                                      "oneOf": [
                                        {
                                          "type": "string"
                                        },
                                        {
                                          "type": "null"
                                        }
                                      ]
                                    },
                                    "latitude": {
                                      "type": "number",
                                      "nullable": true
                                    },
                                    "longitude": {
                                      "type": "number",
                                      "nullable": true
                                    },
                                    "roof": {
                                      "type": "string",
                                      "enum": [
                                        "open",
                                        "fixed",
                                        "retractable",
                                        "unknown"
                                      ],
                                      "description": "Whether the venue is open to the sky."
                                    }
                                  }
                                }
                              ]
                            },
                            "forecast": {
                              "oneOf": [
                                {
                                  "type": "null"
                                },
                                {
                                  "type": "object",
                                  "properties": {
                                    "temperature_c": {
                                      "type": "number",
                                      "nullable": true
                                    },
                                    "apparent_temperature_c": {
                                      "type": "number",
                                      "nullable": true,
                                      "description": "Feels-like temperature: wind chill in the cold, heat index in the heat."
                                    },
                                    "relative_humidity_pct": {
                                      "type": "number",
                                      "nullable": true
                                    },
                                    "precipitation_mm": {
                                      "type": "number",
                                      "nullable": true
                                    },
                                    "wind_speed_kmh": {
                                      "type": "number",
                                      "nullable": true
                                    },
                                    "wind_gusts_kmh": {
                                      "type": "number",
                                      "nullable": true
                                    },
                                    "weather_code": {
                                      "type": "integer",
                                      "nullable": true,
                                      "description": "WMO code."
                                    },
                                    "condition": {
                                      "type": "string",
                                      "nullable": true,
                                      "description": "Human label for weather_code."
                                    },
                                    "observed": {
                                      "type": "boolean",
                                      "description": "true = measured after the fact, false = predicted beforehand."
                                    }
                                  }
                                }
                              ]
                            },
                            "note": {
                              "type": "string"
                            },
                            "unavailable_reason": {
                              "type": "string",
                              "description": "Stable reason that forecast is null.",
                              "enum": [
                                "venue_unknown",
                                "venue_not_geocoded",
                                "roof_fixed",
                                "roof_retractable_state_unknown",
                                "roof_unknown",
                                "provider_licence_pending",
                                "beyond_forecast_horizon",
                                "provider_no_data",
                                "no_reading_for_kickoff_hour"
                              ]
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id (must be UUID).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "No match with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "scores",
        "x-bbs-not-available-for-sports": [
          "mma",
          "formula1"
        ]
      }
    },
    "/v1/leagues/{id}": {
      "get": {
        "tags": [
          "catalogue"
        ],
        "summary": "Single league by canonical key",
        "description": "League naming record from the canonical registry (id, name, sport, country). This is the NAMING catalogue; coverage depth and freshness come from GET /v1/coverage.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Canonical league key, e.g. nba, epl, wc2026."
          }
        ],
        "responses": {
          "200": {
            "description": "ok. Standard Envelope with meta.source = catalogue.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "required": [
                            "id",
                            "name",
                            "sport",
                            "country"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "sport": {
                              "type": "string"
                            },
                            "country": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Unknown league key. The error carries a suggested_fix pointing at /v1/leagues.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/health": {
      "get": {
        "tags": [
          "platform"
        ],
        "summary": "Service health",
        "description": "Liveness probe. NOTE the non-standard shape: this returns a bare object, NOT the standard Envelope (no data/meta/error wrapper). `sha` reports the deployed commit, and is the literal string \"unknown\" when neither COMMIT_SHA nor RAILWAY_GIT_COMMIT_SHA is set in the environment (which is the case today, so hash-based deploy verification is unavailable until that build arg is wired). `deployed_at` is derived from process uptime, not from the build.",
        "responses": {
          "200": {
            "description": "ok. Bare object, not the standard Envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "sha",
                    "deployed_at",
                    "uptime_seconds"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "sha": {
                      "type": "string",
                      "description": "Deployed commit, or \"unknown\" when the build arg is unset."
                    },
                    "deployed_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Derived from process uptime."
                    },
                    "uptime_seconds": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/user/me": {
      "get": {
        "tags": [
          "platform"
        ],
        "summary": "Your account",
        "description": "Who am I? The account behind the calling API key: plan, effective rate limits, a masked key id, and the GitHub-linked / paused flags. Authed by your own key (not a dashboard session), so the answer is always scoped to the key that asked. No parameters.",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "key_id": {
                              "type": "string",
                              "description": "Masked: an ellipsis + the last 4 chars of the key id.",
                              "example": "…afa6"
                            },
                            "email": {
                              "type": "string",
                              "nullable": true,
                              "description": "Owner email, omitted when the key has none on file."
                            },
                            "plan": {
                              "type": "string",
                              "enum": [
                                "free",
                                "solo",
                                "multi",
                                "edge",
                                "pro",
                                "max",
                                "enterprise",
                                "starter",
                                "growth",
                                "business"
                              ],
                              "description": "The EFFECTIVE plan: what this key can actually reach, and what\n`limits` below describes. Equal to the billed plan except while a\nreferral grant is active, in which case `billed_plan` and\n`referral_grant` are also present.\n\nThe ladder actually sold is `free` → `solo` → `multi` →\n`edge` → `pro` → `enterprise`. Note `multi` is the stored slug\nfor the tier sold as **Trio**; the display name changed, the\nslug deliberately did not, so no stored row had to move. `max`\nis an internal comped tier, never self-serve.\n\n`starter`, `growth` and `business` are **deprecated**: retired\nfrom sale, never returned for a key created today, and excluded\nfrom every upgrade path the gateway builds. They stay enumerated\nbecause nothing in the schema prevents a legacy row from still\nholding one, and an enum that rejects a value the API can really\nemit fails closed on a live customer, a worse outcome than a\nwide enum. Treat them as read-only history; do not send a user\nto one.\n"
                            },
                            "billed_plan": {
                              "type": "string",
                              "enum": [
                                "free",
                                "solo",
                                "multi",
                                "edge",
                                "pro",
                                "max",
                                "enterprise",
                                "starter",
                                "growth",
                                "business"
                              ],
                              "description": "What the account is BILLED for. Present only while a referral\ngrant is raising `plan` above it; absent otherwise, when `plan` is\nalready the billed plan. `plan` stays the effective value because\nit is what the caller can actually use and what the `limits`\nbeside it describe.\n\nSame value set as `plan` above, deprecated tiers included; a\nlegacy row is precisely the kind of account that can still be\nbilled for a tier we no longer sell.\n"
                            },
                            "referral_grant": {
                              "type": "object",
                              "description": "A time-boxed plan grant earned through the referral program. Present only while one is active. Not a subscription; it has no price and no invoice, and when it ends the account returns to `billed_plan` with nothing removed from that plan.",
                              "properties": {
                                "plan": {
                                  "type": "string",
                                  "enum": [
                                    "solo",
                                    "multi",
                                    "edge",
                                    "pro"
                                  ],
                                  "description": "The granted plan, the same value `plan` reports while it is active."
                                },
                                "expires_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "description": "When the grant stops applying."
                                }
                              }
                            },
                            "github_connected": {
                              "type": "boolean"
                            },
                            "paused": {
                              "type": "boolean"
                            },
                            "limits": {
                              "type": "object",
                              "properties": {
                                "per_minute": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The cap actually ENFORCED for this key, matching the `X-RateLimit-Limit-Minute` header on the same response. null for the unlimited (enterprise) tier."
                                },
                                "per_day": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The cap actually ENFORCED for this key, matching the `X-RateLimit-Limit-Day` header on the same response; it includes any active volume grandfather, so it can exceed the plan's published figure. null for the unlimited (enterprise) tier."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/athletes": {
      "get": {
        "tags": [
          "combat"
        ],
        "summary": "List boxing and MMA athlete profiles",
        "description": "Requires `sport=boxing` or `sport=mma`. Returns stored profiles with\nnames, biographical fields and available physical attributes. Records\nmay be null when no verified record is loaded. `meta.total` counts the\nfiltered population; `data` holds the requested page.\n",
        "parameters": [
          {
            "in": "query",
            "name": "sport",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "boxing",
                "mma"
              ]
            }
          },
          {
            "in": "query",
            "name": "name",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "description": "Alias for offset; explicit offset wins."
          }
        ],
        "responses": {
          "200": {
            "description": "Athlete profiles in the selected sport.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta",
                    "error"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "nationality": {
                            "type": "string",
                            "nullable": true
                          },
                          "dob": {
                            "type": "string",
                            "nullable": true,
                            "format": "date"
                          },
                          "height_cm": {
                            "type": "number",
                            "nullable": true
                          },
                          "reach_cm": {
                            "type": "number",
                            "nullable": true
                          },
                          "stance": {
                            "type": "string",
                            "nullable": true
                          },
                          "weight_class": {
                            "type": "string",
                            "nullable": true
                          },
                          "current_ranking": {
                            "type": "integer",
                            "nullable": true
                          },
                          "record": {
                            "type": "object",
                            "nullable": true
                          },
                          "photo_url": {
                            "type": "string",
                            "nullable": true
                          },
                          "external_ids": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "count",
                        "total",
                        "limit",
                        "offset",
                        "records_available"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "records_available": {
                          "type": "boolean"
                        }
                      }
                    },
                    "error": {
                      "type": "null"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid sport or paging query."
          },
          "403": {
            "description": "The key does not include the requested sport."
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/players/{id}/shots": {
      "get": {
        "tags": [
          "players",
          "nba"
        ],
        "summary": "NBA player shot coordinates",
        "description": "Pro or higher. Returns coordinate-bearing shooting plays for a canonical\nNBA player, newest games first. Free throws are excluded unless\n`include_ft=true`. An empty array means this player has no qualifying\nshots in the stored play log; it is not a complete game play log.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Canonical player id from /v1/players."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "in": "query",
            "name": "include_ft",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Shot log, possibly empty for this player.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "date",
                          "match_id",
                          "period",
                          "clock",
                          "shot_type",
                          "coordinate_x",
                          "coordinate_y",
                          "made"
                        ],
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "match_id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "period": {
                            "type": "integer",
                            "nullable": true
                          },
                          "clock": {
                            "type": "string",
                            "nullable": true
                          },
                          "shot_type": {
                            "type": "string",
                            "nullable": true
                          },
                          "coordinate_x": {
                            "type": "number",
                            "nullable": true
                          },
                          "coordinate_y": {
                            "type": "number",
                            "nullable": true
                          },
                          "made": {
                            "type": "boolean",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id or query."
          },
          "403": {
            "description": "Pro plan required."
          }
        },
        "x-bbs-min-plan": "pro"
      }
    },
    "/v1/nba/teams/{id}/lineup-combos": {
      "get": {
        "tags": [
          "nba",
          "teams"
        ],
        "summary": "NBA five-player starting lineup combinations",
        "description": "Groups games by the same five starters for one canonical NBA team.\n`games_together` counts appearances; `wins` compares team and opponent\nbox-score points. Returns an empty array when no complete five-starter\ngame is stored. Available on Free with basketball entitlement.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Canonical team id from /v1/teams."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Starting units ordered by games together.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "player_ids",
                          "player_names",
                          "games_together",
                          "wins"
                        ],
                        "properties": {
                          "player_ids": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "format": "uuid"
                            }
                          },
                          "player_names": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "nullable": true
                            }
                          },
                          "games_together": {
                            "type": "integer"
                          },
                          "wins": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid team id or query."
          },
          "403": {
            "description": "The key does not include basketball."
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/wc2026/groups/{group}": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "One World Cup 2026 group table and advancement model",
        "description": "One group A through L, case insensitive. The four team rows include\nrecorded standings and separate stored model probabilities. Model\nvalues have no per-row update time and may predate a finished group;\nthey are not live advancement odds. Probability fields can be null.\nAvailable on Free with football entitlement; cached for five minutes.\n",
        "parameters": [
          {
            "in": "path",
            "name": "group",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-La-l]$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Group and its team rows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "group",
                        "teams"
                      ],
                      "properties": {
                        "group": {
                          "type": "string"
                        },
                        "teams": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "rank",
                              "team_id",
                              "name",
                              "played",
                              "wins",
                              "draws",
                              "losses",
                              "gf",
                              "ga",
                              "gd",
                              "points",
                              "advancement_prob",
                              "p_top2",
                              "p_win_group",
                              "expected_points"
                            ],
                            "properties": {
                              "rank": {
                                "type": "integer",
                                "nullable": true
                              },
                              "team_id": {
                                "type": "string",
                                "nullable": true,
                                "format": "uuid"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true
                              },
                              "played": {
                                "type": "integer"
                              },
                              "wins": {
                                "type": "integer"
                              },
                              "draws": {
                                "type": "integer"
                              },
                              "losses": {
                                "type": "integer"
                              },
                              "gf": {
                                "type": "integer"
                              },
                              "ga": {
                                "type": "integer"
                              },
                              "gd": {
                                "type": "integer"
                              },
                              "points": {
                                "type": "integer"
                              },
                              "advancement_prob": {
                                "type": "number",
                                "nullable": true
                              },
                              "p_top2": {
                                "type": "number",
                                "nullable": true
                              },
                              "p_win_group": {
                                "type": "number",
                                "nullable": true
                              },
                              "expected_points": {
                                "type": "number",
                                "nullable": true
                              }
                            }
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Group must be A through L."
          },
          "403": {
            "description": "The key does not include football."
          },
          "404": {
            "description": "No standings stored for that group."
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/predictions/{id}": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Latest prediction for a match",
        "description": "Solo or higher. The path id is a match UUID from /v1/matches, not a\nprediction row id. Returns the newest stored prediction for that match\nand its freshness metadata. Predictions can be expired; check\n`data.valid_until` and `data.expired` before using one as a forecast.\nThe response is identical to /v1/predictions/match/{id}.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Latest prediction",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SingleMatchPredictionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id."
          },
          "403": {
            "description": "Solo plan or covered-sport entitlement required."
          },
          "404": {
            "description": "No prediction stored for this match."
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/predictions/match/{id}": {
      "get": {
        "tags": [
          "nerd"
        ],
        "summary": "Latest prediction for a match (original URL)",
        "description": "Same response and Solo floor as /v1/predictions/{id}. This original\nURL remains available to existing callers. The id is a match UUID.\nCheck `valid_until` and `expired` before using the row as a forecast.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Latest prediction",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SingleMatchPredictionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid match id."
          },
          "403": {
            "description": "Solo plan or covered-sport entitlement required."
          },
          "404": {
            "description": "No prediction stored for this match."
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/players/{id}/injury": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Current injury status for a player",
        "description": "Available on Free. A covered player absent from the current injury\nreport returns `supported=true` and `status=active`. An uncovered\nsport returns `supported=false` and `status=unknown`; neither is a\nmeasured healthy player. The response includes `as_of` and, when an\ninjury row exists, `updated_at`. NBA and NHL are populated today.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Canonical player id from /v1/players."
          }
        ],
        "responses": {
          "200": {
            "description": "Player injury state and coverage flag.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "player",
                        "sport",
                        "supported",
                        "status",
                        "injury_type",
                        "return_date",
                        "comment",
                        "updated_at",
                        "as_of"
                      ],
                      "properties": {
                        "player": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "name": {
                              "type": "string"
                            },
                            "sport": {
                              "type": "string",
                              "nullable": true
                            },
                            "team": {
                              "type": "object",
                              "nullable": true
                            }
                          }
                        },
                        "sport": {
                          "type": "string",
                          "nullable": true
                        },
                        "supported": {
                          "type": "boolean"
                        },
                        "status": {
                          "type": "string"
                        },
                        "injury_type": {
                          "type": "string",
                          "nullable": true
                        },
                        "return_date": {
                          "type": "string",
                          "nullable": true,
                          "format": "date"
                        },
                        "comment": {
                          "type": "string",
                          "nullable": true
                        },
                        "updated_at": {
                          "type": "string",
                          "nullable": true,
                          "format": "date-time"
                        },
                        "as_of": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id."
          },
          "403": {
            "description": "The key does not include the player's sport."
          },
          "404": {
            "description": "Player id not found."
          }
        },
        "x-bbs-min-plan": "free",
        "x-bbs-capability": "injuries",
        "x-bbs-not-available-for-sports": [
          "baseball",
          "cricket",
          "mma",
          "boxing",
          "formula1"
        ]
      }
    },
    "/v1/players/{id}/vs-team/{teamId}": {
      "get": {
        "tags": [
          "players"
        ],
        "summary": "Player history against one opponent",
        "description": "Solo or higher. NBA and NHL game history with a win-loss summary and\nper-game averages. A known player who has not faced this team returns\n200 with `games=[]` and `games_played=0`. NHL historical opponent\nattribution uses the player's current team and can be wrong after a\ntransfer; do not interpret it as a historical roster record.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Canonical player id."
          },
          {
            "in": "path",
            "name": "teamId",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Canonical opponent team id."
          }
        ],
        "responses": {
          "200": {
            "description": "Opponent summary and game rows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "player",
                        "opponent",
                        "supported",
                        "summary",
                        "games",
                        "as_of"
                      ],
                      "properties": {
                        "player": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "name": {
                              "type": "string"
                            },
                            "sport": {
                              "type": "string",
                              "nullable": true
                            },
                            "team": {
                              "type": "object",
                              "nullable": true
                            }
                          }
                        },
                        "opponent": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "name": {
                              "type": "string"
                            },
                            "abbreviation": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "supported": {
                          "type": "boolean"
                        },
                        "summary": {
                          "type": "object",
                          "properties": {
                            "games_played": {
                              "type": "integer"
                            },
                            "record": {
                              "type": "object",
                              "properties": {
                                "wins": {
                                  "type": "integer"
                                },
                                "losses": {
                                  "type": "integer"
                                }
                              }
                            },
                            "per_game": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "number",
                                "nullable": true
                              }
                            }
                          }
                        },
                        "games": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "match_id": {
                                "type": "string",
                                "format": "uuid"
                              },
                              "game_date": {
                                "type": "string",
                                "format": "date"
                              },
                              "season": {
                                "type": "integer"
                              },
                              "venue": {
                                "type": "string"
                              },
                              "result": {
                                "type": "string"
                              },
                              "team_score": {
                                "type": "integer",
                                "nullable": true
                              },
                              "opp_score": {
                                "type": "integer",
                                "nullable": true
                              },
                              "stats": {
                                "type": "object",
                                "additionalProperties": {
                                  "type": "number",
                                  "nullable": true
                                }
                              }
                            }
                          }
                        },
                        "as_of": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player or team id."
          },
          "403": {
            "description": "Solo plan or sport entitlement required."
          },
          "404": {
            "description": "Player or team id not found."
          }
        },
        "x-bbs-min-plan": "solo"
      }
    },
    "/v1/nba/players/{id}/season": {
      "get": {
        "tags": [
          "nba",
          "players"
        ],
        "summary": "NBA box-score derived season metrics",
        "description": "Most recent season with stored game box scores. Includes basic\nper-game averages and advanced shooting and usage metrics. The result\ncan differ in the last decimal from /season-averages, which serves a\nseparate stored feed. A known NBA player with no box-score rows gets\n200 with `season=null`, `games_played=0`, `basic=null` and\n`advanced=null`. Available on Free with basketball entitlement.\n",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Canonical NBA player id."
          }
        ],
        "responses": {
          "200": {
            "description": "NBA season basic and advanced values, or an explicit no-games response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "player",
                        "season",
                        "games_played",
                        "basic",
                        "advanced",
                        "as_of"
                      ],
                      "properties": {
                        "player": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "name": {
                              "type": "string"
                            },
                            "position": {
                              "type": "string",
                              "nullable": true
                            },
                            "headshot_url": {
                              "type": "string",
                              "nullable": true
                            },
                            "team": {
                              "type": "object"
                            }
                          }
                        },
                        "season": {
                          "type": "integer",
                          "nullable": true
                        },
                        "games_played": {
                          "type": "integer"
                        },
                        "basic": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "ppg": {
                              "type": "number",
                              "nullable": true
                            },
                            "rpg": {
                              "type": "number",
                              "nullable": true
                            },
                            "apg": {
                              "type": "number",
                              "nullable": true
                            },
                            "spg": {
                              "type": "number",
                              "nullable": true
                            },
                            "bpg": {
                              "type": "number",
                              "nullable": true
                            },
                            "mpg": {
                              "type": "number",
                              "nullable": true
                            },
                            "fg_pct": {
                              "type": "number",
                              "nullable": true
                            },
                            "fg3_pct": {
                              "type": "number",
                              "nullable": true
                            },
                            "ft_pct": {
                              "type": "number",
                              "nullable": true
                            }
                          }
                        },
                        "advanced": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "true_shooting_pct": {
                              "type": "number",
                              "nullable": true
                            },
                            "efg_pct": {
                              "type": "number",
                              "nullable": true
                            },
                            "usage_rate": {
                              "type": "number",
                              "nullable": true
                            },
                            "per": {
                              "type": "number",
                              "nullable": true
                            },
                            "per_status": {
                              "type": "string",
                              "enum": [
                                "computed",
                                "pending_validation"
                              ]
                            }
                          }
                        },
                        "as_of": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "cached": {
                          "type": "boolean"
                        },
                        "cache_age_ms": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid player id."
          },
          "403": {
            "description": "The key does not include basketball."
          },
          "404": {
            "description": "No NBA player with that id."
          }
        },
        "x-bbs-min-plan": "free"
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "platform"
        ],
        "summary": "Your usage + remaining quota",
        "description": "Your key's own quota and usage: live remaining quota for today and this minute (from the rate-limit counters) plus a 7-day per-day request + 4xx-error history (from usage_events). Authed by your own key. The recent[] history is empty when the gateway is running without its Postgres pool. Answers \"am I rate-limited / how much have I used\" without a support round-trip.",
        "responses": {
          "200": {
            "description": "ok",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Envelope"
                    },
                    {
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "plan": {
                              "type": "string",
                              "enum": [
                                "free",
                                "solo",
                                "multi",
                                "edge",
                                "pro",
                                "max",
                                "enterprise",
                                "starter",
                                "growth",
                                "business"
                              ],
                              "description": "The EFFECTIVE plan: what this key can actually reach, and what\n`limits` below describes. Equal to the billed plan except while a\nreferral grant is active, in which case `billed_plan` and\n`referral_grant` are also present.\n\nThe ladder actually sold is `free` → `solo` → `multi` →\n`edge` → `pro` → `enterprise`. Note `multi` is the stored slug\nfor the tier sold as **Trio**; the display name changed, the\nslug deliberately did not, so no stored row had to move. `max`\nis an internal comped tier, never self-serve.\n\n`starter`, `growth` and `business` are **deprecated**: retired\nfrom sale, never returned for a key created today, and excluded\nfrom every upgrade path the gateway builds. They stay enumerated\nbecause nothing in the schema prevents a legacy row from still\nholding one, and an enum that rejects a value the API can really\nemit fails closed on a live customer, a worse outcome than a\nwide enum. Treat them as read-only history; do not send a user\nto one.\n"
                            },
                            "billed_plan": {
                              "type": "string",
                              "enum": [
                                "free",
                                "solo",
                                "multi",
                                "edge",
                                "pro",
                                "max",
                                "enterprise",
                                "starter",
                                "growth",
                                "business"
                              ],
                              "description": "What the account is BILLED for. Present only while a referral\ngrant is raising `plan` above it; absent otherwise, when `plan` is\nalready the billed plan. `plan` stays the effective value because\nit is what the caller can actually use and what the `limits`\nbeside it describe.\n\nSame value set as `plan` above, deprecated tiers included; a\nlegacy row is precisely the kind of account that can still be\nbilled for a tier we no longer sell.\n"
                            },
                            "referral_grant": {
                              "type": "object",
                              "description": "A time-boxed plan grant earned through the referral program. Present only while one is active. Not a subscription; it has no price and no invoice, and when it ends the account returns to `billed_plan` with nothing removed from that plan.",
                              "properties": {
                                "plan": {
                                  "type": "string",
                                  "enum": [
                                    "solo",
                                    "multi",
                                    "edge",
                                    "pro"
                                  ],
                                  "description": "The granted plan, the same value `plan` reports while it is active."
                                },
                                "expires_at": {
                                  "type": "string",
                                  "format": "date-time",
                                  "description": "When the grant stops applying."
                                }
                              }
                            },
                            "limits": {
                              "type": "object",
                              "properties": {
                                "per_minute": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The cap actually ENFORCED for this key, matching the `X-RateLimit-Limit-Minute` header on the same response. null for the unlimited (enterprise) tier."
                                },
                                "per_day": {
                                  "type": "integer",
                                  "nullable": true,
                                  "description": "The cap actually ENFORCED for this key, matching the `X-RateLimit-Limit-Day` header on the same response; it includes any active volume grandfather, so it can exceed the plan's published figure. null for the unlimited (enterprise) tier."
                                }
                              }
                            },
                            "used_today": {
                              "type": "integer"
                            },
                            "remaining_today": {
                              "type": "integer",
                              "nullable": true,
                              "description": "`limits.per_day` minus `used_today`, i.e. computed against the enforced cap. null for the unlimited (enterprise) tier."
                            },
                            "window_reset": {
                              "type": "string",
                              "format": "date-time",
                              "description": "Next UTC midnight, when the daily bucket resets."
                            },
                            "minute": {
                              "type": "object",
                              "properties": {
                                "used": {
                                  "type": "integer"
                                },
                                "limit": {
                                  "type": "integer",
                                  "nullable": true
                                },
                                "remaining": {
                                  "type": "integer",
                                  "nullable": true
                                },
                                "resets_at": {
                                  "type": "string",
                                  "format": "date-time"
                                }
                              }
                            },
                            "recent": {
                              "type": "array",
                              "description": "Up to 7 rows, most recent day first. Empty without a Postgres pool.",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "date": {
                                    "type": "string",
                                    "example": "2026-07-17"
                                  },
                                  "requests": {
                                    "type": "integer"
                                  },
                                  "errors_4xx": {
                                    "type": "integer"
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        },
        "x-bbs-min-plan": "free"
      }
    }
  },
  "x-bbs-not-available-by-sport": {
    "football": [
      "player stats",
      "expected goals",
      "player props",
      "play-by-play",
      "depth charts",
      "rosters"
    ],
    "basketball": [
      "team stats",
      "player season stats",
      "expected goals",
      "play-by-play",
      "depth charts",
      "rosters"
    ],
    "ice_hockey": [
      "team stats",
      "player season stats",
      "play-by-play",
      "depth charts",
      "rosters"
    ],
    "baseball": [
      "team stats",
      "player season stats",
      "injuries",
      "Elo ratings",
      "expected goals",
      "player props",
      "play-by-play",
      "depth charts",
      "rosters"
    ],
    "american_football": [
      "expected goals",
      "player props",
      "play-by-play",
      "lineups"
    ],
    "cricket": [
      "standings",
      "team stats",
      "player season stats",
      "injuries",
      "Elo ratings",
      "expected goals",
      "player props",
      "play-by-play",
      "depth charts",
      "rosters"
    ],
    "mma": [
      "scores",
      "standings",
      "team stats",
      "player stats",
      "player season stats",
      "injuries",
      "Elo ratings",
      "expected goals",
      "player props",
      "play-by-play",
      "lineups",
      "depth charts",
      "rosters"
    ],
    "boxing": [
      "standings",
      "team stats",
      "player stats",
      "player season stats",
      "injuries",
      "Elo ratings",
      "expected goals",
      "odds",
      "historical odds",
      "player props",
      "play-by-play",
      "lineups",
      "depth charts",
      "rosters"
    ],
    "formula1": [
      "scores",
      "standings",
      "team stats",
      "player stats",
      "player season stats",
      "injuries",
      "Elo ratings",
      "expected goals",
      "odds",
      "historical odds",
      "player props",
      "play-by-play",
      "lineups",
      "depth charts",
      "rosters"
    ]
  }
}
