{
  "openapi": "3.1.0",
  "info": {
    "title": "SharpAPI",
    "version": "3.28.1",
    "description": "Real-time sports betting odds API with +EV detection, arbitrage, middles, and low-hold opportunities.\n\n## Spec Versioning\n\n`info.version` is bumped on every schema or path change. Minor version (`2.x.0`) for additive changes or breaking shape fixes that align the spec to the live response; major version (`x.0.0`) for backward-incompatible redesigns. Removed paths and renamed fields always bump the minor at minimum. Check `x-generated-at` and `x-commit-sha` for the build provenance of a given snapshot.\n\n## Authentication\n\nAll authenticated endpoints accept an API key via one of three methods:\n\n| Method | Header / Param | Use case |\n|--------|---------------|----------|\n| `X-API-Key` | `X-API-Key: sk_live_...` | Recommended for server-side |\n| `Authorization` | `Authorization: Bearer sk_live_...` | Standard Bearer token |\n| `api_key` query | `?api_key=sk_live_...` | SSE/EventSource (cannot set headers) |\n\n## Subscription Tiers\n\n| Tier | Rate Limit | Data Delay | Max Books | EV | Arb | Middles | Game State |\n|------|-----------|------------|-----------|-----|-----|---------|------------|\n| Free | 12/min | 60s | 2 (DK, FD) | - | - | - | - |\n| Hobby | 120/min | Real-time | 5 | - | Yes | - | Add-on |\n| Pro | 300/min | Real-time | 15 | Yes | Yes | Yes | Add-on |\n| Sharp | 1000/min | Real-time | 25 | Yes | Yes | Yes | Add-on |\n| Enterprise | Custom | Real-time | All | Yes | Yes | Yes | Yes |\n\nGame State is a $79/mo add-on on any paid tier (Hobby, Pro, Sharp) and is included with Enterprise.\n\n## Rate Limit Headers\n\nEvery authenticated response includes:\n\n- `X-RateLimit-Limit` - Requests allowed per minute\n- `X-RateLimit-Remaining` - Requests remaining in current window\n- `X-RateLimit-Reset` - Unix timestamp when the window resets\n- `X-Data-Delay` - Odds delay in seconds for your tier (0 = real-time)\n- `X-Request-Id` - Unique request identifier for support\n\n## WebSocket Streaming\n\nThe WebSocket endpoint at `wss://ws.sharpapi.io` is documented separately in [`asyncapi.yaml`](./asyncapi.yaml) (AsyncAPI 3.0). OpenAPI 3.x cannot express WebSocket subprotocols and message channels, so the SSE endpoint (`/stream`) is the only stream covered by this document.\n\n## MCP Server\n\nThe `POST /mcp` endpoint is a Model Context Protocol server (JSON-RPC 2.0 over Streamable HTTP). Tools are self-described at runtime via `tools/list`, so it's documented as a setup guide rather than an OpenAPI path — see [`/sdks/mcp`](https://docs.sharpapi.io/sdks/mcp).\n",
    "contact": {
      "name": "SharpAPI Support",
      "url": "https://sharpapi.io",
      "email": "support@sharpapi.io"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://sharpapi.io/terms"
    },
    "x-generated-at": "2026-10-17T15:42:28Z",
    "x-commit-sha": "55b3a32"
  },
  "servers": [
    {
      "url": "https://api.sharpapi.io/api/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "Service health and status"
    },
    {
      "name": "Reference Data",
      "description": "Sports, leagues, sportsbooks, markets, and teams"
    },
    {
      "name": "Events",
      "description": "Event discovery and details"
    },
    {
      "name": "Odds",
      "description": "Odds snapshots, comparisons, batch, and deltas"
    },
    {
      "name": "Opportunities",
      "description": "+EV, arbitrage, middles, and low-hold opportunities"
    },
    {
      "name": "Streaming",
      "description": "Real-time SSE streaming"
    },
    {
      "name": "Account",
      "description": "Account info, usage, and key management"
    },
    {
      "name": "Betting Splits",
      "description": "Public betting splits — handle % and bet % from DraftKings (recreational) and Circa Sports (sharp-friendly), plus ticket-only percentages derived from BetMGM. Splits are a fixed three-source feature and do not scale with the number of sportsbooks on your plan."
    },
    {
      "name": "Deep Links",
      "description": "Sportsbook deep link generation"
    },
    {
      "name": "Futures",
      "description": "Futures markets and odds"
    },
    {
      "name": "Game State",
      "description": "Live game state data (Game State add-on or Enterprise)"
    }
  ],
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "BearerAuth": []
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Health check",
        "description": "Returns the service status and how fresh its odds data is. The response is a flat object with exactly two fields, `status` and `newest_odds_age_s`. No authentication required, and no rate-limit headers are returned. `HEAD` returns the status code only, with no body. Any method other than `GET` or `HEAD` returns `405 Method Not Allowed` with an `Allow: GET, HEAD` header and a `method_not_allowed` error body.",
        "tags": [
          "Health"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up. `ok`: odds data was refreshed within the last 60 seconds. `degraded`: no refresh has completed in the last 60 seconds, or none has completed yet.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "newest_odds_age_s"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded"
                      ],
                      "description": "`ok` or `degraded`"
                    },
                    "newest_odds_age_s": {
                      "type": "integer",
                      "minimum": -1,
                      "description": "Seconds since the API last completed a refresh of its odds data; `-1` when no odds data has loaded yet. Measures the refresh cycle, not the age of any individual price."
                    }
                  }
                },
                "example": {
                  "status": "ok",
                  "newest_odds_age_s": 1
                }
              }
            }
          },
          "503": {
            "description": "Service is not ready. `starting`: start-up is not finished. `down`: a core backend dependency is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "newest_odds_age_s"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "starting",
                        "down"
                      ],
                      "description": "`starting` or `down`"
                    },
                    "newest_odds_age_s": {
                      "type": "integer",
                      "minimum": -1,
                      "description": "Seconds since the API last completed a refresh of its odds data; `-1` when no odds data has loaded yet. Measures the refresh cycle, not the age of any individual price."
                    }
                  }
                },
                "example": {
                  "status": "starting",
                  "newest_odds_age_s": -1
                }
              }
            }
          }
        }
      }
    },
    "/splits": {
      "get": {
        "operationId": "listSplits",
        "summary": "List public betting splits",
        "description": "Returns the latest snapshot of public betting splits (handle % and bet %) for every tracked event. Data is sourced from DraftKings (recreational) and Circa Sports (sharp-friendly), refreshed every 5 minutes, plus BetMGM, derived from that book's own public bet-percentage field (ticket % only — the `handle_pct` object is still present on BetMGM rows, but its `away`/`home` members (`over`/`under` on totals) are `null`) and intermittent. These three are the only splits sources; the set is fixed and independent of which sportsbooks your plan selects. Requires Pro tier or higher: both splits routes are registered under the `splits` tier feature, which the Free and Hobby tiers do not carry.",
        "tags": [
          "Betting Splits"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "name": "market",
            "in": "query",
            "description": "Filter to events that have splits for a market. Comma-separated.",
            "schema": {
              "type": "string",
              "enum": [
                "spread",
                "total",
                "moneyline"
              ]
            },
            "example": "spread,moneyline"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of splits, one entry per (event, sportsbook).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination",
                    "updated_at"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SplitsGame"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/splits/history": {
      "get": {
        "operationId": "getSplitsHistory",
        "summary": "Splits history for one event",
        "description": "Returns captured splits samples for one event within the retained 48-hour window. No longer-term archive or backfill is available. Use cursor pagination to read the window. Requires Pro tier or higher.",
        "tags": [
          "Betting Splits"
        ],
        "parameters": [
          {
            "name": "event_id",
            "in": "query",
            "required": true,
            "description": "Canonical event ID (e.g. `mlb_guardians_orioles_2026-04-16`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "name": "start_time",
            "in": "query",
            "description": "Lower bound for snapshot timestamp. Accepts RFC 3339 (`2026-04-16T13:00:00Z`) or Unix seconds (`1776344602`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "end_time",
            "in": "query",
            "description": "Upper bound for snapshot timestamp. Same formats as `start_time`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Entries per page, default 100, maximum 200. Larger values clamp to 200. Applies after the sportsbook filter.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque meta.next_cursor from the previous page. Keep event_id, sportsbook, start_time and end_time unchanged.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Snapshots ordered oldest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SplitsHistoryEntry"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "event_id": {
                          "type": "string"
                        },
                        "total": {
                          "type": "integer",
                          "description": "Number of entries returned in this page."
                        },
                        "books": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Distinct source sportsbooks present in this response."
                        },
                        "oldest": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "Timestamp bound of this page; null for an empty page."
                        },
                        "newest": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "Timestamp bound of this page; null for an empty page."
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "has_more": {
                          "type": "boolean"
                        },
                        "next_cursor": {
                          "type": "string",
                          "description": "Continuation token; empty on the final page."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing event_id, invalid time bounds, or invalid/filter-mismatched cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "invalid_request",
                    "message": "event_id query parameter is required"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "History store temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/sports": {
      "get": {
        "operationId": "listSports",
        "summary": "List sports",
        "description": "Returns available sports with event and live counts. Public endpoint with optional API key for higher rate limits.",
        "tags": [
          "Reference Data"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          },
          {
            "ApiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "include_empty",
            "in": "query",
            "description": "Include sports with zero events",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of sports",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Sport"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "basketball",
                      "name": "Basketball",
                      "event_count": 42,
                      "live_count": 3,
                      "leagues": [
                        "nba",
                        "ncaab",
                        "wnba"
                      ]
                    },
                    {
                      "id": "football",
                      "name": "Football",
                      "event_count": 16,
                      "live_count": 0,
                      "leagues": [
                        "nfl",
                        "ncaaf"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/leagues": {
      "get": {
        "operationId": "listLeagues",
        "summary": "List leagues",
        "description": "Returns available leagues with event counts. Optionally filter by sport.",
        "tags": [
          "Reference Data"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          },
          {
            "ApiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "description": "Filter by sport ID",
            "schema": {
              "type": "string"
            },
            "example": "basketball"
          },
          {
            "name": "include_empty",
            "in": "query",
            "description": "Include leagues with zero events",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of leagues",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/League"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "nba",
                      "name": "NBA",
                      "sport": "basketball",
                      "event_count": 15,
                      "live_count": 2
                    },
                    {
                      "id": "ncaab",
                      "name": "NCAAB",
                      "sport": "basketball",
                      "event_count": 24,
                      "live_count": 1
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/sportsbooks": {
      "get": {
        "operationId": "listSportsbooks",
        "summary": "List sportsbooks",
        "description": "Returns available sportsbooks with status, capabilities, and event counts. Use `catalog=true` for a config-based catalog that doesn't require live data.",
        "tags": [
          "Reference Data"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          },
          {
            "ApiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "region",
            "in": "query",
            "description": "Filter by region",
            "schema": {
              "type": "string",
              "enum": [
                "US",
                "UK",
                "EU",
                "AU",
                "GLOBAL"
              ]
            },
            "example": "US"
          },
          {
            "name": "sharp",
            "in": "query",
            "description": "Filter by sharp book status",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "catalog",
            "in": "query",
            "description": "Return config-based catalog (no Redis dependency)",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "include_unlisted",
            "in": "query",
            "description": "Include unlisted sportsbooks",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "include_inactive",
            "in": "query",
            "description": "Include inactive sportsbooks (zero events)",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of sportsbooks",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Sportsbook"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "draftkings",
                      "name": "draftkings",
                      "display_name": "DraftKings",
                      "is_sharp": false,
                      "has_live_odds": true,
                      "has_player_props": true,
                      "has_limits": false,
                      "regions": [
                        "US"
                      ],
                      "requires_tier": "free",
                      "status": "active",
                      "event_count": 320,
                      "last_update": "2026-03-09T14:22:00Z",
                      "unlisted": false
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/markets": {
      "get": {
        "operationId": "listMarkets",
        "summary": "List market types",
        "description": "Returns available market types with descriptions, examples, and event counts.",
        "tags": [
          "Reference Data"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          },
          {
            "ApiKeyQuery": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of market types",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Market"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "moneyline",
                      "name": "Moneyline",
                      "description": "Bet on which team will win the game outright",
                      "hasLine": false,
                      "examples": [
                        "Lakers -150",
                        "Celtics +130"
                      ],
                      "event_count": 312,
                      "selection_count": 1840,
                      "sports": [
                        "basketball",
                        "football",
                        "baseball",
                        "hockey"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/teams": {
      "get": {
        "operationId": "listTeams",
        "summary": "List teams",
        "description": "Returns teams aggregated from current odds snapshots.",
        "tags": [
          "Reference Data"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          },
          {
            "ApiKeyQuery": []
          }
        ],
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "description": "Filter by sport",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "league",
            "in": "query",
            "description": "Filter by league",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Search team names (partial match)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of teams",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Team"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "boston-celtics",
                      "name": "Boston Celtics",
                      "sport": "basketball",
                      "leagues": [
                        "nba"
                      ],
                      "aliases": [
                        "Celtics"
                      ],
                      "event_count": 3,
                      "live_count": 1
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/events": {
      "get": {
        "operationId": "listEvents",
        "summary": "List events",
        "description": "Returns events aggregated from current odds snapshots. Supports filtering by sport, league, live status, date, and text search.\n\n**Authentication required.**\n",
        "tags": [
          "Events"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/status"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/q"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of events",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Data-Delay": {
                "$ref": "#/components/headers/X-Data-Delay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Event"
                          }
                        },
                        "meta": {
                          "type": "object",
                          "description": "Optional — omitted (never `null`) on responses with events. Present only on empty results (zero events), where it carries `store`.",
                          "properties": {
                            "store": {
                              "$ref": "#/components/schemas/EventsReadiness"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "nba-20260309-lal-bos",
                      "external_ids": {
                        "draftkings": "123456",
                        "fanduel": "789012"
                      },
                      "sport": "basketball",
                      "league": "nba",
                      "home_team": "Boston Celtics",
                      "away_team": "Los Angeles Lakers",
                      "start_time": "2026-03-09T19:30:00Z",
                      "status": "upcoming",
                      "is_live": false,
                      "book_count": 12,
                      "markets": [
                        "moneyline",
                        "point_spread",
                        "total_points",
                        "player_prop"
                      ],
                      "books": [
                        "betmgm",
                        "caesars",
                        "draftkings",
                        "fanduel"
                      ]
                    }
                  ],
                  "pagination": {
                    "limit": 50,
                    "offset": 0,
                    "count": 1,
                    "total": 1,
                    "has_more": false,
                    "next_offset": null
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/events/{eventId}": {
      "get": {
        "operationId": "getEvent",
        "summary": "Get event by ID",
        "description": "Returns details for a single event.",
        "tags": [
          "Events"
        ],
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "nba-20260309-lal-bos"
          }
        ],
        "responses": {
          "200": {
            "description": "Event details",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Event"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/events/{eventId}/markets": {
      "get": {
        "operationId": "getEventMarkets",
        "summary": "Get markets for an event",
        "description": "Returns available market types for a specific event.",
        "tags": [
          "Events"
        ],
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of markets for the event",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/events/{eventId}/odds": {
      "get": {
        "operationId": "getEventOdds",
        "summary": "Get odds for an event",
        "description": "Returns odds for a specific event, optionally filtered by sportsbook and market.",
        "tags": [
          "Events"
        ],
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sportsbook",
            "in": "query",
            "description": "Filter by sportsbook (comma-separated)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "market",
            "in": "query",
            "description": "Filter by market type (comma-separated)",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated odds for the event",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Odds"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/odds": {
      "get": {
        "operationId": "listOdds",
        "summary": "List odds",
        "description": "Returns odds across all sportsbooks and events. Supports extensive filtering and grouping. Results are ordered chronologically by event start time.\n",
        "tags": [
          "Odds"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/market"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/q"
          },
          {
            "$ref": "#/components/parameters/min_odds"
          },
          {
            "$ref": "#/components/parameters/max_odds"
          },
          {
            "name": "group_by",
            "in": "query",
            "description": "Group results by event",
            "schema": {
              "type": "string",
              "enum": [
                "event"
              ]
            }
          },
          {
            "name": "is_alternate_line",
            "in": "query",
            "description": "Filter on the row's main-line cohort. `false` returns main lines only, `true` returns alternate lines only, omit for both. A row is an alternate line when its `line` differs from the best-effort main line for its cohort. A cohort is (event, market_type, selection axis), resolved per sportsbook: the selection axis is the player for player-prop markets, the team side for spread and team-total markets, and the raw selection everywhere else. Rows in markets with no line (moneyline, outright) are always main lines. Note the cohort-pending case: a row whose cohort has not been published yet (cold start, brand-new event) has both `is_alternate_line` and `is_main_line` false, so `is_alternate_line=false` includes those rows while `is_main_line=true` excludes them. Use `is_main_line=true` for a strict main-lines-only view.",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "example": false
          },
          {
            "name": "is_main_line",
            "in": "query",
            "description": "Filter on the row's main-line cohort, positive polarity. `true` returns main lines only and excludes cohort-pending rows, `false` returns alternate lines only, omit for both. See `is_alternate_line` for the negative-polarity sibling and for how the two differ on cohort-pending rows. Also accepted on `/odds/best`.",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "example": true
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset_capped"
          },
          {
            "name": "include_futures",
            "in": "query",
            "description": "Include futures markets (season-long props, outrights, championship winners, etc.) in results. Defaults to false. Naming a futures market type via `market=` does not opt you in on its own — pass `include_futures=true` as well.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated odds",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/X-RateLimit-Reset"
              },
              "X-Data-Delay": {
                "$ref": "#/components/headers/X-Data-Delay"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Odds"
                          }
                        },
                        "meta": {
                          "type": "object",
                          "description": "Optional — omitted (never `null`) on ordinary non-empty single-book responses. Carries `store` on every empty result (zero rows), and `books` on queries that resolve to two or more sportsbooks — or, on an empty page, to none at all because the key's dashboard book selection resolved to nothing (`books.reason`).",
                          "properties": {
                            "store": {
                              "$ref": "#/components/schemas/StoreReadiness"
                            },
                            "books": {
                              "type": "object",
                              "description": "Resolved book scope. Present for queries resolving to two or more sportsbooks, so a book that contributed nothing to this page is not mistaken for a book SharpAPI does not carry — and on the empty page of a dashboard book selection that resolved to no servable book, where `in_scope` / `in_page` are empty and `reason` / `selected` name the cause.",
                              "required": [
                                "in_scope",
                                "in_page"
                              ],
                              "properties": {
                                "in_scope": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  },
                                  "description": "The resolved book set, sorted, whether or not each book contributed to this page. `[]` on the `selection_disjoint` page.",
                                  "example": [
                                    "draftkings",
                                    "fanduel",
                                    "pinnacle"
                                  ]
                                },
                                "in_page": {
                                  "type": "object",
                                  "additionalProperties": {
                                    "type": "integer"
                                  },
                                  "description": "Rows per book in this page. Books contributing nothing are omitted rather than emitted as 0 — `in_scope` already says they were in scope. `{}` on the `selection_disjoint` page.",
                                  "example": {
                                    "draftkings": 31,
                                    "fanduel": 19
                                  }
                                },
                                "reason": {
                                  "type": "string",
                                  "enum": [
                                    "selection_disjoint"
                                  ],
                                  "description": "Optional. Present only on the zero-book page: the key's dashboard sportsbook selection resolved to nothing — every selected book absent from the served snapshot or not included in the tier — while the tier alone would have resolved to at least one book. Takes precedence over `meta.store.reason`, which stays `no_match` (the store is populated). Never emitted on the Free tier, on an empty or warming store, or with an explicit `sportsbook=` filter (that case is answered with `403 tier_restricted` / `403 book_not_selected` / `503 book_unavailable` instead). The 200 stands.",
                                  "example": "selection_disjoint"
                                },
                                "selected": {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  },
                                  "description": "Optional; accompanies `reason`. The dashboard selection that resolved to nothing — canonical sportsbook IDs, sorted.",
                                  "example": [
                                    "betano",
                                    "bwin"
                                  ]
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/odds/best": {
      "get": {
        "operationId": "getBestOdds",
        "summary": "Best odds per selection",
        "description": "Returns the best available odds for each selection across all sportsbooks.",
        "tags": [
          "Odds"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/market"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated best odds",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Odds"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/odds/comparison": {
      "get": {
        "operationId": "compareOdds",
        "summary": "Compare odds across books",
        "description": "Returns side-by-side odds comparison for a specific event across all sportsbooks.",
        "tags": [
          "Odds"
        ],
        "parameters": [
          {
            "name": "event_id",
            "in": "query",
            "required": true,
            "description": "Event ID to compare. A single id only: unlike the `event_id` filter on the other paths, a comma-separated list is not honored here — exactly one of the ids is used and which one is not defined. The legacy spelling `event` is still accepted, with the same `Deprecation` and `Warning` headers described on the shared `event_id` parameter. No `Sunset` date is sent.",
            "schema": {
              "type": "string"
            },
            "example": "nba-20260309-lal-bos"
          },
          {
            "name": "market",
            "in": "query",
            "description": "Filter by market type (comma-separated)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include_futures",
            "in": "query",
            "description": "Include futures markets (season-long props, outrights, championship winners, etc.) in results. Defaults to false. Naming a futures market type via `market=` does not opt you in on its own — pass `include_futures=true` as well.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Odds comparison",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/Odds"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/odds/batch": {
      "post": {
        "operationId": "batchOdds",
        "summary": "Batch odds lookup",
        "description": "Returns odds for multiple events in a single request. Batch size limits vary by tier:\n- Free: 5 events\n- Hobby: 20 events\n- Pro: 50 events\n- Sharp/Enterprise: 100 events\n",
        "tags": [
          "Odds"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchOddsRequest"
              },
              "example": {
                "event_ids": [
                  "nba-20260309-lal-bos",
                  "nba-20260309-gsw-mia"
                ],
                "sportsbook": "draftkings",
                "market": "moneyline"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Odds for requested events",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/Odds"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/odds/delta": {
      "get": {
        "operationId": "getOddsDelta",
        "summary": "Odds changes since timestamp",
        "description": "Returns odds that have changed since the given ISO 8601 timestamp.",
        "tags": [
          "Odds"
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": true,
            "description": "ISO 8601 timestamp to get changes since",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2026-03-09T14:00:00Z"
          },
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/market"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset_capped"
          }
        ],
        "responses": {
          "200": {
            "description": "Odds changed since the given timestamp",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Odds"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/opportunities/ev": {
      "get": {
        "operationId": "listEVOpportunities",
        "summary": "List +EV opportunities",
        "description": "Returns positive expected value betting opportunities. Requires **Pro** tier or higher.\n\nEV is calculated by devigging sharp book lines (typically Pinnacle) and comparing\nagainst other sportsbook prices. When multiple sportsbooks are +EV on the same\nselection, a separate opportunity is returned for each book, ranked by EV%.\n\n**Sort options:** `ev` (default), `odds`, `confidence`, `start_time`, `kelly`.\n",
        "tags": [
          "Opportunities"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/market"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/min_odds"
          },
          {
            "$ref": "#/components/parameters/max_odds"
          },
          {
            "$ref": "#/components/parameters/min_ev"
          },
          {
            "$ref": "#/components/parameters/max_ev"
          },
          {
            "$ref": "#/components/parameters/min_market_width"
          },
          {
            "$ref": "#/components/parameters/max_market_width"
          },
          {
            "$ref": "#/components/parameters/max_odds_age"
          },
          {
            "$ref": "#/components/parameters/date_range"
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field",
            "schema": {
              "type": "string",
              "enum": [
                "ev",
                "-ev",
                "odds",
                "-odds",
                "confidence",
                "-confidence",
                "start_time",
                "-start_time",
                "kelly",
                "-kelly"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated EV opportunities",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/EVOpportunity"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "a1b2c3d4e5f67890",
                      "game_id": "nba-20260309-lal-bos",
                      "ev_percentage": 4.2,
                      "odds_american": 135,
                      "odds_decimal": 2.35,
                      "no_vig_odds": 125,
                      "selection": "Los Angeles Lakers",
                      "market": "moneyline",
                      "line": null,
                      "sportsbook": "draftkings",
                      "game": "Los Angeles Lakers @ Boston Celtics",
                      "sport": "basketball",
                      "league": "nba",
                      "home_team": "Boston Celtics",
                      "away_team": "Los Angeles Lakers",
                      "start_time": "2026-03-09T19:30:00Z",
                      "is_live": false,
                      "market_width": 6.5,
                      "fair_probability": 0.444,
                      "devig_method": "multiplicative",
                      "sharp_book": "pinnacle",
                      "external_event_id": "dk-123456",
                      "selection_id": "sel-789",
                      "detected_at": "2026-03-09T14:22:15Z",
                      "confidence_score": 78,
                      "kelly_percent": 0.031,
                      "book_count": 8,
                      "arb_available": false,
                      "arb_profit": null,
                      "is_player_prop": false,
                      "player_name": null,
                      "player_id": null,
                      "stat_category": null,
                      "possibly_stale": false,
                      "oldest_odds_age_seconds": 12,
                      "warnings": []
                    }
                  ],
                  "pagination": {
                    "limit": 50,
                    "offset": 0,
                    "has_more": false,
                    "next_offset": null
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/opportunities/arbitrage": {
      "get": {
        "operationId": "listArbitrageOpportunities",
        "summary": "List arbitrage opportunities",
        "description": "Returns arbitrage (sure-bet) opportunities across sportsbooks. Requires **Hobby** tier or higher.\n\n**Sort options:** `profit` (default), `start_time`, `sport`.\n\n**Format:** Set `format=csv` for CSV export. Set `group=best` to return only the highest-profit arb per event.\n",
        "tags": [
          "Opportunities"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/market"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/min_odds"
          },
          {
            "$ref": "#/components/parameters/max_odds"
          },
          {
            "$ref": "#/components/parameters/min_profit"
          },
          {
            "$ref": "#/components/parameters/max_odds_age"
          },
          {
            "$ref": "#/components/parameters/date_range"
          },
          {
            "name": "format",
            "in": "query",
            "description": "Response format",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ],
              "default": "json"
            }
          },
          {
            "name": "group",
            "in": "query",
            "description": "Grouping mode",
            "schema": {
              "type": "string",
              "enum": [
                "best"
              ]
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field",
            "schema": {
              "type": "string",
              "enum": [
                "profit",
                "-profit",
                "start_time",
                "-start_time",
                "sport",
                "-sport"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated arbitrage opportunities",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ArbitrageOpportunity"
                          }
                        }
                      }
                    }
                  ]
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/opportunities/middles": {
      "get": {
        "operationId": "listMiddleOpportunities",
        "summary": "List middle opportunities",
        "description": "Returns middle betting opportunities where both sides of a line can win simultaneously. Requires **Pro** tier or higher.\n\n**Sort options:** `quality` (default), `ev`, `probability`, `middle_size`.\n",
        "tags": [
          "Opportunities"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/market"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/min_odds"
          },
          {
            "$ref": "#/components/parameters/max_odds"
          },
          {
            "$ref": "#/components/parameters/min_size"
          },
          {
            "$ref": "#/components/parameters/max_odds_age"
          },
          {
            "$ref": "#/components/parameters/date_range"
          },
          {
            "name": "state",
            "in": "query",
            "description": "US state code for deep-link routing only; does not filter odds or return state-specific prices. Accepts the 50 US state codes plus dc. Unsupported nonempty codes (including on) return 400 invalid_filter. Omitted or empty values use pa.",
            "schema": {
              "type": "string",
              "default": "pa"
            },
            "example": "nj"
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field",
            "schema": {
              "type": "string",
              "enum": [
                "quality",
                "-quality",
                "ev",
                "-ev",
                "probability",
                "-probability",
                "middle_size",
                "-middle_size"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated middle opportunities",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/MiddleOpportunity"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/opportunities/middles/{id}": {
      "get": {
        "operationId": "getMiddleOpportunity",
        "summary": "Get middle opportunity by ID",
        "description": "Returns details for a single middle opportunity. Requires **Pro** tier or higher.",
        "tags": [
          "Opportunities"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "16-character hex opportunity ID",
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{16}$"
            },
            "example": "a1b2c3d4e5f67890"
          }
        ],
        "responses": {
          "200": {
            "description": "Middle opportunity details",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/MiddleOpportunity"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/opportunities/middles/summary": {
      "get": {
        "operationId": "getMiddlesSummary",
        "summary": "Middles summary by sport/league",
        "description": "Returns aggregated middle opportunity counts by sport and league. Requires **Pro** tier or higher.",
        "tags": [
          "Opportunities"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          }
        ],
        "responses": {
          "200": {
            "description": "Summary of middle opportunities",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          }
        }
      }
    },
    "/opportunities/low_hold": {
      "get": {
        "operationId": "listLowHoldOpportunities",
        "summary": "List low-hold opportunities",
        "description": "Returns markets with unusually low bookmaker hold (vig), indicating efficient pricing. Requires **Hobby** tier or higher.\n\n**Sort options:** `hold` (default), `market`, `sport`.\n",
        "tags": [
          "Opportunities"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/market"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/min_odds"
          },
          {
            "$ref": "#/components/parameters/max_odds"
          },
          {
            "$ref": "#/components/parameters/max_odds_age"
          },
          {
            "$ref": "#/components/parameters/date_range"
          },
          {
            "name": "max_hold",
            "in": "query",
            "description": "Maximum hold percentage to return",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "US state code for deep-link routing only; does not filter odds or return state-specific prices. Accepts the 50 US state codes plus dc. Unsupported nonempty codes (including on) return 400 invalid_filter. Omitted or empty values use pa.",
            "schema": {
              "type": "string",
              "default": "pa"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Sort field",
            "schema": {
              "type": "string",
              "enum": [
                "hold",
                "-hold",
                "market",
                "-market",
                "sport",
                "-sport"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated low-hold opportunities",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/LowHoldOpportunity"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stream": {
      "get": {
        "operationId": "stream",
        "summary": "Real-time SSE stream",
        "description": "Server-Sent Events stream for real-time odds and opportunity updates. Requires streaming access (WebSocket add-on, Enterprise tier, or active trial).\n\nUse `api_key` query parameter for authentication since `EventSource` cannot set headers.\n\n## Channels\n\n| Channel | Description |\n|---------|-------------|\n| `opportunities` | EV, arbitrage, middles, low-hold updates (default) |\n| `odds` | Raw odds changes |\n| `all` | Both odds and opportunities |\n| `gamestate` | Live game state updates (Game State add-on or Enterprise) |\n\n## Event Types\n\n| Event | Payload | Description |\n|-------|---------|-------------|\n| `connected` | `{filters, channels}` | Stream established |\n| `snapshot` | Array of items | Initial data dump (one per type) |\n| `snapshot:complete` | `{}` | All snapshots sent |\n| `odds:update` | `Odds` | Odds changed |\n| `odds:removed` | `{id, sportsbook}` | Selection removed |\n| `ev:detected` | `{opportunities: EVOpportunity[], count, type}` | New or updated +EV opportunities |\n| `ev:expired` | `{expired: id[], count, type}` | EV opportunities gone |\n| `arb:detected` | `{opportunities: ArbitrageOpportunity[], count, type}` | New or updated arbitrage |\n| `arb:expired` | `{expired: id[], count, type}` | Arbitrage gone |\n| `middles:detected` | `{opportunities: MiddleOpportunity[], count, type}` | New or updated middles |\n| `middles:expired` | `{expired: id[], count, type}` | Middles gone |\n| `low_hold:detected` | `{opportunities: LowHoldOpportunity[], count, type}` | New or updated low-hold |\n| `low_hold:expired` | `{expired: id[], count, type}` | Low-hold gone |\n| `gamestate:snapshot` | `{sport, events}` | Initial game state |\n| `gamestate:update` | `{sport, event_id, ...}` | Game state changed |\n| `heartbeat` | `{ts}` | Keep-alive (every 30s) |\n| `error` | `{message}` | Recoverable error |\n\nA `:detected` event carries new opportunities **and** updated versions of ones already sent: since 2026-09-26, an opportunity whose content changes (price, EV%, fair probability, leg odds, `is_suspended`, `quality_tier`) is re-sent under the same `id`. Upsert by `id`, and treat only an `id` you have not seen as new.\n\nSupports `Last-Event-ID` header for reconnection.\n",
        "tags": [
          "Streaming"
        ],
        "security": [
          {
            "ApiKeyQuery": []
          },
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "api_key",
            "in": "query",
            "description": "API key (for EventSource which cannot set headers)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channel",
            "in": "query",
            "description": "Stream channel",
            "schema": {
              "type": "string",
              "enum": [
                "opportunities",
                "odds",
                "all",
                "gamestate"
              ],
              "default": "opportunities"
            }
          },
          {
            "$ref": "#/components/parameters/sport"
          },
          {
            "$ref": "#/components/parameters/league"
          },
          {
            "$ref": "#/components/parameters/sportsbook"
          },
          {
            "$ref": "#/components/parameters/market"
          },
          {
            "$ref": "#/components/parameters/event_id"
          },
          {
            "$ref": "#/components/parameters/is_live"
          },
          {
            "$ref": "#/components/parameters/live"
          },
          {
            "$ref": "#/components/parameters/min_ev"
          },
          {
            "$ref": "#/components/parameters/min_profit"
          },
          {
            "name": "state",
            "in": "query",
            "description": "US state code for deep-link routing only; does not filter odds or return state-specific prices. Recognises the 50 US state codes plus dc. When a recognised code is set, deep_link values in odds events include ?state=XX. Omitted, empty, and unsupported codes all emit no ?state= — unlike the REST odds and single-redirect endpoints, the stream handshake does not reject an unsupported code, but emits filter_warning after connected — and the redirect endpoint then applies its own pa default.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SSE event stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "Server-Sent Events stream. Each event has `event:` type, `data:` JSON payload, and `id:` for reconnection."
                },
                "example": "event: connected\ndata: {\"filters\":{\"channel\":\"opportunities\",\"sport\":null},\"session_id\":\"abc123\"}\nid: 1709993400000-1\n\nevent: ev:detected\ndata: {\"id\":\"a1b2c3d4e5f67890\",\"ev_percentage\":4.2,\"sportsbook\":\"draftkings\",\"selection\":\"Los Angeles Lakers\",\"market\":\"moneyline\",\"sport\":\"basketball\",\"league\":\"nba\"}\nid: 1709993400000-2\n\nevent: heartbeat\ndata: {\"ts\":1709993430000}\nid: 1709993430000-3\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Streaming access required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "streaming_not_available",
                    "message": "Streaming requires WebSocket add-on or Enterprise tier",
                    "docs": "https://sharpapi.io/pricing"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/account": {
      "get": {
        "operationId": "getAccount",
        "summary": "Get account info",
        "description": "Returns API key details, tier, limits, feature access, and add-on status.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Account information",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Account"
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "data": {
                    "key_id": "key_abc123",
                    "tier": "pro",
                    "rate_limit": {
                      "requests_per_minute": 300,
                      "max_books": 15
                    },
                    "features": [
                      "odds",
                      "schedule",
                      "ev",
                      "arbitrage",
                      "middles",
                      "low_hold",
                      "closing_line",
                      "splits"
                    ],
                    "streaming": {
                      "enabled": false,
                      "max_connections": 0
                    }
                  },
                  "updated_at": "2026-02-08T14:44:32.123Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account/usage": {
      "get": {
        "operationId": "getAccountUsage",
        "summary": "Get usage statistics",
        "description": "Returns request counts, rate limit status, and stream usage for the current billing period.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "Usage statistics",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Usage"
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": {
                    "requests": {
                      "today": 1420,
                      "this_week": 8200,
                      "this_month": 32500,
                      "limit": {
                        "per_minute": 300,
                        "remaining": 285,
                        "resets_at": "2026-03-09T14:23:00Z"
                      }
                    },
                    "streams": {
                      "active": 0,
                      "max": 0
                    },
                    "period": {
                      "start": "2026-03-01T00:00:00Z",
                      "end": "2026-03-31T23:59:59Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account/keys": {
      "get": {
        "operationId": "listApiKeys",
        "summary": "List API keys",
        "description": "Returns all API keys for the authenticated user.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "List of API keys",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ApiKey"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "key_abc123",
                      "id_masked": "...key_abc123",
                      "name": "Production Key",
                      "tier": "pro",
                      "is_active": true,
                      "created_at": "2026-01-15T10:00:00Z",
                      "updated_at": "2026-03-01T00:00:00Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "operationId": "createApiKey",
        "summary": "Create a new API key",
        "description": "Creates a new API key for the authenticated user. The key value is returned only once in the response.\nMaximum keys per tier: Free/Hobby/Pro = 1, Sharp = 2, Enterprise = custom.\n",
        "tags": [
          "Account"
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Optional display name for the key"
                  }
                }
              },
              "example": {
                "name": "My new key"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New API key created (key value shown only once)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "required": [
                            "id",
                            "key",
                            "tier"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "key": {
                              "type": "string",
                              "description": "The API key value (shown only once)"
                            },
                            "name": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "tier": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "key_new456",
                    "key": "sk_live_abc123def456",
                    "name": "My new key",
                    "tier": "pro"
                  },
                  "meta": {
                    "warning": "This is the only time the key value will be shown. Store it securely."
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account/keys/{keyId}": {
      "delete": {
        "operationId": "deleteApiKey",
        "summary": "Revoke an API key",
        "description": "Permanently revokes a specific API key. Users can only revoke their own keys.",
        "tags": [
          "Account"
        ],
        "parameters": [
          {
            "name": "keyId",
            "in": "path",
            "required": true,
            "description": "The Unkey key ID to revoke",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key revoked successfully",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "revoked": {
                              "type": "boolean"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "key_abc123",
                    "revoked": true
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/account/keys/{keyId}/rotate": {
      "post": {
        "operationId": "rotateApiKey",
        "summary": "Rotate an API key",
        "description": "Creates a new API key and revokes the old one. Supports an optional grace period\n(0-72 hours) to keep the old key valid during migration.\n",
        "tags": [
          "Account"
        ],
        "parameters": [
          {
            "name": "keyId",
            "in": "path",
            "required": true,
            "description": "The Unkey key ID to rotate",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "gracePeriodHours": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 72,
                    "default": 0,
                    "description": "Hours to keep the old key valid (0 = immediate revocation)"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Optional name for the new key"
                  }
                }
              },
              "example": {
                "gracePeriodHours": 24,
                "name": "Rotated production key"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key rotated successfully (new key value shown only once)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "required": [
                            "new_key_id",
                            "new_key",
                            "old_key_id"
                          ],
                          "properties": {
                            "new_key_id": {
                              "type": "string"
                            },
                            "new_key": {
                              "type": "string",
                              "description": "The new API key value (shown only once)"
                            },
                            "old_key_id": {
                              "type": "string"
                            },
                            "old_key_expires_at": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "format": "date-time",
                              "description": "When the old key expires (null if immediate)"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/deeplinks/batch": {
      "post": {
        "operationId": "batchDeepLinks",
        "summary": "Batch deep link lookup",
        "description": "Returns redirect paths for multiple odds IDs or opportunity hash IDs. Resolved IDs return a redirect path, unresolvable IDs return null. Requires **Hobby** tier or higher.",
        "tags": [
          "Deep Links"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 500
                  },
                  "state": {
                    "description": "US state code for deep-link routing only; does not filter odds or return state-specific prices. Uses the 50 US state codes plus dc; omitted, empty, or unsupported values (including on) fall back to pa in this batch endpoint. The single-link redirect rejects unsupported nonempty codes instead.",
                    "type": "string",
                    "default": "pa"
                  }
                }
              },
              "example": {
                "ids": [
                  "17336125542407",
                  "77b0749a1faae425"
                ],
                "state": "nj"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Redirect paths for each ID",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "additionalProperties": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Redirect path (e.g. /api/v1/deeplink/{id}) or null if unavailable"
                      }
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "data": {
                    "17336125542407": "/api/v1/deeplink/17336125542407",
                    "77b0749a1faae425": null
                  },
                  "updated_at": "2026-02-11T12:00:15.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          }
        }
      }
    },
    "/deeplink/{id}": {
      "get": {
        "operationId": "redirectDeepLink",
        "summary": "Deep link redirect",
        "description": "Redirects to a sportsbook deep link. No authentication required.\nReturns a 302 redirect to the sportsbook URL, or 404 if the link cannot be generated.\n",
        "tags": [
          "Deep Links"
        ],
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Opportunity or event identifier",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "US state code for deep-link routing only; does not filter odds or return state-specific prices. Accepts the 50 US state codes plus dc. Unsupported nonempty codes (including on) return 400 invalid_filter. Omitted or empty values use pa.",
            "schema": {
              "type": "string",
              "default": "pa"
            }
          },
          {
            "name": "book",
            "in": "query",
            "description": "Target sportsbook",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "game_id",
            "in": "query",
            "description": "Game/event ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sport",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "league",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "home",
            "in": "query",
            "description": "Home team name",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "away",
            "in": "query",
            "description": "Away team name",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fallback",
            "in": "query",
            "description": "Where to redirect if the deep link cannot be resolved. Honoured only for an absolute http or https URL, with no port or user info, on sharpapi.io, sharplines.io or the website of a sportsbook the endpoint recognises (subdomains count). Any other value is ignored, so an unresolved ID returns 404.",
            "schema": {
              "type": "string",
              "format": "uri"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to sportsbook",
            "headers": {
              "Location": {
                "description": "Sportsbook deep link URL",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "404": {
            "description": "Deep link not found, and no accepted `fallback` URL was given"
          }
        }
      }
    },
    "/futures": {
      "get": {
        "operationId": "listFuturesMarkets",
        "summary": "List futures markets",
        "description": "Returns available futures markets with metadata, merged across all sportsbooks. Public endpoint.",
        "tags": [
          "Futures"
        ],
        "security": [
          {},
          {
            "ApiKeyHeader": []
          }
        ],
        "parameters": [
          {
            "name": "sport",
            "in": "query",
            "description": "Filter by sport",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "league",
            "in": "query",
            "description": "Filter by league",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "future_type",
            "in": "query",
            "description": "Filter by futures type",
            "schema": {
              "type": "string",
              "enum": [
                "TEAM",
                "PLAYER",
                "OVER_UNDER",
                "YES_NO"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of futures markets",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/FutureMarket"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "market_id": "championship_winner",
                      "display_name": "NBA Championship Winner",
                      "sport": "basketball",
                      "league": "nba",
                      "future_type": "TEAM",
                      "selection_count": 30,
                      "books_available": [
                        "draftkings",
                        "fanduel",
                        "betmgm"
                      ]
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/futures/odds": {
      "get": {
        "operationId": "listFuturesOdds",
        "summary": "Get futures odds",
        "description": "Returns cross-book odds for a specific futures market. Requires authentication.",
        "tags": [
          "Futures"
        ],
        "parameters": [
          {
            "name": "future_id",
            "in": "query",
            "required": true,
            "description": "Futures market ID",
            "schema": {
              "type": "string"
            },
            "example": "championship_winner"
          },
          {
            "name": "sportsbook",
            "in": "query",
            "description": "Filter by sportsbook (comma-separated)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sport",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "league",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated futures odds",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/PaginatedEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/FutureOdds"
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "selection": "Boston Celtics",
                      "sportsbook": "draftkings",
                      "odds_american": 350,
                      "odds_decimal": 4.5,
                      "probability": 0.2222,
                      "future_type": "TEAM",
                      "future_market_id": "championship_winner",
                      "future_market_name": "NBA Championship Winner",
                      "sport": "basketball",
                      "league": "nba",
                      "event_start_time": "2026-06-15T00:00:00Z",
                      "timestamp": "2026-03-09T14:00:00Z"
                    }
                  ],
                  "pagination": {
                    "limit": 100,
                    "offset": 0,
                    "has_more": false,
                    "next_offset": null
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/gamestate": {
      "get": {
        "operationId": "listAllGameState",
        "summary": "Live game state for every sport",
        "description": "Returns aggregated live game state (scores, periods, clocks, situational data) for every sport with active events. Merged across sportsbooks with a consensus-based algorithm so a single misbehaving book can't skew the result. Requires the **Game State add-on** ($79/mo, available on any paid plan) or the **Enterprise** tier. A fixture that has just finished stays here for 15 minutes after completion as a terminal row (`status: \"final\"`) — see the `status` / `is_live` parameters and the `EventGameState` schema.",
        "tags": [
          "Game State"
        ],
        "responses": {
          "200": {
            "description": "Live game state keyed by sport then event_id",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "description": "Map of sport name to a map of event_id → EventGameState",
                          "additionalProperties": {
                            "type": "object",
                            "additionalProperties": {
                              "$ref": "#/components/schemas/EventGameState"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "data": {
                    "soccer": {
                      "argentina_-_primera_division_bocajuniors_defensayjusticia_2026-04-23": {
                        "home_team": "Defensa y Justicia",
                        "away_team": "Boca Juniors",
                        "sport": "soccer",
                        "league": "argentina_-_primera_division",
                        "home_score": 0,
                        "away_score": 1,
                        "game_period": "2H",
                        "game_clock": "49:06",
                        "is_live": true,
                        "possession": "away",
                        "corners_home": 1,
                        "corners_away": 0,
                        "primary_book": "draftkings",
                        "book_count": 6
                      },
                      "montenegro_-_prva_liga_bokelj_otrantolympiculcinj_2026-04-23": {
                        "home_team": "Bokelj",
                        "away_team": "FK Otrant-Olympic Ulcinj",
                        "sport": "soccer",
                        "league": "montenegro_-_prva_liga",
                        "home_score": 2,
                        "away_score": 1,
                        "score_type": "points",
                        "winner": "home",
                        "status": "final",
                        "completed_at": "2026-04-23T23:48:55Z",
                        "is_live": false,
                        "stale": false
                      }
                    },
                    "baseball": {
                      "mlb_redsox_yankees_2026-04-23": {
                        "home_team": "New York Yankees",
                        "away_team": "Boston Red Sox",
                        "sport": "baseball",
                        "league": "mlb",
                        "home_score": 2,
                        "away_score": 1,
                        "game_period": "T5",
                        "is_live": true,
                        "primary_book": "fliff",
                        "book_count": 9
                      }
                    }
                  },
                  "updated_at": "2026-04-23T23:55:01.234Z"
                }
              }
            }
          },
          "400": {
            "description": "Unrecognised `status` or `is_live` value. The error body names the accepted set in `details.accepted`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "validation_error",
                    "message": "is_live must be one of: true, false",
                    "details": {
                      "parameter": "is_live",
                      "accepted": [
                        "true",
                        "false"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          }
        },
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "final"
              ],
              "example": "final"
            },
            "description": "Return only the carried terminal (finished) rows. `final` is the only accepted value — live rows are the default set, and `is_live=true` selects them alone. Case-insensitive. Any other value returns `400 validation_error` with `details.accepted` = `[\"final\"]`."
          },
          {
            "name": "is_live",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "true"
            },
            "description": "`true` returns live rows only, excluding every carried terminal row; `false` returns non-live rows only, which today means the carried terminal rows. Case-insensitive. Any other value — including `1` and `0` — returns `400 validation_error` with `details.accepted` = `[\"true\",\"false\"]`."
          }
        ]
      }
    },
    "/gamestate/{sport}": {
      "get": {
        "operationId": "getGameStateForSport",
        "summary": "Live game state for a single sport",
        "description": "Returns aggregated live game state for events in one sport. Response shape matches `GET /gamestate` but `data` contains a single key (the path `sport`). Unknown sports return an empty `data` object, never 404. Requires the **Game State add-on** ($79/mo, available on any paid plan) or the **Enterprise** tier. A fixture that has just finished stays here for 15 minutes after completion as a terminal row (`status: \"final\"`) — see the `status` / `is_live` parameters and the `EventGameState` schema.",
        "tags": [
          "Game State"
        ],
        "parameters": [
          {
            "name": "sport",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "soccer"
            },
            "description": "Atlas sport id. Case-insensitive. Known values: `baseball`, `basketball`, `football`, `hockey`, `soccer`, `tennis`, `table_tennis`, `cricket`, `esports`, `snooker`, `other`."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "final"
              ],
              "example": "final"
            },
            "description": "Return only the carried terminal (finished) rows. `final` is the only accepted value — live rows are the default set, and `is_live=true` selects them alone. Case-insensitive. Any other value returns `400 validation_error` with `details.accepted` = `[\"final\"]`."
          },
          {
            "name": "is_live",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "true"
            },
            "description": "`true` returns live rows only, excluding every carried terminal row; `false` returns non-live rows only, which today means the carried terminal rows. Case-insensitive. Any other value — including `1` and `0` — returns `400 validation_error` with `details.accepted` = `[\"true\",\"false\"]`."
          }
        ],
        "responses": {
          "200": {
            "description": "Live game state for the requested sport (envelope identical to `/gamestate`, just scoped to one sport)",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DataEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "object",
                          "description": "Map with a single key (the path sport) whose value is a map of event_id → EventGameState.",
                          "additionalProperties": {
                            "type": "object",
                            "additionalProperties": {
                              "$ref": "#/components/schemas/EventGameState"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "data": {
                    "soccer": {
                      "chile_-_primera_division_concepcion_palestino_2026-04-24": {
                        "home_team": "Palestino",
                        "away_team": "Concepción",
                        "sport": "soccer",
                        "league": "chile_-_primera_division",
                        "home_score": 0,
                        "away_score": 0,
                        "is_live": true,
                        "primary_book": "unibet",
                        "book_count": 1,
                        "stale": true
                      },
                      "montenegro_-_prva_liga_bokelj_otrantolympiculcinj_2026-04-23": {
                        "home_team": "Bokelj",
                        "away_team": "FK Otrant-Olympic Ulcinj",
                        "sport": "soccer",
                        "league": "montenegro_-_prva_liga",
                        "home_score": 2,
                        "away_score": 1,
                        "score_type": "points",
                        "winner": "home",
                        "status": "final",
                        "completed_at": "2026-04-23T23:48:55Z",
                        "is_live": false,
                        "stale": false
                      }
                    }
                  },
                  "updated_at": "2026-04-23T23:55:01.234Z"
                }
              }
            }
          },
          "400": {
            "description": "Unrecognised `status` or `is_live` value. The error body names the accepted set in `details.accepted`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": {
                    "code": "validation_error",
                    "message": "is_live must be one of: true, false",
                    "details": {
                      "parameter": "is_live",
                      "accepted": [
                        "true",
                        "false"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierRestricted"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key passed in the `X-API-Key` header"
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key passed as a Bearer token in the `Authorization` header"
      },
      "ApiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "api_key",
        "description": "API key as query parameter (for SSE/EventSource only)"
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Maximum requests allowed per minute",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Requests remaining in the current window",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Reset": {
        "description": "Unix timestamp (ms) when the rate limit window resets",
        "schema": {
          "type": "integer"
        }
      },
      "X-Data-Delay": {
        "description": "Odds data delay in seconds (0 = real-time)",
        "schema": {
          "type": "integer"
        }
      },
      "X-Request-Id": {
        "description": "Unique request identifier",
        "schema": {
          "type": "string"
        }
      }
    },
    "parameters": {
      "sport": {
        "name": "sport",
        "in": "query",
        "description": "Filter by sport (comma-separated)",
        "schema": {
          "type": "string"
        },
        "example": "basketball"
      },
      "league": {
        "name": "league",
        "in": "query",
        "description": "Filter by league (comma-separated)",
        "schema": {
          "type": "string"
        },
        "example": "nba"
      },
      "sportsbook": {
        "name": "sportsbook",
        "in": "query",
        "description": "Filter by sportsbook (comma-separated). **Intentionally not enumerated.** This one parameter definition is shared by eleven operations, and they do not accept the same set of values, so no single `enum` would be correct for all of them. Nine of them validate the value and answer `400 invalid_filter` for one they do not accept; the other two do not reject an unrecognised value at all — `/splits/history` returns no rows for it and `/stream` opens the stream. See `/sportsbooks` for the book roster. The clearest per-operation difference is the synthetic `consensus` value: it is meaningful only on `/splits`, and every other operation that validates this filter rejects it.",
        "schema": {
          "type": "string"
        },
        "example": "draftkings,fanduel"
      },
      "status": {
        "name": "status",
        "in": "query",
        "description": "Filter by event lifecycle state. Omit for the default result set, which contains `upcoming` and `live` events only. `final` selects retained completions instead — a separate result set ordered newest-completion-first, whose rows omit `start_time` and carry `completed_at`. An unrecognised value is rejected with `400 validation_error`.",
        "schema": {
          "type": "string",
          "enum": [
            "upcoming",
            "live",
            "final"
          ]
        }
      },
      "market": {
        "name": "market",
        "in": "query",
        "description": "Filter by market type (comma-separated)",
        "schema": {
          "type": "string"
        },
        "example": "moneyline,point_spread"
      },
      "event_id": {
        "name": "event_id",
        "in": "query",
        "description": "Filter by event ID (comma-separated). `event_id` is the canonical name. The legacy spellings `event`, `events` and `event_ids` are still accepted, but a request that uses one is answered with `Deprecation: true` and `Warning: 299 - \"filter alias(es) ... deprecated; use event_id\"`. No `Sunset` date is sent. Send `event_id` to avoid those headers.",
        "schema": {
          "type": "string"
        }
      },
      "live": {
        "name": "live",
        "in": "query",
        "deprecated": true,
        "description": "Deprecated alias of `is_live`, still accepted and behaving identically. A request using it is answered with `Deprecation: true` and `Warning: 299 - \"filter alias(es) live deprecated; use is_live\"`. No `Sunset` date is sent. Prefer `is_live`.",
        "schema": {
          "type": "boolean"
        }
      },
      "is_live": {
        "name": "is_live",
        "in": "query",
        "description": "Filter by live status. `is_live` is the canonical name. The legacy spelling `live` is still accepted, but a request that uses it is answered with `Deprecation: true` and `Warning: 299 - \"filter alias(es) live deprecated; use is_live\"`. Send `is_live` to avoid those headers.",
        "schema": {
          "type": "boolean"
        }
      },
      "has_score": {
        "name": "has_score",
        "in": "query",
        "description": "Filter events that have (true) or lack (false) game state scores",
        "schema": {
          "type": "boolean"
        }
      },
      "date": {
        "name": "date",
        "in": "query",
        "description": "Filter by event date (YYYY-MM-DD)",
        "schema": {
          "type": "string",
          "format": "date"
        },
        "example": "2026-03-09"
      },
      "q": {
        "name": "q",
        "in": "query",
        "description": "Full-text search on team names",
        "schema": {
          "type": "string"
        }
      },
      "min_odds": {
        "name": "min_odds",
        "in": "query",
        "description": "Minimum American odds",
        "schema": {
          "type": "number"
        }
      },
      "max_odds": {
        "name": "max_odds",
        "in": "query",
        "description": "Maximum American odds",
        "schema": {
          "type": "number"
        }
      },
      "min_ev": {
        "name": "min_ev",
        "in": "query",
        "description": "Minimum EV percentage",
        "schema": {
          "type": "number",
          "default": 0
        }
      },
      "max_ev": {
        "name": "max_ev",
        "in": "query",
        "description": "Maximum EV percentage",
        "schema": {
          "type": "number"
        }
      },
      "min_profit": {
        "name": "min_profit",
        "in": "query",
        "description": "Minimum profit percentage",
        "schema": {
          "type": "number",
          "default": 0.5
        }
      },
      "min_size": {
        "name": "min_size",
        "in": "query",
        "description": "Minimum middle size",
        "schema": {
          "type": "number",
          "default": 0.5
        }
      },
      "min_market_width": {
        "name": "min_market_width",
        "in": "query",
        "description": "Minimum market width",
        "schema": {
          "type": "number"
        }
      },
      "max_market_width": {
        "name": "max_market_width",
        "in": "query",
        "description": "Maximum market width",
        "schema": {
          "type": "number"
        }
      },
      "max_odds_age": {
        "name": "max_odds_age",
        "in": "query",
        "description": "Maximum odds age in seconds (filters out stale odds)",
        "schema": {
          "type": "number"
        }
      },
      "date_range": {
        "name": "date_range",
        "in": "query",
        "description": "Date range filter",
        "schema": {
          "type": "string",
          "enum": [
            "today",
            "tomorrow",
            "week"
          ]
        }
      },
      "limit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum results per page",
        "schema": {
          "type": "integer",
          "default": 50,
          "minimum": 0,
          "maximum": 500
        }
      },
      "offset": {
        "name": "offset",
        "in": "query",
        "description": "Number of results to skip",
        "schema": {
          "type": "integer",
          "default": 0,
          "minimum": 0
        }
      },
      "offset_capped": {
        "name": "offset",
        "in": "query",
        "description": "Number of results to skip. Must be 500 or less on this endpoint; a higher value returns 400 `offset_too_large`. To page deeper, use `cursor` on `/odds` and follow `next_offset` on `/odds/delta`.",
        "schema": {
          "type": "integer",
          "default": 0,
          "minimum": 0,
          "maximum": 500
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "missing": {
                "summary": "Missing API key",
                "value": {
                  "error": {
                    "code": "missing_api_key",
                    "message": "API key required",
                    "docs": "https://docs.sharpapi.io/en/authentication#authentication-methods"
                  }
                }
              },
              "invalid": {
                "summary": "Invalid API key",
                "value": {
                  "error": {
                    "code": "invalid_api_key",
                    "message": "Invalid API key",
                    "docs": "https://docs.sharpapi.io/en/authentication#authentication-methods"
                  }
                }
              }
            }
          }
        }
      },
      "TierRestricted": {
        "description": "Feature requires a higher subscription tier",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "tier_restricted",
                "message": "EV opportunities requires pro tier or higher",
                "tier": "hobby",
                "required_tier": "pro",
                "docs": "https://sharpapi.io/pricing"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/X-RateLimit-Limit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/X-RateLimit-Remaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/X-RateLimit-Reset"
          },
          "Retry-After": {
            "description": "Seconds until the rate limit resets",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "Rate limit exceeded",
                "retry_after": 1709993460000,
                "retryAfter": 3,
                "docs": "https://docs.sharpapi.io/en/authentication#rate-limits"
              }
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid request parameters",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "invalid_request",
                "message": "Missing required parameter"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "Resource not found"
              }
            }
          }
        }
      }
    },
    "schemas": {
      "PaginatedEnvelope": {
        "type": "object",
        "description": "Standard shape for paginated list responses. No `success` flag — HTTP status codes indicate success/failure.",
        "required": [
          "data",
          "pagination",
          "updated_at"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {}
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Server emit time (RFC 3339)."
          }
        }
      },
      "Pagination": {
        "type": "object",
        "required": [
          "limit",
          "offset",
          "has_more",
          "next_offset"
        ],
        "properties": {
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "count": {
            "type": "integer",
            "description": "Length of the current `data` page.",
            "example": 50
          },
          "total": {
            "type": "integer",
            "description": "Size of the full matching set. Emitted where the endpoint computes it (`/events` does); `/odds` omits it.",
            "example": 1247
          },
          "has_more": {
            "type": "boolean"
          },
          "next_offset": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "StoreReadiness": {
        "type": "object",
        "description": "Explains an empty `/odds` result set. Attached at `meta.store` only when zero rows are returned (September 2026); responses with rows are byte-identical to before and carry no `meta.store`. `books` and `rows` describe the whole snapshot the response was served from, not the matched set. `event_status` and `completed_at` (both optional) additionally name a finished event when the empty page was filtered on exactly one `event_id`.",
        "required": [
          "generation",
          "ready",
          "books",
          "rows",
          "reason"
        ],
        "properties": {
          "generation": {
            "type": "integer",
            "description": "Snapshot generation this response was served from. `0` means nothing has been loaded since the process started.",
            "example": 5019
          },
          "ready": {
            "type": "boolean",
            "description": "At least one refresh cycle has completed since the serving instance's last mode transition.",
            "example": true
          },
          "books": {
            "type": "integer",
            "description": "Sportsbooks present in the snapshot this response was served from, independent of the query.",
            "example": 39
          },
          "rows": {
            "type": "integer",
            "description": "Total odds rows held by that snapshot, independent of the query — evidence that the store was populated, not a count of what the query would have matched.",
            "example": 810137
          },
          "reason": {
            "type": "string",
            "enum": [
              "warming",
              "store_empty",
              "no_match"
            ],
            "description": "The field to branch on. `warming` = the serving instance has not completed a refresh cycle since its last mode transition, nothing is loaded yet (retry); `store_empty` = ready but the instance holds zero odds rows, e.g. mid-way through a store swap (retry — do not treat the empty page as authoritative); `no_match` = ready and populated, the query matched nothing (authoritative — retrying will not change the answer).",
            "example": "no_match"
          },
          "event_status": {
            "type": "string",
            "enum": [
              "final",
              "gone"
            ],
            "description": "Optional. Present only when the page is empty with `reason: \"no_match\"`, the request filtered on exactly one `event_id`, and that event is known to have ended. `final` = a settled completion is retained — the same record `/events/{eventId}` serves as `status: \"final\"`. `gone` = the event left the schedule within roughly the last hour with no settled completion (postponed, cancelled, or inside the ~5-minute settlement lag) — the same condition under which `/events/{eventId}` answers 410 Gone, and not a claim that it finished. Omitted for live/upcoming events, unknown ids, comma-separated multi-id filters, and on `warming` / `store_empty` pages. The status code is unchanged (200).",
            "example": "final"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "description": "Optional. Accompanies `event_status: \"final\"` only — the time the completion was detected (typically a few minutes after the event left the live feed, not the final whistle), identical to `completed_at` on `/events/{eventId}`. Never present with `gone`.",
            "example": "2026-09-07T02:09:37Z"
          }
        }
      },
      "EventsReadiness": {
        "type": "object",
        "description": "Explains an empty `/events` page. Attached at `meta.store` only when zero events are returned (September 2026); responses with events are unchanged. Same `reason` vocabulary as `StoreReadiness`. Deliberately carries no `generation` — that counter belongs to the odds store, which does not back `/events`.",
        "required": [
          "ready",
          "events",
          "reason"
        ],
        "properties": {
          "ready": {
            "type": "boolean",
            "description": "At least one refresh cycle has completed since the serving instance's last mode transition.",
            "example": true
          },
          "events": {
            "type": "integer",
            "description": "Events in the cache this page was built from, independent of the query. `?status=final` pages are served from the settled-event store and report that store's size instead.",
            "example": 11064
          },
          "reason": {
            "type": "string",
            "enum": [
              "warming",
              "store_empty",
              "no_match"
            ],
            "description": "The field to branch on. `warming` = the serving instance has not completed a refresh cycle since its last mode transition, nothing is loaded yet (retry); `store_empty` = ready but the event cache this page was built from is empty (retry — do not treat the empty page as authoritative); `no_match` = ready and populated, the query matched nothing (authoritative — retrying will not change the answer).",
            "example": "no_match"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_api_key",
                  "invalid_api_key",
                  "unauthorized",
                  "forbidden",
                  "tier_restricted",
                  "rate_limited",
                  "invalid_request",
                  "validation_error",
                  "not_found",
                  "streaming_not_available",
                  "stream_limit_exceeded",
                  "service_unavailable",
                  "internal_error"
                ],
                "description": "Machine-readable error code"
              },
              "message": {
                "type": "string",
                "description": "Human-readable error message"
              },
              "docs": {
                "type": "string",
                "format": "uri",
                "description": "Link to relevant documentation"
              },
              "tier": {
                "type": "string",
                "description": "Current subscription tier"
              },
              "required_tier": {
                "type": "string",
                "description": "Minimum tier required for this feature"
              },
              "retry_after": {
                "type": "integer",
                "description": "Unix timestamp (ms) when request can be retried"
              },
              "retryAfter": {
                "type": "integer",
                "description": "Seconds until the request can be retried — same instant as retry_after, different unit and casing"
              },
              "max_books": {
                "type": "integer"
              },
              "requested_books": {
                "type": "integer"
              }
            }
          }
        }
      },
      "Sport": {
        "type": "object",
        "required": [
          "id",
          "name",
          "event_count",
          "live_count",
          "leagues"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "basketball"
          },
          "name": {
            "type": "string",
            "example": "Basketball"
          },
          "event_count": {
            "type": "integer"
          },
          "live_count": {
            "type": "integer"
          },
          "leagues": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "League": {
        "type": "object",
        "required": [
          "id",
          "name",
          "sport",
          "event_count",
          "live_count"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "nba"
          },
          "name": {
            "type": "string",
            "example": "NBA"
          },
          "sport": {
            "type": "string",
            "example": "basketball"
          },
          "event_count": {
            "type": "integer"
          },
          "live_count": {
            "type": "integer"
          }
        }
      },
      "Sportsbook": {
        "type": "object",
        "required": [
          "id",
          "name",
          "display_name",
          "is_sharp",
          "has_live_odds",
          "has_player_props",
          "has_limits",
          "regions",
          "requires_tier",
          "status",
          "event_count"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "draftkings"
          },
          "name": {
            "type": "string",
            "example": "draftkings"
          },
          "display_name": {
            "type": "string",
            "example": "DraftKings"
          },
          "is_sharp": {
            "type": "boolean"
          },
          "has_live_odds": {
            "type": "boolean"
          },
          "has_player_props": {
            "type": "boolean"
          },
          "has_limits": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the book publishes per-selection bet limits (`max_bet` on odds rows). DERIVED from the book's currently-served snapshot on every recompute — not declared per book the way `has_live_odds` / `has_player_props` are. `true` = at least one row in the current snapshot carries `max_bet` (individual rows may still omit it); `false` = rows were observed and none carry it; `null` = no rows observed, so no claim is made (books not yet live and books with zero loaded rows). Key always present. Recomputed alongside `last_update` and inherits its staleness bound (worst case ~40s, typically <=15s). Added September 2026.",
            "example": false
          },
          "regions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requires_tier": {
            "type": "string",
            "enum": [
              "free",
              "sharp"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive",
              "limited"
            ]
          },
          "event_count": {
            "type": "integer"
          },
          "last_update": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "unlisted": {
            "type": "boolean"
          }
        }
      },
      "Market": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "hasLine"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "moneyline"
          },
          "name": {
            "type": "string",
            "example": "Moneyline"
          },
          "description": {
            "type": "string"
          },
          "hasLine": {
            "type": "boolean"
          },
          "examples": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "event_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "selection_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "sports": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Team": {
        "type": "object",
        "required": [
          "id",
          "name",
          "sport",
          "leagues",
          "aliases",
          "event_count",
          "live_count"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "boston-celtics"
          },
          "name": {
            "type": "string",
            "example": "Boston Celtics"
          },
          "sport": {
            "type": "string",
            "example": "basketball"
          },
          "leagues": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "aliases": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "event_count": {
            "type": "integer"
          },
          "live_count": {
            "type": "integer"
          }
        }
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "sport",
          "league",
          "home_team",
          "away_team",
          "status",
          "is_live",
          "book_count",
          "markets",
          "books"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "nba-20260309-lal-bos"
          },
          "external_ids": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Sportsbook-native event IDs keyed by book"
          },
          "sport": {
            "type": "string",
            "example": "basketball"
          },
          "league": {
            "type": "string",
            "example": "nba"
          },
          "home_team": {
            "type": "string",
            "example": "Boston Celtics"
          },
          "away_team": {
            "type": "string",
            "example": "Los Angeles Lakers"
          },
          "start_time": {
            "type": "string",
            "format": "date-time",
            "description": "Scheduled kickoff, ISO 8601. **Optional — the key is absent when the row has no kickoff time.** A completed event served from the retention store carries none, so `/events?status=final` and `/events/{eventId}` omit the key rather than send an empty string. Branch on key presence, not on `status`: a finished event whose odds have not yet drained from the live cache still carries a real `start_time`."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "description": "Completion time, ISO 8601. Present only on terminal (`status: \"final\"`) rows; absent on `upcoming` and `live` events."
          },
          "status": {
            "type": "string",
            "enum": [
              "upcoming",
              "live",
              "final"
            ],
            "description": "Event lifecycle state. The unfiltered `/events` list contains `upcoming` and `live` only — `final` appears there only under `?status=final`. `/events/{eventId}` returns `final` for any completed event."
          },
          "is_live": {
            "type": "boolean"
          },
          "book_count": {
            "type": "integer"
          },
          "markets": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "books": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "game_state": {
            "$ref": "#/components/schemas/EventGameState"
          }
        }
      },
      "EventGameState": {
        "type": "object",
        "description": "Aggregated live game state for a single event, merged across the sportsbooks covering it. Scores are consensus-selected; temporal fields use sport-aware clock direction; situational fields priority-fill from a fixed book ranking. Every field is optional except the always-present core set — sport coverage varies, so e.g. tennis rows carry sets_home/server while hockey rows carry power_play. Rows also come in a terminal flavour: a fixture that has finished is carried here for 15 minutes after completion with status \"final\", completed_at, score_type and the final scores. A terminal row is projected from the completion record, not from a live cross-book merge, so it carries no primary_book/book_count/game_period/game_clock and no situational fields — branch on status == \"final\" before reading those.",
        "required": [
          "home_team",
          "away_team",
          "sport",
          "league",
          "is_live"
        ],
        "properties": {
          "home_team": {
            "type": "string",
            "description": "Normalized home team name."
          },
          "away_team": {
            "type": "string",
            "description": "Normalized away team name."
          },
          "sport": {
            "type": "string",
            "description": "Atlas sport id (e.g. `baseball`, `soccer`)."
          },
          "league": {
            "type": "string",
            "description": "Atlas league id (e.g. `mlb`, `england_-_premier_league`)."
          },
          "home_score": {
            "type": "integer",
            "description": "Current home score in the natural unit for the sport (runs, points, goals, sets — tennis uses summed set points). Present on live rows and on terminal rows whose `score_type` is `points`; a terminal row with `score_type: \"sets\"` carries `sets_home`/`sets_away` instead and omits this."
          },
          "away_score": {
            "type": "integer",
            "description": "Current away score. Present on live rows and on terminal rows whose `score_type` is `points`; a terminal row with `score_type: \"sets\"` carries `sets_home`/`sets_away` instead and omits this."
          },
          "is_live": {
            "type": "boolean",
            "description": "`true` for a live event; `false` on a carried terminal (finished) row."
          },
          "primary_book": {
            "type": "string",
            "description": "The sportsbook whose scores were selected by the merge. Consensus-respecting — if ≥2 books agree on a score the highest-priority book in that group wins. Live rows only — absent on a terminal row."
          },
          "book_count": {
            "type": "integer",
            "description": "Number of sportsbooks that contributed live state for this event at merge time. Live rows only — absent on a terminal row."
          },
          "game_period": {
            "type": "string",
            "description": "Current period. Sport-specific format: `T5` (MLB top 5), `Q3` (NBA Q3), `2H` (soccer 2nd half), `P2` (NHL P2), `S2` (tennis set 2), `FT` (full time)."
          },
          "game_clock": {
            "type": "string",
            "description": "Game clock. Countup format (`49:06`) for soccer; countdown format (`5:42`) for basketball/hockey/football."
          },
          "possession": {
            "type": "string",
            "enum": [
              "home",
              "away"
            ],
            "description": "Which team has possession (team sports) or is receiving (tennis)."
          },
          "server": {
            "type": "string",
            "enum": [
              "home",
              "away"
            ],
            "description": "Tennis only — current server."
          },
          "last_play": {
            "type": "string",
            "description": "Sport-specific last-play description."
          },
          "is_timeout": {
            "type": "boolean",
            "description": "`true` during a timeout."
          },
          "fouls_home": {
            "type": "integer",
            "description": "Team fouls (basketball, soccer)."
          },
          "fouls_away": {
            "type": "integer"
          },
          "corners_home": {
            "type": "integer",
            "description": "Soccer only."
          },
          "corners_away": {
            "type": "integer"
          },
          "yellow_cards_home": {
            "type": "integer",
            "description": "Soccer only."
          },
          "yellow_cards_away": {
            "type": "integer"
          },
          "red_cards_home": {
            "type": "integer",
            "description": "Soccer only."
          },
          "red_cards_away": {
            "type": "integer"
          },
          "power_play": {
            "type": "string",
            "enum": [
              "home",
              "away"
            ],
            "description": "Hockey only — which team is on the power play."
          },
          "sets_home": {
            "type": "integer",
            "description": "Tennis only — sets won."
          },
          "sets_away": {
            "type": "integer"
          },
          "hits_home": {
            "type": "integer",
            "description": "Baseball only."
          },
          "hits_away": {
            "type": "integer"
          },
          "home_pitcher": {
            "type": "string",
            "description": "Baseball only — starting pitcher display name."
          },
          "away_pitcher": {
            "type": "string"
          },
          "wickets_home": {
            "type": "integer",
            "description": "Cricket only — dismissals."
          },
          "wickets_away": {
            "type": "integer"
          },
          "overs": {
            "type": "number",
            "description": "Cricket only."
          },
          "batting_team": {
            "type": "string",
            "enum": [
              "home",
              "away"
            ],
            "description": "Cricket only."
          },
          "stale": {
            "type": "boolean",
            "description": "Only emitted when `true`. Indicates the `primary_book`'s livestate key TTL fell below the aggregator's freshness threshold (~10s) at merge time — scores reflect the last known values; the book has gone quiet. Absence of the field means fresh. On live rows it is written only when `true` (absence = fresh); a terminal row always carries it explicitly as `false`."
          },
          "aggregator_stale": {
            "type": "boolean",
            "description": "Only emitted when `true`. Indicates the SharpAPI aggregator itself hasn't advanced the gen counter for this event's sport in more than ~30s. Broader signal than `stale` — suggests a pipeline hiccup, not a single-book issue."
          },
          "status": {
            "type": "string",
            "enum": [
              "final"
            ],
            "description": "Present only on a carried terminal row, always `final`. Live rows carry no `status` key, so `status == \"final\"` is the reliable terminal test."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "description": "Terminal rows only — when SharpAPI detected the completion (not a league-official final whistle). The 15-minute carry window is measured from this timestamp."
          },
          "score_type": {
            "type": "string",
            "enum": [
              "points",
              "sets"
            ],
            "description": "Terminal rows only — which fields carry the final result. `sets` for the set-scored sports (`tennis`, `table_tennis`, `volleyball`, `badminton`), whose result is in `sets_home`/`sets_away`; `points` everywhere else, whose result is in `home_score`/`away_score`."
          },
          "winner": {
            "type": "string",
            "enum": [
              "home",
              "away",
              "draw"
            ],
            "description": "Terminal rows only, and optional even there — omitted when the recorded result does not settle the match (a level score in a sport that cannot draw, a sub-terminal set count, or a snapshot whose scores never arrived and is recorded as 0-0). A missing `winner` means \"not settled\", not a tie. `draw` is produced only for sports where a level score is a real outcome: `soccer`, `hockey`, `football`, `rugby_union`, `rugby_league`."
          }
        }
      },
      "Odds": {
        "type": "object",
        "required": [
          "id",
          "sportsbook",
          "sportsbook_name",
          "event_id",
          "sport",
          "league",
          "home_team",
          "away_team",
          "market_type",
          "selection",
          "selection_type",
          "odds_american",
          "odds_decimal",
          "probability",
          "event_start_time",
          "is_live",
          "timestamp"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "sportsbook": {
            "type": "string",
            "example": "draftkings"
          },
          "sportsbook_name": {
            "type": "string",
            "example": "DraftKings"
          },
          "event_id": {
            "type": "string"
          },
          "sport": {
            "type": "string",
            "example": "basketball"
          },
          "league": {
            "type": "string",
            "example": "nba"
          },
          "home_team": {
            "type": "string",
            "example": "Boston Celtics"
          },
          "away_team": {
            "type": "string",
            "example": "Los Angeles Lakers"
          },
          "market_type": {
            "type": "string",
            "example": "moneyline"
          },
          "selection": {
            "type": "string",
            "description": "The outcome this price is on — a team name on moneyline/spread markets, `Over`/`Under` on totals. On market types whose name *starts with* `player_` it is always the side (`Over`, `Under`, `Yes` or `No`), never the player; player identity is carried solely by `player_name`. `is_player_prop: true` is wider than that set — it marks every player-level market, including scorer markets (`anytime_goal_scorer`, `anytime_touchdown_scorer`), period- and segment-scoped player markets (`1st_quarter_player_points`) and `most_*_player`. On those rows `selection` is passed through as the book sent it, which normally names the player, and `selection_type` is `other` where the market has no Over/Under axis. Rare prop rows whose side is not one of those four (parity props with `selection_type` `odd`/`even`, and props the upstream book never attributed to a player) pass the book's own label through — read `selection_type` there.",
            "example": "Los Angeles Lakers"
          },
          "selection_type": {
            "type": "string",
            "description": "Canonical side identifier. Two-way markets emit one of the simple values listed in `enum`. Multi-axis markets (soccer double-chance, BTTS-combined, result × O/U, MMA round betting, tennis set betting, correct-score, etc.) emit compound forms joined by `_` — e.g. `home_over`, `away_under`, `home_yes`, `home_r1`. Long-tail markets such as `correct_score` and `set_betting` also encode score-level outcomes directly (e.g. `2_1`). Treat any unrecognised value as opaque rather than erroring. See the `/odds` page for the full table.",
            "enum": [
              "home",
              "away",
              "over",
              "under",
              "yes",
              "no",
              "even",
              "odd",
              "draw",
              "other",
              "home_over",
              "home_under",
              "away_over",
              "away_under",
              "home_yes",
              "home_no",
              "away_yes",
              "away_no",
              "draw_yes",
              "draw_no",
              "home_draw",
              "away_draw",
              "home_away"
            ],
            "example": "away"
          },
          "team_side": {
            "type": "string",
            "description": "Raw team-side hint from the adapter — useful when `selection_type` carries a compound value (e.g. `home_over`) and you want just the team axis without parsing. Absent when the adapter did not stamp it.",
            "enum": [
              "home",
              "away",
              "draw"
            ]
          },
          "odds_american": {
            "type": "number",
            "description": "The price in American format. On Kalshi, Matchbook and ProphetX it is the best price a bettor can take, net of the venue's fee or commission, so it reflects what a bettor actually receives after fees. A market whose fee cannot be determined is published at the price before the fee. On Polymarket it is the best ask net of a flat 5% of winnings on every row: a decimal price `d` is published as `1 + (d - 1) × 0.95`. This is a fixed deduction, not Polymarket's own fee schedule, and `odds_american` is that same net price in American format.",
            "example": 135
          },
          "odds_decimal": {
            "type": "number",
            "description": "The price in decimal format. On Kalshi, Matchbook and ProphetX it is the best price a bettor can take, net of the venue's fee or commission, so it reflects what a bettor actually receives after fees. A market whose fee cannot be determined is published at the price before the fee. On Polymarket it is the best ask net of a flat 5% of winnings on every row: a decimal price `d` is published as `1 + (d - 1) × 0.95`. This is a fixed deduction, not Polymarket's own fee schedule.",
            "example": 2.35
          },
          "probability": {
            "type": "number",
            "description": "Implied probability (0-1)",
            "example": 0.4255
          },
          "line": {
            "type": [
              "number",
              "null"
            ],
            "description": "Point spread or total line (null for moneyline)",
            "example": null
          },
          "is_alternate_line": {
            "type": "boolean",
            "description": "True when this row's `line` differs from the best-effort main line for its cohort. A cohort is (event, market_type, selection axis), resolved per sportsbook: the selection axis is the player for player-prop markets, the team side for spread and team-total markets, and the raw selection everywhere else. False for the main line, for markets with no line (moneyline, outright), and for rows whose cohort has not been published yet. Mutually exclusive with `is_main_line`: both true never coexists, both false is the cohort-pending state."
          },
          "is_main_line": {
            "type": "boolean",
            "description": "True when this row's `line` equals the best-effort main line for its cohort, and for markets with no line (moneyline, outright), which are always main by definition. A cohort is (event, market_type, selection axis), resolved per sportsbook: the selection axis is the player for player-prop markets, the team side for spread and team-total markets, and the raw selection everywhere else. Best-effort, not a uniqueness guarantee: several rows of one (event, market_type) are normally main at once, one per selection axis — Over and Under of a game total are separate cohorts, and so is each outcome of a combination market. False for alternate lines and for cohort-pending rows. Filter with `is_main_line=true` for a main-lines-only view that excludes cohort-pending rows."
          },
          "event_start_time": {
            "type": "string",
            "format": "date-time"
          },
          "is_live": {
            "type": "boolean"
          },
          "is_active": {
            "type": "boolean",
            "description": "true (default) = market open and bettable; false = market suspended/closed with the price frozen. Exposed as a queryable field you can also filter on. Absent is treated as true. An active->suspended transition is emitted on the odds stream (odds:update with is_active=false)."
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 time SharpAPI last refreshed/observed this odd through its pipeline — advances on every ingest cycle. A feed-freshness / liveness signal. NOTE: this is delivery freshness, not a price-last-changed time; SharpAPI does not expose a separate when-the-price-moved field."
          },
          "event_uuid": {
            "type": "string",
            "description": "Stable canonical event UUID from the SharpAPI atlas. Use for cross-feed joins; absent for unmapped events."
          },
          "external_event_id": {
            "type": "string",
            "description": "The sportsbook's own native event ID, when distinct from `event_id`."
          },
          "deep_link": {
            "type": "string",
            "description": "Resolver URL pointing to the sportsbook's event or bet-slip page. Pass `state=` on the request to route through state-specific subdomains for books that need them (BetMGM, Caesars, BetRivers)."
          },
          "market_id": {
            "type": "string",
            "description": "Sportsbook's own native market identifier. Absent when the book does not expose one."
          },
          "selection_id": {
            "type": "string",
            "description": "Sportsbook's own native selection/outcome identifier. Absent when the book does not expose one."
          },
          "volume": {
            "type": "number",
            "description": "Matched volume on this selection. Exchange books only (Betfair, Novig, ProphetX, Polymarket). Units follow the originating book."
          },
          "volume_24h": {
            "type": "number",
            "description": "Trailing 24-hour matched volume on this selection. Exchange books only."
          },
          "open_interest": {
            "type": "number",
            "description": "Outstanding open interest on this selection. Exchange books only."
          },
          "max_bet": {
            "type": "number",
            "description": "Largest size available on this row. On a traditional sportsbook (Pinnacle, Circa Sports, SBOBET) the maximum wager the book will accept, in USD. On an exchange-style book the money resting at the best price, denominated in `size_currency`. On Kalshi, Matchbook, ProphetX and Polymarket that is the money resting at the venue's own price, before the fee or deduction that `odds_american` and `odds_decimal` already take out. Absent rather than 0.0 when no size is published; read absence as \"not published\", never as zero. `has_limits` on /sportsbooks is the live per-book answer to whether a book publishes this field."
          },
          "size_currency": {
            "type": "string",
            "description": "ISO-4217 code the money figures on this row are denominated in: `max_bet`, `best_bid_liquidity` and `total_liquidity`. GBP on Betfair and Smarkets, USD on every other book that publishes a size. Never converted - the venue's own figure is passed through in the venue's own currency. Absent on rows carrying no size, and on traditional sportsbooks, whose `max_bet` is a USD wager cap rather than a resting size."
          },
          "best_bid_liquidity": {
            "type": "number",
            "description": "Money resting at the published price - what a taker can match without moving the price. Exchange books only, denominated in `size_currency`. 0.0 means nothing is resting there; absence means the venue does not publish the figure."
          },
          "total_liquidity": {
            "type": "number",
            "description": "Risk-able size summed across the whole book for this selection, not just the best price. Exchange books only, denominated in `size_currency`. 0.0 means an empty book; absence means the venue does not publish the figure. Not derivable from `best_bid_liquidity`: some books publish one without the other."
          },
          "player_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Player name (player prop markets only). The only field carrying player identity — `selection` holds the side. The canonical parse for a prop row is `player_name` + `selection_type` + `line`."
          },
          "stat_category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Stat category (player prop markets only)"
          },
          "home_pitcher": {
            "type": "string",
            "description": "MLB only. Home-team starting pitcher when published by the book."
          },
          "away_pitcher": {
            "type": "string",
            "description": "MLB only. Away-team starting pitcher when published by the book."
          },
          "polymarket_resolution": {
            "type": "string",
            "description": "Polymarket only. UMA optimistic-oracle resolution status when the market has terminated. Absent for live (unresolved) markets and for non-Polymarket books.",
            "enum": [
              "settled_normal",
              "voided",
              "disputed",
              "proposed",
              "unknown"
            ]
          }
        }
      },
      "BatchOddsRequest": {
        "type": "object",
        "required": [
          "event_ids"
        ],
        "properties": {
          "event_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "description": "Array of event IDs to fetch odds for"
          },
          "sportsbook": {
            "type": "string",
            "description": "Optionally filter by sportsbook"
          },
          "market": {
            "type": "string",
            "description": "Optionally filter by market type"
          },
          "include_futures": {
            "type": "boolean",
            "default": false,
            "description": "Include futures markets in results. Defaults to false."
          }
        }
      },
      "EVOpportunity": {
        "type": "object",
        "required": [
          "id",
          "game_id",
          "ev_percentage",
          "odds_american",
          "odds_decimal",
          "no_vig_odds",
          "selection",
          "market",
          "line",
          "sportsbook",
          "game",
          "sport",
          "league",
          "home_team",
          "away_team",
          "start_time",
          "is_live",
          "market_width",
          "fair_probability",
          "devig_method",
          "sharp_book",
          "external_event_id",
          "selection_id",
          "detected_at",
          "confidence_score",
          "kelly_percent",
          "book_count",
          "arb_available",
          "arb_profit",
          "is_player_prop",
          "player_name",
          "player_id",
          "stat_category",
          "possibly_stale",
          "oldest_odds_age_seconds",
          "warnings"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "16-character hex opportunity ID",
            "example": "a1b2c3d4e5f67890"
          },
          "game_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "ev_percentage": {
            "type": "number",
            "description": "Expected value as a percentage (e.g., 4.2 = 4.2% EV)",
            "example": 4.2
          },
          "odds_american": {
            "type": "number",
            "example": 135
          },
          "odds_decimal": {
            "type": "number",
            "example": 2.35
          },
          "no_vig_odds": {
            "type": [
              "number",
              "null"
            ],
            "description": "Fair odds with vig removed",
            "example": 125
          },
          "selection": {
            "type": "string",
            "example": "Los Angeles Lakers"
          },
          "market": {
            "type": "string",
            "example": "moneyline"
          },
          "line": {
            "type": [
              "number",
              "null"
            ]
          },
          "is_alternate_line": {
            "type": "boolean",
            "description": "True when this row's `line` differs from the best-effort main line for its cohort. A cohort is (event, market_type, selection axis), resolved per sportsbook: the selection axis is the player for player-prop markets, the team side for spread and team-total markets, and the raw selection everywhere else. Always false for markets with no line (moneyline, outright). The live endpoint always emits this field; it was absent from this schema until spec 3.9.0."
          },
          "sportsbook": {
            "type": "string",
            "description": "Sportsbook offering these odds. Multiple entries may exist for the same selection if several books are +EV.",
            "example": "draftkings"
          },
          "game": {
            "type": "string",
            "example": "Los Angeles Lakers @ Boston Celtics"
          },
          "sport": {
            "type": "string",
            "example": "basketball"
          },
          "league": {
            "type": "string",
            "example": "nba"
          },
          "home_team": {
            "type": [
              "string",
              "null"
            ]
          },
          "away_team": {
            "type": [
              "string",
              "null"
            ]
          },
          "start_time": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "is_live": {
            "type": "boolean"
          },
          "market_width": {
            "type": [
              "number",
              "null"
            ],
            "description": "Spread between best and worst odds across books"
          },
          "fair_probability": {
            "type": [
              "number",
              "null"
            ],
            "description": "No-vig (devigged) fair probability (0.0 to 1.0)"
          },
          "devig_method": {
            "type": "string",
            "description": "Method used to remove vig",
            "example": "multiplicative"
          },
          "sharp_book": {
            "type": "string",
            "description": "Sharp book used as reference (e.g., pinnacle)",
            "example": "pinnacle"
          },
          "external_event_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sportsbook-native event ID for deep linking"
          },
          "selection_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sportsbook outcome/selection ID for bet-level deep links"
          },
          "detected_at": {
            "type": "string",
            "format": "date-time"
          },
          "confidence_score": {
            "type": "number",
            "description": "Multi-factor confidence score (0-100)",
            "example": 78
          },
          "kelly_percent": {
            "type": [
              "number",
              "null"
            ],
            "description": "Optimal bet fraction (0.0-1.0) from Kelly criterion"
          },
          "book_count": {
            "type": "integer",
            "description": "Number of books offering this market"
          },
          "arb_available": {
            "type": "boolean",
            "description": "Whether an arbitrage opportunity exists for this market"
          },
          "arb_profit": {
            "type": [
              "number",
              "null"
            ],
            "description": "Arbitrage profit percentage if available"
          },
          "is_player_prop": {
            "type": "boolean"
          },
          "player_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "player_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Canonical cross-book player identifier resolved by atlas_players (e.g. \"baseball_mlb_corbin_carroll\"). null when the producer has not resolved a canonical id for this player — group on `player_id` and fall back to `player_name`. Player-prop markets only."
          },
          "stat_category": {
            "type": [
              "string",
              "null"
            ]
          },
          "possibly_stale": {
            "type": "boolean",
            "description": "Whether the odds may be stale"
          },
          "oldest_odds_age_seconds": {
            "type": [
              "number",
              "null"
            ],
            "description": "Age of the oldest odds used in this calculation"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Data-quality warning flags. The EV engine emits exactly four: LIVE_STALE_ODDS (live, and the oldest odds used are more than 30s old), STALE_PREMATCH_ODDS (prematch, and the bet-side price has not moved in more than 60 minutes while the feed is still alive; the row is served with possibly_stale: true), SINGLE_SHARP_REF (only one sharp book priced the market, so the fair probability is uncorroborated), SINGLE_SHARP_PERIOD (the single-sharp subset that is also a period/derivative sub-slice). The key is always present and never null (it is listed in required); it is an empty array [] when none apply. LOW_IMPLIED_TOTAL and POTENTIALLY_STALE_ODDS are arbitrage-only flags and are never emitted here — see ArbitrageOpportunity.warnings."
          },
          "quality_tier": {
            "type": "string",
            "enum": [
              "A",
              "B",
              "C"
            ],
            "description": "Grader-certified quality label. A = ev_percentage >= 4 (>= 6 for player props), Pinnacle-anchored (sharp_book = pinnacle), prematch (is_live = false), and the (book, sport, market) segment is unflagged by the nightly reliability model — the only class with consistently positive graded ROI. B = ev_percentage >= 2 (>= 4 props) and the segment is not exclusion-flagged; includes live, non-Pinnacle-anchored, and watch-flagged rows that clear the band. C = everything else, plus any exclusion-flagged segment regardless of EV (disclosed as market intelligence, not a pick). A high ev_percentage alone does not imply A — the sharp anchor, prematch/live state, and the segment-reliability model also gate it. Omitted on suspended rows (is_suspended).",
            "example": "A"
          }
        }
      },
      "ArbitrageOpportunity": {
        "type": "object",
        "required": [
          "id",
          "event_id",
          "event_name",
          "sport",
          "league",
          "market_type",
          "line",
          "profit_percent",
          "implied_total",
          "start_time",
          "is_live",
          "game_state",
          "is_alternate_line",
          "possibly_stale",
          "oldest_odds_age_seconds",
          "warnings",
          "ev_available",
          "ev_percentage",
          "is_player_prop",
          "player_name",
          "stat_category",
          "estimated_net_profit_percent",
          "legs",
          "detected_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "b2c3d4e5f6789012"
          },
          "event_id": {
            "type": "string"
          },
          "event_name": {
            "type": "string",
            "example": "Los Angeles Lakers @ Boston Celtics"
          },
          "sport": {
            "type": "string"
          },
          "league": {
            "type": "string"
          },
          "market_type": {
            "type": "string",
            "example": "moneyline"
          },
          "line": {
            "type": [
              "number",
              "null"
            ]
          },
          "profit_percent": {
            "type": "number",
            "description": "Guaranteed profit percentage before costs",
            "example": 1.8
          },
          "implied_total": {
            "type": "number",
            "description": "Sum of implied probabilities (below 1.0 = arbitrage)",
            "example": 0.982
          },
          "start_time": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "is_live": {
            "type": "boolean"
          },
          "game_state": {
            "$ref": "#/components/schemas/OpportunityGameState"
          },
          "is_alternate_line": {
            "type": "boolean"
          },
          "possibly_stale": {
            "type": "boolean"
          },
          "oldest_odds_age_seconds": {
            "type": [
              "number",
              "null"
            ]
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Warning flags. Emitted: LIVE_GAME, LIVE_HIGH_PROFIT_SUSPICIOUS, LIVE_STALE_ODDS, POTENTIALLY_STALE_ODDS. Reserved (defined in the producer but not currently emitted): HIGH_PROFIT_SUSPICIOUS, VERY_STALE_ODDS, LOW_IMPLIED_TOTAL. The key is always present and never null (it is listed in required); it is an empty array [] when none apply."
          },
          "ev_available": {
            "type": "boolean",
            "description": "Whether a +EV opportunity also exists for this market"
          },
          "ev_percentage": {
            "type": [
              "number",
              "null"
            ]
          },
          "is_player_prop": {
            "type": "boolean"
          },
          "player_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "stat_category": {
            "type": [
              "string",
              "null"
            ]
          },
          "estimated_net_profit_percent": {
            "type": "number",
            "description": "Profit percentage after estimated transaction costs"
          },
          "legs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ArbitrageLeg"
            }
          },
          "detected_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ArbitrageLeg": {
        "type": "object",
        "required": [
          "sportsbook",
          "selection",
          "odds_american",
          "odds_decimal",
          "implied_probability",
          "stake_percent",
          "timestamp",
          "external_event_id",
          "selection_id",
          "market_id"
        ],
        "properties": {
          "sportsbook": {
            "type": "string",
            "example": "draftkings"
          },
          "selection": {
            "type": "string",
            "example": "Los Angeles Lakers"
          },
          "odds_american": {
            "type": "number",
            "example": 135
          },
          "odds_decimal": {
            "type": "number",
            "example": 2.35
          },
          "implied_probability": {
            "type": "number",
            "example": 0.4255
          },
          "stake_percent": {
            "type": "number",
            "description": "Recommended stake as percentage of total bankroll",
            "example": 42.55
          },
          "timestamp": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "external_event_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "selection_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "market_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "OpportunityGameState": {
        "type": [
          "object",
          "null"
        ],
        "description": "Simplified game state for opportunity endpoints",
        "properties": {
          "period": {
            "type": [
              "string",
              "null"
            ]
          },
          "clock": {
            "type": [
              "string",
              "null"
            ]
          },
          "score_home": {
            "type": [
              "integer",
              "null"
            ]
          },
          "score_away": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "MiddleOpportunity": {
        "type": "object",
        "required": [
          "id",
          "event_id",
          "event_name",
          "sport",
          "league",
          "market_type",
          "home_team",
          "away_team",
          "start_time",
          "side1",
          "side2",
          "middle_size",
          "middle_numbers",
          "middle_probability",
          "expected_value",
          "roi_percentage",
          "worst_case_loss",
          "best_case_profit",
          "break_even_percent",
          "is_live",
          "is_player_prop",
          "player_name",
          "stat_category",
          "odds_age_seconds",
          "is_guaranteed_profit",
          "guaranteed_roi",
          "key_numbers",
          "key_number_probability",
          "quality_score",
          "market_overround",
          "warnings",
          "is_team_total",
          "team_name",
          "detected_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "event_id": {
            "type": "string"
          },
          "event_name": {
            "type": "string"
          },
          "sport": {
            "type": "string"
          },
          "league": {
            "type": "string"
          },
          "market_type": {
            "type": "string"
          },
          "home_team": {
            "type": "string"
          },
          "away_team": {
            "type": "string"
          },
          "start_time": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "side1": {
            "$ref": "#/components/schemas/MiddleSide"
          },
          "side2": {
            "$ref": "#/components/schemas/MiddleSide"
          },
          "middle_size": {
            "type": "number",
            "description": "Gap between the two lines",
            "example": 2
          },
          "middle_numbers": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "The numbers that fall in the middle"
          },
          "middle_probability": {
            "type": "number",
            "description": "Probability of landing in the middle"
          },
          "expected_value": {
            "type": "number"
          },
          "roi_percentage": {
            "type": "number"
          },
          "worst_case_loss": {
            "type": "number",
            "description": "Maximum loss if the middle doesn't hit"
          },
          "best_case_profit": {
            "type": "number",
            "description": "Profit if the result lands in the middle"
          },
          "break_even_percent": {
            "type": "number"
          },
          "is_live": {
            "type": "boolean"
          },
          "is_player_prop": {
            "type": "boolean"
          },
          "player_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "stat_category": {
            "type": [
              "string",
              "null"
            ]
          },
          "odds_age_seconds": {
            "type": [
              "number",
              "null"
            ]
          },
          "is_guaranteed_profit": {
            "type": "boolean",
            "description": "Whether this middle guarantees profit regardless of outcome"
          },
          "guaranteed_roi": {
            "type": [
              "number",
              "null"
            ]
          },
          "key_numbers": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "Key numbers in this sport (e.g., 3 and 7 in NFL)"
          },
          "key_number_probability": {
            "type": "number"
          },
          "quality_score": {
            "type": "number",
            "description": "Overall quality score combining multiple factors"
          },
          "market_overround": {
            "type": "number",
            "description": "Combined implied probability of both sides"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "is_team_total": {
            "type": "boolean"
          },
          "team_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "alt_side1": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "book": {
                  "type": "string"
                },
                "odds": {
                  "type": "number"
                }
              }
            },
            "description": "Alternative books offering side 1"
          },
          "alt_side2": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "book": {
                  "type": "string"
                },
                "odds": {
                  "type": "number"
                }
              }
            },
            "description": "Alternative books offering side 2"
          },
          "detected_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "MiddleSide": {
        "type": "object",
        "required": [
          "book",
          "selection",
          "line",
          "odds",
          "stake_percent",
          "odds_age_seconds",
          "deep_link"
        ],
        "properties": {
          "book": {
            "type": "string",
            "example": "draftkings"
          },
          "selection": {
            "type": "string",
            "example": "Over 220.5"
          },
          "line": {
            "type": "number",
            "example": 220.5
          },
          "odds": {
            "type": "object",
            "required": [
              "american",
              "decimal",
              "probability",
              "fair_probability"
            ],
            "properties": {
              "american": {
                "type": "number",
                "example": -110
              },
              "decimal": {
                "type": "number",
                "example": 1.909
              },
              "probability": {
                "type": "number",
                "example": 0.5238
              },
              "fair_probability": {
                "type": "number",
                "example": 0.5
              }
            }
          },
          "stake_percent": {
            "type": "number",
            "example": 50
          },
          "odds_age_seconds": {
            "type": [
              "number",
              "null"
            ]
          },
          "deep_link": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "LowHoldOpportunity": {
        "type": "object",
        "required": [
          "id",
          "event_id",
          "event_name",
          "sport",
          "league",
          "market_type",
          "line",
          "home_team",
          "away_team",
          "start_time",
          "hold_percentage",
          "side1",
          "side2",
          "side3",
          "is_live",
          "game_state",
          "is_alternate_line",
          "all_books",
          "is_player_prop",
          "player_name",
          "stat_category",
          "confidence",
          "odds_age_seconds",
          "possibly_stale",
          "detected_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "event_id": {
            "type": "string"
          },
          "event_name": {
            "type": "string"
          },
          "sport": {
            "type": "string"
          },
          "league": {
            "type": "string"
          },
          "market_type": {
            "type": "string"
          },
          "line": {
            "type": [
              "number",
              "null"
            ]
          },
          "home_team": {
            "type": "string"
          },
          "away_team": {
            "type": "string"
          },
          "start_time": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "hold_percentage": {
            "type": "number",
            "description": "Bookmaker hold (vig) percentage. Lower is better for bettors.",
            "example": 1.2
          },
          "side1": {
            "$ref": "#/components/schemas/LowHoldSide"
          },
          "side2": {
            "$ref": "#/components/schemas/LowHoldSide"
          },
          "side3": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/LowHoldSide"
              },
              {
                "type": "null"
              }
            ],
            "description": "Third side (3-way markets like soccer moneyline with draw)"
          },
          "is_live": {
            "type": "boolean"
          },
          "game_state": {
            "$ref": "#/components/schemas/OpportunityGameState"
          },
          "is_alternate_line": {
            "type": "boolean"
          },
          "all_books": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "All sportsbooks contributing to this opportunity"
          },
          "is_player_prop": {
            "type": "boolean"
          },
          "player_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "stat_category": {
            "type": [
              "string",
              "null"
            ]
          },
          "confidence": {
            "type": "number",
            "description": "Confidence score (0-100)"
          },
          "odds_age_seconds": {
            "type": "number",
            "description": "Age of the odds in seconds"
          },
          "possibly_stale": {
            "type": "boolean"
          },
          "detected_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LowHoldSide": {
        "type": "object",
        "required": [
          "selection",
          "books",
          "line",
          "odds",
          "deep_links"
        ],
        "properties": {
          "selection": {
            "type": "string",
            "example": "Los Angeles Lakers"
          },
          "books": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Sportsbooks offering the best odds on this side"
          },
          "line": {
            "type": [
              "number",
              "null"
            ]
          },
          "odds": {
            "type": "object",
            "required": [
              "american",
              "decimal",
              "implied_probability",
              "fair_probability"
            ],
            "properties": {
              "american": {
                "type": "number",
                "example": -108
              },
              "decimal": {
                "type": "number",
                "example": 1.926
              },
              "implied_probability": {
                "type": "number",
                "example": 0.5192
              },
              "fair_probability": {
                "type": "number",
                "example": 0.5
              }
            }
          },
          "deep_links": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            },
            "description": "Deep link URLs keyed by sportsbook"
          }
        }
      },
      "Account": {
        "type": "object",
        "description": "Flat account shape returned by `GET /api/v1/account`. All fields are always present.",
        "required": [
          "key_id",
          "tier",
          "rate_limit",
          "features",
          "streaming"
        ],
        "properties": {
          "key_id": {
            "type": "string",
            "description": "Identifier for the API key used in this request."
          },
          "tier": {
            "type": "string",
            "description": "Subscription tier.",
            "enum": [
              "free",
              "hobby",
              "pro",
              "sharp",
              "enterprise"
            ]
          },
          "rate_limit": {
            "type": "object",
            "required": [
              "requests_per_minute",
              "max_books"
            ],
            "properties": {
              "requests_per_minute": {
                "type": "integer",
                "description": "Maximum API requests allowed per minute on this tier."
              },
              "max_books": {
                "type": "integer",
                "description": "Maximum sportsbooks accessible on this tier (999 = unlimited)."
              }
            }
          },
          "features": {
            "type": "array",
            "description": "Capabilities enabled on this tier.",
            "items": {
              "type": "string",
              "enum": [
                "odds",
                "schedule",
                "ev",
                "arbitrage",
                "middles",
                "low_hold",
                "history",
                "futures",
                "closing_line",
                "splits",
                "injuries"
              ]
            }
          },
          "streaming": {
            "type": "object",
            "required": [
              "enabled",
              "max_connections"
            ],
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "Whether SSE/WebSocket streaming is available (via tier, add-on, or trial)."
              },
              "max_connections": {
                "type": "integer",
                "description": "Maximum concurrent SSE/WebSocket connections (0 if streaming not enabled)."
              }
            }
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "requests",
          "streams",
          "period"
        ],
        "properties": {
          "requests": {
            "type": "object",
            "required": [
              "today",
              "this_week",
              "this_month",
              "limit"
            ],
            "properties": {
              "today": {
                "type": "integer"
              },
              "this_week": {
                "type": "integer"
              },
              "this_month": {
                "type": "integer"
              },
              "limit": {
                "type": "object",
                "required": [
                  "per_minute",
                  "remaining",
                  "resets_at"
                ],
                "properties": {
                  "per_minute": {
                    "type": "integer"
                  },
                  "remaining": {
                    "type": "integer"
                  },
                  "resets_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          },
          "streams": {
            "type": "object",
            "required": [
              "active",
              "max"
            ],
            "properties": {
              "active": {
                "type": "integer"
              },
              "max": {
                "type": "integer"
              }
            }
          },
          "period": {
            "type": "object",
            "required": [
              "start",
              "end"
            ],
            "properties": {
              "start": {
                "type": "string",
                "format": "date-time"
              },
              "end": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "required": [
          "id",
          "id_masked",
          "name",
          "tier",
          "is_active",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "id_masked": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "tier": {
            "type": "string"
          },
          "is_active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FutureMarket": {
        "type": "object",
        "required": [
          "market_id",
          "display_name",
          "sport",
          "league",
          "future_type",
          "selection_count"
        ],
        "properties": {
          "market_id": {
            "type": "string",
            "example": "championship_winner"
          },
          "display_name": {
            "type": "string",
            "example": "NBA Championship Winner"
          },
          "sport": {
            "type": "string",
            "example": "basketball"
          },
          "league": {
            "type": "string",
            "example": "nba"
          },
          "future_type": {
            "type": "string",
            "enum": [
              "TEAM",
              "PLAYER",
              "OVER_UNDER",
              "YES_NO"
            ]
          },
          "selection_count": {
            "type": "integer",
            "description": "Number of available selections"
          },
          "books_available": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Sportsbooks offering this futures market"
          }
        }
      },
      "FutureOdds": {
        "type": "object",
        "required": [
          "selection",
          "sportsbook",
          "odds_american",
          "odds_decimal",
          "probability",
          "future_type",
          "future_market_id",
          "future_market_name",
          "sport",
          "league",
          "event_start_time",
          "timestamp"
        ],
        "properties": {
          "selection": {
            "type": "string",
            "example": "Boston Celtics"
          },
          "sportsbook": {
            "type": "string",
            "example": "draftkings"
          },
          "odds_american": {
            "type": "number",
            "example": 350
          },
          "odds_decimal": {
            "type": "number",
            "example": 4.5
          },
          "probability": {
            "type": "number",
            "example": 0.2222
          },
          "line": {
            "type": [
              "number",
              "null"
            ]
          },
          "future_type": {
            "type": "string"
          },
          "future_market_id": {
            "type": "string"
          },
          "future_market_name": {
            "type": "string"
          },
          "sport": {
            "type": "string"
          },
          "league": {
            "type": "string"
          },
          "event_start_time": {
            "type": "string",
            "format": "date-time"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SplitsGame": {
        "type": "object",
        "description": "Public betting splits for a single event from one source sportsbook.",
        "required": [
          "event_id",
          "sport",
          "league",
          "sportsbook"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Canonical event ID (joinable with /odds)"
          },
          "sport": {
            "type": "string"
          },
          "league": {
            "type": "string"
          },
          "sportsbook": {
            "type": "string",
            "enum": [
              "draftkings",
              "circa",
              "betmgm",
              "consensus"
            ],
            "description": "Source sportsbook for the splits data. `consensus` is a synthetic label, not a sportsbook: when every source covering an event posts identical numbers those rows are merged into one row tagged `consensus`, so a `sportsbook=draftkings` filter does not return them. It is accepted by this endpoint's `sportsbook` filter but never appears in /api/v1/sportsbooks."
          },
          "away_team": {
            "type": "string"
          },
          "home_team": {
            "type": "string"
          },
          "spread": {
            "type": "object",
            "description": "Spread market splits. NOTE: `away_odds` and `home_odds` here carry spread *line* values (e.g. -1.5/+1.5), not American odds. Field naming is a known inconsistency vs `/splits/history` which uses `away_line`/`home_line`.",
            "properties": {
              "away_odds": {
                "type": "number"
              },
              "home_odds": {
                "type": "number"
              },
              "handle_pct": {
                "type": "object",
                "description": "Share of money wagered on each side (0.0-1.0). Both members are `null` on `betmgm` rows: BetMGM publishes a ticket percentage only, so no handle figure exists to report. The keys are always present - only their values are null.",
                "properties": {
                  "away": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  },
                  "home": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  }
                }
              },
              "bets_pct": {
                "type": "object",
                "description": "Share of tickets wagered on each side (0.0-1.0). Members are `null` when the source has posted no percentage for this market; observed on `betmgm` rows, whose feed is intermittent. The keys are always present - only their values are null.",
                "properties": {
                  "away": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  },
                  "home": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  }
                }
              }
            }
          },
          "total": {
            "type": "object",
            "properties": {
              "line": {
                "type": "number"
              },
              "handle_pct": {
                "type": "object",
                "description": "Share of money wagered on each side (0.0-1.0). Both members are `null` on `betmgm` rows: BetMGM publishes a ticket percentage only, so no handle figure exists to report. The keys are always present - only their values are null.",
                "properties": {
                  "over": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  },
                  "under": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  }
                }
              },
              "bets_pct": {
                "type": "object",
                "description": "Share of tickets wagered on each side (0.0-1.0). Members are `null` when the source has posted no percentage for this market; observed on `betmgm` rows, whose feed is intermittent. The keys are always present - only their values are null.",
                "properties": {
                  "over": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  },
                  "under": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  }
                }
              }
            }
          },
          "moneyline": {
            "type": "object",
            "properties": {
              "away_odds": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "American odds. `null` when the source has pulled the moneyline for this event while still publishing its split percentages; the key stays present."
              },
              "home_odds": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "American odds. `null` when the source has pulled the moneyline for this event while still publishing its split percentages; the key stays present."
              },
              "handle_pct": {
                "type": "object",
                "description": "Share of money wagered on each side (0.0-1.0). Both members are `null` on `betmgm` rows: BetMGM publishes a ticket percentage only, so no handle figure exists to report. The keys are always present - only their values are null.",
                "properties": {
                  "away": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  },
                  "home": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  }
                }
              },
              "bets_pct": {
                "type": "object",
                "description": "Share of tickets wagered on each side (0.0-1.0). Members are `null` when the source has posted no percentage for this market; observed on `betmgm` rows, whose feed is intermittent. The keys are always present - only their values are null.",
                "properties": {
                  "away": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  },
                  "home": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 1
                  }
                }
              }
            }
          },
          "fetched_at": {
            "type": "string",
            "format": "date-time"
          },
          "available_metrics": {
            "type": "array",
            "description": "Which public-betting split metrics this row's sportsbook publishes AT ALL. This is a per-book declaration, not a description of the values this particular row happens to hold, and it is serialised in the order shown. Omitted entirely when SharpAPI has not declared what the book publishes: an absent field means 'not stated', never 'publishes nothing'. Read it together with the value - `handle_pct` listed while its members are `null` is a number we are missing, whereas `handle_pct` absent from this list while its members are `null` means the book does not publish money share. A synthetic `consensus` row carries the intersection of its contributing books, and omits the field if any contributor is undeclared.",
            "items": {
              "type": "string",
              "enum": [
                "bets_pct",
                "handle_pct"
              ]
            }
          }
        }
      },
      "SplitsHistoryEntry": {
        "type": "object",
        "description": "A single timestamped splits snapshot for an event from one source.",
        "required": [
          "book",
          "ts",
          "timestamp"
        ],
        "properties": {
          "book": {
            "type": "string",
            "enum": [
              "draftkings",
              "circa",
              "betmgm"
            ],
            "description": "Source sportsbook; history uses book where /splits uses sportsbook. No synthetic consensus history is produced."
          },
          "ts": {
            "type": "string",
            "format": "date-time",
            "description": "Snapshot timestamp (RFC 3339)."
          },
          "timestamp": {
            "type": "number",
            "description": "Snapshot timestamp (Unix seconds, fractional)."
          },
          "spread": {
            "type": "object",
            "properties": {
              "away_line": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Spread line for this side, in the book's own sign convention (e.g. `-1.5`). `null` when the source posted no spread for this event at snapshot time; the key may be absent on BetMGM samples."
              },
              "home_line": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Spread line for this side, in the book's own sign convention (e.g. `-1.5`). `null` when the source posted no spread for this event at snapshot time; the key may be absent on BetMGM samples."
              },
              "handle_pct": {
                "type": "object",
                "properties": {
                  "away": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "home": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              },
              "bets_pct": {
                "type": "object",
                "properties": {
                  "away": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "home": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              }
            }
          },
          "total": {
            "type": "object",
            "properties": {
              "line": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Total (over/under) line. `null` when the source posted no total for this event at snapshot time; the key may be absent on BetMGM samples."
              },
              "handle_pct": {
                "type": "object",
                "properties": {
                  "over": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "under": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              },
              "bets_pct": {
                "type": "object",
                "properties": {
                  "over": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "under": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              }
            }
          },
          "moneyline": {
            "type": "object",
            "properties": {
              "away_odds": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "American odds. `null` when the source has pulled the moneyline for this event while still publishing its split percentages; the key may be absent on BetMGM samples."
              },
              "home_odds": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "American odds. `null` when the source has pulled the moneyline for this event while still publishing its split percentages; the key may be absent on BetMGM samples."
              },
              "handle_pct": {
                "type": "object",
                "properties": {
                  "away": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "home": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              },
              "bets_pct": {
                "type": "object",
                "properties": {
                  "away": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "home": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              }
            }
          },
          "available_metrics": {
            "type": "array",
            "description": "Metrics the source book publishes at all. BetMGM declares bets_pct only; DraftKings and Circa declare bets_pct and handle_pct. Omitted for undeclared books.",
            "items": {
              "type": "string",
              "enum": [
                "bets_pct",
                "handle_pct"
              ]
            }
          }
        }
      },
      "DataEnvelope": {
        "type": "object",
        "description": "Standard shape for non-paginated responses. Emitted by endpoints that return a single resource or a small fixed list. No `success` flag — HTTP status codes indicate success/failure.",
        "required": [
          "data",
          "updated_at"
        ],
        "properties": {
          "data": {},
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "Server emit time (RFC 3339)."
          }
        }
      }
    }
  }
}
