# SportWizzard API — Full Documentation > The sports betting data API — real-time odds from 35+ books, players, teams, box-score stats, historical prices, and game context (weather, lineups, probable pitchers) in one canonical schema. Build your own models on it. SportWizzard is a self-hosted sports betting intelligence platform exposed as a REST API. It ingests real-time odds from 35+ sportsbooks and DFS platforms and normalizes everything into a single canonical schema — events, odds, players, teams, box-score stats, historical prices, and per-game context (weather, lineups, probable pitchers, venue, scores). Pull the entire live board as one gzip snapshot, or query any slice to train and run your own models. DFS edge and cross-book arbitrage signals are computed on top of the same data and returned as a convenience. - Base URL: `https://api.sportwizzard.com` - All endpoints are under `/api/v1` and return JSON. - OpenAPI 3 spec: `https://sportwizzard.com/openapi.json` - Interactive reference: `https://sportwizzard.com/developers/reference` - Agent quickstart (curl + MCP setup): `https://sportwizzard.com/developers/agents` - Agent-facing project guide: `https://sportwizzard.com/AGENTS.md` Covered leagues span MLB, NBA, WNBA, NFL, NHL, MLS, PGA Tour, international soccer (FIFA World Cup, Brazil, and more), and esports/combat sports (CS2, League of Legends, Dota 2, MMA, tennis). Coverage is seasonal — leagues rotate in and out as their seasons start/end. **`GET /api/v1/leagues` is the source of truth for what's live right now**; don't hardcode a league list. Sources include PrizePicks, Underdog, Sleeper, DraftKings, FanDuel, BetMGM, Pinnacle, Caesars, BetRivers, Fanatics, Kalshi, Polymarket, and 20+ others. --- ## Authentication Send your API key in the `X-Api-Key` HTTP header. API keys are issued through Clerk and managed in your SportWizzard account under the **API Keys** tab (`https://sportwizzard.com/account`). ``` curl "https://api.sportwizzard.com/api/v1/events?league=mlb" \ -H "X-Api-Key: sw_live_your_api_key_here" ``` `Authorization: Bearer ` is also accepted (the convention MCP clients and most agent tooling default to) — use whichever header is more convenient. Reference endpoints (`/leagues`, `/seasons`, `/sportsbooks`, `/teams`, `/players`, `/markets`, `/status`, `/events`) are free, uncounted, and do not require a key. Data endpoints (`/odds`, stats, box scores, the snapshot, per-event/player/team odds) and the derived `/edges` and `/arbitrage` signals require a key. Historical endpoints require a paid plan and cost 5 credits per event; the snapshot requires Growth or Enterprise and costs 1 credit per pull. **Current state (pre-billing):** billing enforcement has not yet flipped on, so all data endpoints are currently reachable with no key at all (an anonymous caller gets the Enterprise-tier rate limit, unmetered). This is a transitional state, not a permanent guarantee — always send a key if you have one, and don't build automation that assumes anonymous access stays open. --- ## Response envelope Every response uses the same envelope: ```json { "success": true, "data": [ /* array or object */ ], "nextCursor": "opaque-cursor-string-or-null", "meta": { "count": 50, "updated": "2026-07-09T16:41:27Z" } } ``` - `success` — boolean. - `data` — the result: an array for list endpoints, a single object for get-by-id endpoints. - `nextCursor` — present on list endpoints; opaque pagination token, or `null` at the end. - `meta.count` — number of items in `data`. - `meta.updated` — ISO-8601 timestamp of the underlying data. The one exception is `/api/v1/snapshot`, which returns the raw gzip snapshot body (not the envelope) so it can be served as a single pre-built blob. ## Pagination List endpoints accept `limit` (page size) and `cursor`. To page: 1. Call the endpoint with an optional `limit`. 2. Read `nextCursor` from the response. 3. Call again with `?cursor=`. 4. Stop when `nextCursor` is `null`. Cursors are opaque — do not parse or construct them. ## Freshness & caching Live-odds responses include an `ETag` header equal to the current snapshot build hash. Send it back with `If-None-Match` to receive a cheap `304 Not Modified` when nothing has changed. For efficient syncing, poll the delta feed `GET /api/v1/odds/updated?since={ISO-8601 timestamp}` to fetch only rows changed since your last sync — it accepts the same `league`, `sportsbook`, and `market` filters as `/odds`, so you can scope the delta to just the slice you track — or pull the whole board once via `/api/v1/snapshot` and diff locally. Live odds carry short cache TTLs; reference data carries long TTLs. ## Odds data model Understand this section and the rest of the API falls into place. ### Events are the spine Everything hangs off an **event** — a single game or match. An event carries `homeTeamId`/`homeTeamName`, `awayTeamId`/`awayTeamName`, `startTime`, `league`, `status`, and a `hasOdds` flag. Odds, box-score stats, and per-game context are all keyed by `eventId`. The normal path is: list events (`GET /api/v1/events?league=mlb`), take an `id`, then pull that event's board (`GET /api/v1/events/{id}/odds`). ### Odds are a flat array — one row per (sportsbook × market × selection) The odds feed is **not nested**. It's a flat list where every row is one bettable line from one book, and each row is fully self-describing — it repeats its `eventId`, `sportsbook`, `market`, `selection`, `side`, `line`, and price, so you never have to walk a tree to know what a row means. The same shape describes a baseball run line, an NBA player-points prop, a soccer 3-way moneyline, and a golf outright. That makes odds trivial to filter, join, and load into a table or dataframe: group by `eventId` to reconstruct a board, group by `(market, marketSubtype, line, side)` across `sportsbook` to line-shop, or pivot on `id` to dedupe. ### The composite `id` Each row's `id` is a stable composite key of the form `{eventId}:{sportsbook}:{marketSubtype}:{selectionId}`, e.g. `31cd6744-9444-4fbd-b25c-c4a7d1fce547:ballybet:SPREAD:019f495d-8558-7324-9c9d-91fb4c35d548`. The `selectionId` is a time-ordered UUIDv7. The `id` is stable across polls for the same line, so use it as your primary key to upsert/dedupe: a price or line move on the same selection keeps the same `id` and only changes `priceAmerican`/`priceDecimal`/`line`/`updated`. ### Example row (trimmed) ```json { "id": "31cd6744-9444-4fbd-b25c-c4a7d1fce547:ballybet:SPREAD:019f495d-8558-7324-9c9d-91fb4c35d548", "sportsbook": "ballybet", "league": "mlb", "eventId": "31cd6744-9444-4fbd-b25c-c4a7d1fce547", "market": "SPREAD", "marketSubtype": "SPREAD", "period": "FULL", "selection": "KC Royals", "side": "AWAY", "teamSide": "AWAY", "teamName": "KC Royals", "line": -1.5, "priceAmerican": 200, "priceDecimal": 3.00, "suspended": false, "eventStartTime": "2026-07-10T23:05", "updated": "2026-07-10T14:53:35Z" } ``` ### Field glossary - **`sportsbook`** — the book/DFS platform this line is from (e.g. `ballybet`, `prizepicks`, `draftkings`). Full list at `GET /api/v1/sportsbooks`. - **`market`** — the broad market family: `MONEYLINE`, `SPREAD`, `TOTAL`, `TEAM_TOTAL`, `PLAYER_TOTAL`, `PLAYER_MILESTONE`, `PLAYER_YES_NO`, `MONEYLINE_3_WAY`, `CORRECT_SCORE`, `CATEGORICAL`, `YES_NO`, … - **`marketSubtype`** — the specific stat/variant within the family, e.g. `TOTAL_RUNS`, `PLAYER_TOTAL_FANTASY_POINTS`, `PLAYER_TOTAL_SHOTS`. For simple markets it equals `market` (e.g. `SPREAD`/`SPREAD`). This is the segment used in the composite `id`. Catalog at `GET /api/v1/markets`. - **`period`** — the scope of the wager. `FULL` is the whole game; sport-specific partials appear too (baseball `1INN`, `1INN_5INN`, `1INN_7INN`, `3INN`; halves/quarters in other sports). - **`selection`** — the human label of the picked outcome (team name, `Over`, `Under`, `Draw`, a score line). - **`side` / `outcome`** — the normalized side of a two/three-way market: `HOME`, `AWAY`, `DRAW` (3-way moneyline / correct score), or `OVER` / `UNDER` (totals & player props). Filter on this rather than parsing `selection`. - **`teamSide` / `teamName`** — which side of the event the selection belongs to (`HOME`/`AWAY`) and that team's name. For a player prop this is the player's team side. - **`line`** — the handicap or total the price is quoted against (spread `-1.5`, total `8.5`, prop `4.5`). Absent for pure moneylines. A line move creates a new `line` value on the same `id`. - **`priceAmerican` / `priceDecimal`** — the same price in American (`+200` / `-278`) and decimal (`3.00` / `1.36`) formats; use whichever your math prefers. Control which you get with `odds_format` (`american` | `decimal` | `probability` | `all`, default `all` = both): `american`/`decimal` return only that field; `probability` replaces both with `priceProbability` (implied probability = 1/decimal, 4dp). DFS pick'em offers may omit price and instead carry `dfsMultiplier`. - **`playerId` / `playerName`** — present only on player-prop rows; the canonical player this line is about. Join to `GET /api/v1/players/{id}` and to box-score stats. Absent on team/game markets. - **`dfsMultiplier`** — payout multiplier on DFS pick'em offers (PrizePicks/Underdog-style); `1.0` for a standard pick. Present only where the offer is a multiplier, not a priced line. - **`suspended`** — `true` when the book has the line temporarily locked (do not treat as bettable). - **`eventStartTime`** — scheduled start of the underlying event (denormalized for convenience). - **`updated`** — when this line was last refreshed (ISO-8601, UTC). In the delta feed this is what `since=` compares against. Note: raw odds rows carry book prices only — there is no `fairOdds`/devigged consensus field on an odds row. The no-vig fair value and cross-book comparison live on the derived `GET /api/v1/edges` and `GET /api/v1/arbitrage` signals, computed from these same rows. ### Three (four) ways to read odds — a decision guide - **`GET /api/v1/odds` (+ `/events/{id}/odds`, `/players/{id}/odds`, `/teams/{id}/odds`)** — the current live board, cache-served and seconds-fresh. Use for a point-in-time read of what's bettable now. Scope it (see below) — unscoped it paginates hard. - **`GET /api/v1/odds/updated?since={timestamp}`** — a delta feed returning only rows whose `updated` is newer than `since`. Use for cheap continuous polling after an initial load: pull once, then poll this with your last sync time. Scopeable with `league`, `sportsbook`, `market` (same multi-value semantics as `/odds`) and `odds_format`. - **`GET /api/v1/snapshot`** — the entire live board across every book and league as one pre-built gzip blob. Use to bootstrap a full local copy in a single request, then keep it fresh with the delta feed. - **`GET /api/v1/historical/odds?event_id=…`** — point-in-time replay of a past event's prices for backtesting/model training (paid plans, ×5 credits). Rows are trimmed (no live `id`/`suspended`) and carry an `asOf` timestamp. Rule of thumb: **snapshot** to bootstrap → **/odds/updated** to stay in sync → **/odds** (scoped) for targeted reads → **/historical/odds** to replay the past. ### Scoping odds Unscoped, `/api/v1/odds` spans every league and book and paginates hard — always scope it: - **By event** — `GET /api/v1/events/{id}/odds` (the whole board for one game). - **By entity** — `GET /api/v1/players/{id}/odds` or `GET /api/v1/teams/{id}/odds`. - **By filter** — `GET /api/v1/odds` with any of `league`, `sportsbook`, `market`, `event_id`, `player_id`, `team_id` (combine freely), plus `cursor` and `limit`. `league`, `sportsbook`, and `market` each accept up to 20 comma-separated values matched ANY-of (e.g. `sportsbook=draftkings,fanduel`): league/sportsbook tokens are case-insensitive exact, market tokens are substring matches; more than 20 → `400 invalid_parameter`. Two shaping params apply on every live-odds read (`/odds`, `/odds/updated`, `/events/{id}/odds`, `/players/{id}/odds`, `/teams/{id}/odds`): - **`odds_format`** — `american` | `decimal` | `probability` | `all` (default `all` = both price fields). `probability` swaps the price fields for `priceProbability` (implied probability = 1/decimal, 4dp). Invalid → `400`. - **`is_main`** (boolean; not on `/odds/updated`) — `true` keeps only each market's primary line per book: among a book's alt over/under lines for the same market+period+player/team scope, the most balanced two-way price. Single-line and non-over/under markets pass through. Great for collapsing 12+ alt total lines to the one main number. ## Rate limits & tiers Plans are subscription tiers with an included monthly credit budget. **1 credit = 1 event served** — no matter how many markets, books, or props ride on that event in the response. Empty responses and 304 Not Modified are free; reference endpoints never consume credits. Historical endpoints cost 5 credits per event; a snapshot pull costs 1 credit. Annual billing = 2 months free (~17% off). | Tier | Price | Rate limit | Credits/month | Historical | Snapshot | At the cap | | --- | --- | --- | --- | --- | --- | --- | | Free | $0 | 2 req/min | 5,000 | — | — | Hard stop — upgrade to continue | | Pro | $25/mo ($250/yr) | 10 req/min | 500,000 | ×5 credits | — | Hard stop — upgrade to continue | | Growth | $50/mo ($500/yr) | 30 req/min | 1,500,000 | ×5 credits | 5-min refresh | Hard stop — upgrade to continue | | Enterprise | $100/mo ($1,000/yr) | 60 req/min | Soft-unlimited (5M fair-use) | ×5 credits | Real-time | Fair-use 5M — we'll reach out | Usage is returned in response headers so clients can track consumption — authenticated responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` (epoch seconds when the window resets) — and in full via `GET /api/v1/account/usage`. --- ## Endpoints ### Events Events are the spine of the API — filter by league, team, status, or time, then pull the enriched detail (scores, result, venue, and per-game `context`) or the full box score for any event. #### GET /api/v1/events List events. Query params: `league`, `team_id`, `status`, `starts_after`, `starts_before`, `cursor`, `limit`. ``` GET /api/v1/events?league=mlb&limit=1 ``` ```json { "success": true, "data": [ { "id": "04618737-e571-44d9-befe-24c1d8b1be3f", "homeTeamId": "7d0a80b1-138a-11f1-aba1-de512ebea802", "homeTeamName": "Tampa Bay Rays", "awayTeamId": "7d0a7fdb-138a-11f1-aba1-de512ebea802", "awayTeamName": "New York Yankees", "startTime": "2026-07-09T17:10Z", "status": "scheduled", "league": "mlb", "hasOdds": true } ], "nextCursor": "MTc4MzYxNzAwMDAwMHwwNDYxODcz...", "meta": { "count": 1 } } ``` #### GET /api/v1/events/active Events that currently have live markets. Query params: `league`, `cursor`, `limit`. #### GET /api/v1/events/{id} Enriched detail for one event. Beyond the list fields, the detail adds `venueId`, `seasonYear`, live/final `homeScore`, `awayScore`, `result`, and a `context` object with game conditions — `weather`, `lineups`, `lineups_confirmed`, `lineups_projected`, `probable_pitchers`, `officials`, `actual_starters`, `first_pitch_actual_utc`, and `lineup_source`. Fields populate as they become known (probables/projected lineups pre-game, confirmed lineups and weather near first pitch, scores/result once underway). ``` GET /api/v1/events/04618737-e571-44d9-befe-24c1d8b1be3f ``` ```json { "success": true, "data": { "id": "04618737-e571-44d9-befe-24c1d8b1be3f", "homeTeamId": "7d0a80b1-138a-11f1-aba1-de512ebea802", "homeTeamName": "Tampa Bay Rays", "awayTeamId": "7d0a7fdb-138a-11f1-aba1-de512ebea802", "awayTeamName": "New York Yankees", "startTime": "2026-07-09T17:10Z", "status": "final", "league": "mlb", "hasOdds": true, "venueId": "3f2a...", "seasonYear": 2026, "homeScore": 5, "awayScore": 3, "result": "HOME", "context": { "weather": { "temperatureF": 88, "condition": "Partly Cloudy", "windMph": 7, "windDir": "SSW" }, "probable_pitchers": { "home": "Shane McClanahan", "away": "Gerrit Cole" }, "lineups_confirmed": true, "lineups": { "home": [ /* batting order */ ], "away": [ /* batting order */ ] }, "lineups_projected": null, "officials": [ { "role": "HP", "name": "..." } ], "actual_starters": { "home": "Shane McClanahan", "away": "Gerrit Cole" }, "first_pitch_actual_utc": "2026-07-09T17:12:04Z", "lineup_source": "mlb-statsapi" } }, "meta": { "count": 1 } } ``` #### GET /api/v1/events/{id}/stats Full box score for one event: team-level totals plus a per-player stat blob. `teams` carries the final line (`homeScore`, `awayScore`, `result`) and a team `stats` object; `players` carries one row per participant with a `data` blob whose keys vary by sport (e.g. `batting_stats` / `pitching_stats` for MLB, shooting/rebounding splits for basketball). ``` GET /api/v1/events/04618737-e571-44d9-befe-24c1d8b1be3f/stats ``` ```json { "success": true, "data": { "eventId": "04618737-e571-44d9-befe-24c1d8b1be3f", "teams": [ { "eventId": "04618737-e571-44d9-befe-24c1d8b1be3f", "homeId": "7d0a80b1-138a-11f1-aba1-de512ebea802", "awayId": "7d0a7fdb-138a-11f1-aba1-de512ebea802", "league": "mlb", "homeScore": 5, "awayScore": 3, "result": "HOME", "stats": { "hits": 9, "errors": 0, "runs": 5 } } ], "players": [ { "eventId": "04618737-e571-44d9-befe-24c1d8b1be3f", "playerId": "9a11...", "teamId": "7d0a80b1-138a-11f1-aba1-de512ebea802", "date": "2026-07-09", "data": { "batting_stats": { "atBats": 4, "hits": 2, "homeRuns": 1, "rbi": 3 } } } ] }, "meta": { "count": 1, "updated": "2026-07-09T21:05:00Z" } } ``` #### GET /api/v1/events/{eventId}/odds The full normalized odds board for one event. Query params: `odds_format`, `is_main`. Supports `If-None-Match` for `304` responses. Returns an array of odds rows (see the Odds schema below). ### Odds #### GET /api/v1/odds Flat, normalized odds feed with entity scoping. Query params: `league`, `sportsbook`, `market` (each accepts up to 20 comma-separated values, ANY-of; league/sportsbook = case-insensitive exact per token, market = substring per token), `event_id`, `player_id`, `team_id`, `odds_format`, `is_main`, `cursor`, `limit`, and `If-None-Match` header. ``` GET /api/v1/odds?league=wnba&sportsbook=ballybet&limit=1 ``` ```json { "success": true, "data": [ { "id": "b9445260-...:ballybet:SPREAD:019f40db-...", "sportsbook": "ballybet", "league": "wnba", "eventId": "b9445260-2841-4c38-9b8d-0598229ca565", "market": "SPREAD", "marketSubtype": "SPREAD", "period": "FULL", "selection": "Atlanta Dream", "priceAmerican": 190, "priceDecimal": 2.9, "line": -16.5, "side": "HOME", "teamSide": "HOME", "teamName": "Atlanta Dream", "playerId": null, "playerName": null, "dfsMultiplier": null, "suspended": false, "eventStartTime": "2026-07-10T00:00", "updated": "2026-07-09T16:41:27Z" } ], "nextCursor": "YmFsbHliZXQg...", "meta": { "count": 1, "updated": "2026-07-09T16:41:27Z" } } ``` Odds row fields: `id` (stable line id), `sportsbook`, `league`, `eventId`, `market`, `marketSubtype`, `period`, `selection`, `priceAmerican`, `priceDecimal`, `line`, `side`, `teamSide`, `teamName`, `playerId`, `playerName`, `dfsMultiplier` (for DFS pick'em offers), `suspended`, `eventStartTime`, `updated`. #### GET /api/v1/odds/updated Delta feed — only odds that changed since a timestamp. Query params: `since` (ISO-8601), `league`, `sportsbook`, `market` (multi-value, same semantics as `/odds` — scope the delta to just your slice), `odds_format`, `cursor`, `limit`, and `If-None-Match` header. Same row shape as `/odds`. #### GET /api/v1/players/{playerId}/odds All odds for one player. Query params: `odds_format`, `is_main`, `cursor`, `limit`, `If-None-Match`. #### GET /api/v1/teams/{teamId}/odds All odds for one team. Query params: `odds_format`, `is_main`, `cursor`, `limit`, `If-None-Match`. ### Stats Box-score stats for training and backtesting — pull one player, one team, or bulk-filter across an event, team, or player. #### GET /api/v1/players/{id}/stats Per-event box-score stats for one player. Query params: `season`. Each row has `eventId`, `playerId`, `teamId`, `date`, and a `data` JSON blob of stats. #### GET /api/v1/teams/{id}/stats Per-event box-score stats for one team. Query params: `season`. Same shape as player stats. #### GET /api/v1/stats/players Bulk player box-score stats across events. Requires at least one of `event_id`, `team_id`, or `player_id` (a request with none returns `400`). Query params: `event_id`, `team_id`, `player_id`, `season`, `cursor`, `limit`. Rows share the per-player stats shape (`eventId`, `playerId`, `teamId`, `date`, `data`). ``` GET /api/v1/stats/players?team_id=7d0a80b1-138a-11f1-aba1-de512ebea802&limit=1 ``` ```json { "success": true, "data": [ { "eventId": "04618737-e571-44d9-befe-24c1d8b1be3f", "playerId": "9a11...", "teamId": "7d0a80b1-138a-11f1-aba1-de512ebea802", "date": "2026-07-09", "data": { "batting_stats": { "atBats": 4, "hits": 2, "homeRuns": 1, "rbi": 3 } } } ], "nextCursor": "MjAyNi0wNy0wOXw5YTEx...", "meta": { "count": 1 } } ``` ### Snapshot #### GET /api/v1/snapshot The entire live board — every normalized odds row across all books and leagues — as a single pre-built **gzip** blob (~30–40 MB compressed). This is the fastest way to bootstrap a local copy of the whole dataset in one request; thereafter poll `/api/v1/odds/updated?since=` to stay in sync. - The response body is gzip-compressed. Use `curl --compressed` (or set `Accept-Encoding: gzip` and gunzip yourself). - Every response carries a strong-ish `ETag` (e.g. `W/"1c6b2b2-d4f14326"`) equal to the current snapshot build hash. - Send the ETag back in `If-None-Match` to get a cheap `304 Not Modified` when the board hasn't changed since your last pull — no body transferred. ``` # First pull — download and decompress the full board curl --compressed "https://api.sportwizzard.com/api/v1/snapshot" \ -H "X-Api-Key: sw_live_your_api_key_here" \ -D headers.txt -o snapshot.json # Read the ETag from headers.txt, e.g. ETag: W/"1c6b2b2-d4f14326" # Next pull — 304 if nothing changed, full body if it did curl --compressed "https://api.sportwizzard.com/api/v1/snapshot" \ -H "X-Api-Key: sw_live_your_api_key_here" \ -H 'If-None-Match: W/"1c6b2b2-d4f14326"' -i # → HTTP/1.1 304 Not Modified (no body) ``` ### Reference #### GET /api/v1/status Platform status: total live markets and sportsbook count. No parameters. ```json { "success": true, "data": [ { "totalMarkets": 115604, "sportsbooks": 29 } ], "meta": { "count": 1, "updated": "2026-07-09T16:42:41Z" } } ``` #### GET /api/v1/leagues Active leagues — the ones with an in-progress season right now. No parameters. **Call this first**: leagues rotate seasonally (e.g. `nba`/`nfl` are absent for most of the summer and reappear in-season). Querying `/odds`, `/events`, or `/edges` with a league that isn't in this list is not an error — it's a valid request that returns `data: []`, meaning "out of season," not "broken" or "unsupported." ```json { "success": true, "data": ["mma", "dota2", "mlb", "brazil-sa", "lol", "fifa-world-cup", "tennis", "wnba", "cs2", "pga-tour", "mls"], "meta": { "count": 11, "updated": "2026-07-14T05:52:07Z" } } ``` `/odds`, `/odds/updated`, `/events`, `/events/active`, and `/edges` also set `meta.note` automatically when a league-scoped query comes back empty because none of the requested league(s) currently has an in-progress season — it names the leagues that ARE active right now, e.g. `{"data":[],"meta":{"count":0,"note":"No league in [nba] currently has an in-progress season (leagues are seasonal — always check GET /api/v1/leagues first). Active leagues right now: mma, dota2, mlb, ..."}}`. You still don't need to guess "no data" vs. "wrong league" — the hint travels with the response. #### GET /api/v1/seasons Seasons for a given calendar year, across every league. Query params: `year` (required). Useful for mapping a league to its current/past season window (`startDate`/`endDate`) or resolving a `seasonYear` from `/events/{id}` into concrete dates. ``` GET /api/v1/seasons?year=2026 ``` ```json { "success": true, "data": [ { "id": "3184d2cc-4c2a-40bd-a12e-2d625a3f7f8d", "league": "mlb", "year": 2026, "startDate": "2026-03-25", "endDate": "2026-11-12" }, { "id": "0337f816-86e6-45fd-8f67-6ab60b4cf771", "league": "mma", "year": 2026, "startDate": "2026-01-01", "endDate": "2026-12-31" } ], "meta": { "count": 2 } } ``` #### GET /api/v1/sportsbooks Sportsbooks & DFS platforms with live market counts. No parameters. ```json { "success": true, "data": [ { "id": "fanduel", "marketCount": 8421 }, { "id": "prizepicks", "marketCount": 3120 } ], "meta": { "count": 29 } } ``` #### GET /api/v1/markets Full market-type catalog we normalize into. No parameters. #### GET /api/v1/markets/active Market types that currently have live markets. Returns `marketType`, `marketSubtype`, `liveCount`. ```json { "success": true, "data": [ { "marketType": "OVER_UNDER", "marketSubtype": "POINTS", "liveCount": 842 } ], "meta": { "count": 120 } } ``` #### GET /api/v1/teams Teams. Query params: `league`, `cursor`, `limit`. ```json { "success": true, "data": [ { "id": "7d0a80b1-...", "name": "Tampa Bay Rays", "abbreviation": "TB", "league": "mlb" } ], "nextCursor": "…", "meta": { "count": 30 } } ``` #### GET /api/v1/teams/{id} Get one team by ID. #### GET /api/v1/players Players. Query params: `league`, `team` (team id), `team_id` (alias of `team`; wins if both are given), `cursor`, `limit`. ```json { "success": true, "data": [ { "id": "…", "name": "Jayson Tatum", "league": "nba", "teamId": "…", "position": "SF" } ], "nextCursor": "…", "meta": { "count": 50 } } ``` #### GET /api/v1/players/{id} Get one player by ID. ### Historical (paid plans, ×5 credits) #### GET /api/v1/historical/odds Point-in-time odds for backtesting. Query params: `event_id`, `date`. Rows include `eventId`, `sportsbook`, `market`, `marketSubtype`, `period`, `selection`, `side`, `line`, `priceAmerican`, `asOf`. #### GET /api/v1/historical/arbitrage Settled arbitrage history. Query params: `from`, `to`, `min_profit`, `limit`. ### Edges & Arbitrage (derived signals) Edges and arbitrage are computed from the same odds data above — handy defaults, yours to outdo. Build your own signals on the raw odds/stats/snapshot if you'd rather. #### GET /api/v1/edges DFS edge opportunities: a DFS pick'em line compared against a sharp sportsbook's implied probability. Query params: `league`, `source`, `min_edge` (fractional, must be within [0, 1] — e.g. 0.02 = 2%; outside → `400`), `event_id`, `cursor`, `limit`. How to read an edge: it measures the gap between a DFS line and the *most favorable* sportsbook's vig-removed (no-vig) implied probability. Because the reference is the best available book, the number is optimistic by construction, and an edge resting on a single book's quote can reflect a stale line rather than real value. Treat edges as a screening signal for soft DFS lines that warrants your own verification — not a guaranteed +EV bet. When presenting edges to end users, relay this caveat. ``` GET /api/v1/edges?league=nba&min_edge=0.02 ``` ```json { "success": true, "data": [ { "id": "…", "league": "nba", "eventId": "…", "eventHomeTeam": "Boston Celtics", "eventAwayTeam": "Miami Heat", "eventStartTime": "2026-07-10T23:30Z", "market": "PLAYER_POINTS", "statType": "POINTS", "period": "FULL", "line": 27.5, "side": "OVER", "playerId": "…", "playerName": "Jayson Tatum", "playerTeamName": "Boston Celtics", "dfsSource": "prizepicks", "dfsMultiplier": 1.5, "dfsSelectionLabel": "Over 27.5", "bookSource": "pinnacle", "bookImpliedProbability": 58.2, "oddsByBook": [ { "source": "pinnacle", "over": -139, "under": 118, "impliedProb": 58.2 }, { "source": "fanduel", "over": -132, "under": 110 } ] } ], "nextCursor": null, "meta": { "count": 1 } } ``` #### GET /api/v1/arbitrage Cross-book arbitrage opportunities. Query params: `type` (`two-way` or `three-way`), `league` (multi-value, up to 20 comma-separated codes, ANY-of), `event_id` (single event), `min_profit`, `cursor`, `limit`. The quoted margin holds only if every leg fills at the listed price before it moves — verify prices at the books at placement time. ``` GET /api/v1/arbitrage?type=two-way&min_profit=0.01 ``` ```json { "success": true, "data": [ { "id": "…", "type": "two-way", "profitPercent": 2.3, "league": "nba", "eventId": "…", "eventHomeTeam": "Los Angeles Lakers", "eventAwayTeam": "Golden State Warriors", "eventStartTime": "2026-07-11T02:30Z", "playerId": "…", "playerName": "LeBron James", "lastSeen": "2026-07-09T16:40:00Z", "selections": [ { "source": "fanduel", "marketType": "OVER_UNDER", "marketSubtype": "POINTS", "outcome": "OVER 25.5", "outcomeCode": "OVER", "selectionLabel": "Over 25.5", "lineValue": 25.5, "odds": -110, "stakeFraction": 0.488 }, { "source": "betmgm", "marketType": "OVER_UNDER", "marketSubtype": "POINTS", "outcome": "UNDER 25.5", "outcomeCode": "UNDER", "selectionLabel": "Under 25.5", "lineValue": 25.5, "odds": 105, "stakeFraction": 0.512 } ] } ], "nextCursor": null, "meta": { "count": 1 } } ``` ### Account #### GET /api/v1/account/usage Your current plan, rate limit, and monthly usage against your cap. Called with a valid key it reflects that key's plan; called anonymously it reports the effective anonymous plan. Never errors. ``` GET /api/v1/account/usage ``` ```json { "success": true, "data": [ { "plan": "enterprise", "rateLimitPerMin": 60, "authenticated": false, "note": "No API key supplied; showing the effective anonymous plan (pre-billing, unmetered)." } ], "meta": { "count": 1 } } ``` --- ## Errors Errors return `success: false` with an appropriate HTTP status (`400` bad request — e.g. `/stats/players` with no entity filter, more than 20 comma-separated filter values, an invalid `odds_format`, or `min_edge` outside [0, 1] (all with error code `invalid_parameter`); `401` missing/invalid key, `404` not found, `429` rate limited). Rate-limited responses include headers indicating remaining quota. `304 Not Modified` is returned when an `If-None-Match` ETag matches. ## MCP server SportWizzard ships a Model Context Protocol server so agents (Claude Code, Claude Desktop, Codex, and any MCP-compatible client) can query the platform as tools instead of raw HTTP. 20 tools cover the v1 surface: live odds, DFS edges, cross-book arbitrage, events, box-score stats, and reference data (leagues, sportsbooks, teams, players, markets). `download_snapshot` streams the entire live board (`/api/v1/snapshot`) straight to a file on disk, so an agent can bootstrap a full local copy in one call. **Hosted (recommended, no install)** — Streamable HTTP at `https://mcp.sportwizzard.com/mcp`, stateless, authenticate with `Authorization: Bearer `. Verified live this session (a bare request without a key returns a proper JSON-RPC error naming `/account` as where to get one, not a connection failure): ``` claude mcp add --transport http sportwizzard https://mcp.sportwizzard.com/mcp \ --header "Authorization: Bearer sw_live_your_api_key_here" ``` **From source (stdio)** — open-source, mock mode (`SPORTWIZZARD_MOCK=1`) for zero-API-call development: - Repository: https://github.com/adamruehle/sportwizzard-mcp - Quick start (works today, no clone/build step — `npx` installs straight from GitHub): `npx -y github:adamruehle/sportwizzard-mcp`. The shorter `npx -y sportwizzard-mcp` (no `github:` prefix) will work once the package is published to npm — as of this document's last update it is not yet on the registry (404); see the repo README's "npm publish status" section for the one-time human step that unblocks it. Claude Desktop config (`claude_desktop_config.json`) — stdio, from-source: ```json { "mcpServers": { "sportwizzard": { "command": "npx", "args": ["-y", "github:adamruehle/sportwizzard-mcp"], "env": { "SPORTWIZZARD_API_KEY": "sw_live_..." } } } } ``` ## Links - Documentation: https://sportwizzard.com/developers - Agent quickstart: https://sportwizzard.com/developers/agents - Agent-facing project guide (AGENTS.md): https://sportwizzard.com/AGENTS.md - Interactive reference: https://sportwizzard.com/developers/reference - OpenAPI spec: https://sportwizzard.com/openapi.json - MCP server: https://github.com/adamruehle/sportwizzard-mcp - Pricing: https://sportwizzard.com/pricing