|
| 1 | +--- |
| 2 | +description: "GET /api/v1/gamestate — Aggregated live game state (scores, periods, clocks, situational data) merged across sportsbooks into a single authoritative view per event. Enterprise tier only." |
| 3 | +--- |
| 4 | + |
| 5 | +import { Callout, Tabs } from 'nextra/components' |
| 6 | + |
| 7 | +# Live Game State |
| 8 | + |
| 9 | +Aggregated live game state — scores, periods, clocks, possession, and |
| 10 | +sport-specific situational data — merged across sportsbooks into a |
| 11 | +single authoritative view per event. One row per live fixture, |
| 12 | +consensus-selected from the books covering it. |
| 13 | + |
| 14 | +``` |
| 15 | +GET /api/v1/gamestate |
| 16 | +GET /api/v1/gamestate/{sport} |
| 17 | +``` |
| 18 | + |
| 19 | +## Authentication |
| 20 | + |
| 21 | +Requires API key. **Enterprise tier only.** Non-Enterprise tiers receive |
| 22 | +`403 tier_restricted`. |
| 23 | + |
| 24 | +Live game state also streams over the `gamestate` channel on |
| 25 | +[SSE](/api-reference/stream) and [WebSocket](/api-reference/websocket) |
| 26 | +— see [Streaming](#streaming) below. |
| 27 | + |
| 28 | +## Path Parameters |
| 29 | + |
| 30 | +| Parameter | Type | Description | |
| 31 | +|-----------|------|-------------| |
| 32 | +| `sport` | string | (optional) Single sport to return. Matches a known Atlas sport (`baseball`, `basketball`, `football`, `hockey`, `soccer`, `tennis`, `table_tennis`, `cricket`, `esports`, `snooker`, `other`). Case-insensitive. Unknown sports return an empty `data` object. | |
| 33 | + |
| 34 | +Without the path parameter, returns every sport with live events. |
| 35 | + |
| 36 | +## Response Envelope |
| 37 | + |
| 38 | +```json |
| 39 | +{ |
| 40 | + "data": { |
| 41 | + "<sport>": { |
| 42 | + "<event_id>": { ...event state... } |
| 43 | + } |
| 44 | + }, |
| 45 | + "updated_at": "2026-04-23T23:55:01.234Z" |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +Every event state is keyed by its canonical `event_id` within its sport |
| 50 | +bucket. `updated_at` is the server's time when this response was built |
| 51 | +— use it to gauge freshness if polling. |
| 52 | + |
| 53 | +## Event State Fields |
| 54 | + |
| 55 | +Every field is optional except where noted. Presence varies by sport — |
| 56 | +tennis events carry `sets_home` / `server`; hockey events carry |
| 57 | +`power_play`; soccer events carry `corners_*` / `yellow_cards_*`; etc. |
| 58 | + |
| 59 | +### Always present |
| 60 | + |
| 61 | +| Field | Type | Notes | |
| 62 | +|---|---|---| |
| 63 | +| `home_team` | string | Normalized home team name | |
| 64 | +| `away_team` | string | Normalized away team name | |
| 65 | +| `sport` | string | Sport bucket (`baseball`, `soccer`, …) | |
| 66 | +| `league` | string | Atlas league id (e.g. `mlb`, `england_-_premier_league`) | |
| 67 | +| `home_score` | integer | Current home score in the natural unit for the sport (runs, points, goals, sets — tennis uses summed set points) | |
| 68 | +| `away_score` | integer | Current away score | |
| 69 | +| `is_live` | boolean | Always `true` in this endpoint (non-live events aren't included) | |
| 70 | +| `primary_book` | string | The book the merged scores were selected from. Consensus-respecting — higher-quality books win ties | |
| 71 | +| `book_count` | integer | Number of sportsbooks that contributed live state for this event at merge time | |
| 72 | + |
| 73 | +### Temporal (most team sports) |
| 74 | + |
| 75 | +| Field | Type | Example | |
| 76 | +|---|---|---| |
| 77 | +| `game_period` | string | `"T5"` (MLB top of 5th), `"Q3"` (NBA Q3), `"2H"` (soccer 2nd half), `"P2"` (NHL), `"S2"` (tennis 2nd set), `"FT"` (full time) | |
| 78 | +| `game_clock` | string | `"49:06"` for countup sports (soccer), `"5:42"` for countdown sports (basketball, hockey) | |
| 79 | + |
| 80 | +### Situational |
| 81 | + |
| 82 | +| Field | Type | Notes | |
| 83 | +|---|---|---| |
| 84 | +| `possession` | `"home" \| "away"` | Which team has possession (team sports) or is serving (tennis) | |
| 85 | +| `server` | `"home" \| "away"` | Tennis only — current server | |
| 86 | +| `last_play` | string | Sport-specific last-play description when available | |
| 87 | +| `is_timeout` | boolean | `true` during a timeout | |
| 88 | + |
| 89 | +### Sport-specific |
| 90 | + |
| 91 | +| Field | Sport | Type | |
| 92 | +|---|---|---| |
| 93 | +| `fouls_home`, `fouls_away` | basketball, soccer | integer | |
| 94 | +| `corners_home`, `corners_away` | soccer | integer | |
| 95 | +| `yellow_cards_home`, `yellow_cards_away` | soccer | integer | |
| 96 | +| `red_cards_home`, `red_cards_away` | soccer | integer | |
| 97 | +| `power_play` | hockey | `"home" \| "away"` | |
| 98 | +| `sets_home`, `sets_away` | tennis | integer (sets won) | |
| 99 | +| `hits_home`, `hits_away` | baseball | integer | |
| 100 | +| `home_pitcher`, `away_pitcher` | baseball | string — starting pitcher display name | |
| 101 | +| `wickets_home`, `wickets_away` | cricket | integer | |
| 102 | +| `overs` | cricket | float | |
| 103 | +| `batting_team` | cricket | `"home" \| "away"` | |
| 104 | + |
| 105 | +### Freshness |
| 106 | + |
| 107 | +| Field | Type | Meaning | |
| 108 | +|---|---|---| |
| 109 | +| `stale` | `true` (omitted otherwise) | The `primary_book`'s livestate key TTL dropped below the aggregator's freshness threshold (~10s) at merge time. Scores are the last known values; the book has gone quiet. Treat the event's scores and period as potentially lagging the real game state. | |
| 110 | +| `aggregator_stale` | `true` (omitted otherwise) | The SharpAPI aggregator itself hasn't written a new `gamestate:{sport}` shard for this sport in more than ~30s. Broader signal than `stale` — indicates a pipeline hiccup, not a single-book issue. If you see this persistently, reach out to support. | |
| 111 | + |
| 112 | +<Callout type="info"> |
| 113 | +**Absence = fresh.** Both `stale` and `aggregator_stale` are only |
| 114 | +written when truthy, keeping the payload compact. If you don't see |
| 115 | +them on an event, the data is considered fresh. |
| 116 | +</Callout> |
| 117 | + |
| 118 | +## Example Requests |
| 119 | + |
| 120 | +<Tabs items={['cURL', 'JavaScript', 'Python']}> |
| 121 | + <Tabs.Tab> |
| 122 | +```bash |
| 123 | +# All live events across every sport |
| 124 | +curl "https://api.sharpapi.io/api/v1/gamestate" \ |
| 125 | + -H "X-API-Key: YOUR_API_KEY" |
| 126 | + |
| 127 | +# One sport |
| 128 | +curl "https://api.sharpapi.io/api/v1/gamestate/soccer" \ |
| 129 | + -H "X-API-Key: YOUR_API_KEY" |
| 130 | +``` |
| 131 | + </Tabs.Tab> |
| 132 | + <Tabs.Tab> |
| 133 | +```javascript |
| 134 | +const res = await fetch('https://api.sharpapi.io/api/v1/gamestate/soccer', { |
| 135 | + headers: { 'X-API-Key': process.env.SHARPAPI_KEY } |
| 136 | +}); |
| 137 | +const { data, updated_at } = await res.json(); |
| 138 | +for (const [eventId, state] of Object.entries(data.soccer ?? {})) { |
| 139 | + if (state.stale || state.aggregator_stale) continue; // skip lagging rows |
| 140 | + console.log( |
| 141 | + `${state.home_team} ${state.home_score}-${state.away_score} ${state.away_team}`, |
| 142 | + `(${state.game_period ?? '?'} ${state.game_clock ?? ''}, ${state.primary_book})` |
| 143 | + ); |
| 144 | +} |
| 145 | +``` |
| 146 | + </Tabs.Tab> |
| 147 | + <Tabs.Tab> |
| 148 | +```python |
| 149 | +import httpx, os |
| 150 | +r = httpx.get( |
| 151 | + "https://api.sharpapi.io/api/v1/gamestate/soccer", |
| 152 | + headers={"X-API-Key": os.environ["SHARPAPI_KEY"]}, |
| 153 | +) |
| 154 | +body = r.json() |
| 155 | +for event_id, state in body["data"].get("soccer", {}).items(): |
| 156 | + if state.get("stale") or state.get("aggregator_stale"): |
| 157 | + continue # skip lagging rows |
| 158 | + print( |
| 159 | + f"{state['home_team']} {state['home_score']}-{state['away_score']} " |
| 160 | + f"{state['away_team']} ({state.get('game_period')} " |
| 161 | + f"{state.get('game_clock','')}, {state['primary_book']})" |
| 162 | + ) |
| 163 | +``` |
| 164 | + </Tabs.Tab> |
| 165 | +</Tabs> |
| 166 | +
|
| 167 | +## Example Response |
| 168 | +
|
| 169 | +```json |
| 170 | +{ |
| 171 | + "data": { |
| 172 | + "soccer": { |
| 173 | + "argentina_-_primera_division_bocajuniors_defensayjusticia_2026-04-23": { |
| 174 | + "home_team": "Defensa y Justicia", |
| 175 | + "away_team": "Boca Juniors", |
| 176 | + "sport": "soccer", |
| 177 | + "league": "argentina_-_primera_division", |
| 178 | + "home_score": 0, |
| 179 | + "away_score": 1, |
| 180 | + "game_period": "2H", |
| 181 | + "game_clock": "49:06", |
| 182 | + "is_live": true, |
| 183 | + "possession": "away", |
| 184 | + "corners_home": 1, |
| 185 | + "corners_away": 0, |
| 186 | + "fouls_home": 0, |
| 187 | + "fouls_away": 0, |
| 188 | + "yellow_cards_home": 0, |
| 189 | + "yellow_cards_away": 0, |
| 190 | + "red_cards_home": 0, |
| 191 | + "red_cards_away": 0, |
| 192 | + "primary_book": "draftkings", |
| 193 | + "book_count": 6 |
| 194 | + }, |
| 195 | + "chile_-_primera_division_concepcion_palestino_2026-04-24": { |
| 196 | + "home_team": "Palestino", |
| 197 | + "away_team": "Concepción", |
| 198 | + "sport": "soccer", |
| 199 | + "league": "chile_-_primera_division", |
| 200 | + "home_score": 0, |
| 201 | + "away_score": 0, |
| 202 | + "is_live": true, |
| 203 | + "primary_book": "unibet", |
| 204 | + "book_count": 1, |
| 205 | + "stale": true |
| 206 | + } |
| 207 | + } |
| 208 | + }, |
| 209 | + "updated_at": "2026-04-23T23:55:01.234Z" |
| 210 | +} |
| 211 | +``` |
| 212 | +
|
| 213 | +## Cross-Book Merge Model |
| 214 | +
|
| 215 | +Each event's fields are merged from every sportsbook's livestate |
| 216 | +snapshot via a three-class algorithm: |
| 217 | +
|
| 218 | +- **Class A — Scores** are picked by consensus: among books at the most |
| 219 | + advanced period rank, the highest-total score with ≥2 backers wins. A |
| 220 | + single book reporting an outlier score (common in the first seconds |
| 221 | + of a score change, or from a misbehaving adapter) is rejected. |
| 222 | +- **Class B — Temporal fields** (`game_period`, `game_clock`) are |
| 223 | + selected by sport-aware clock direction — countdown vs countup — and |
| 224 | + period rank. |
| 225 | +- **Class C — Situational fields** (`possession`, `corners_*`, etc.) |
| 226 | + are priority-filled from a fixed book ranking. |
| 227 | +
|
| 228 | +`primary_book` reports which book the winning scores came from. |
| 229 | +`book_count` is the total number of books that contributed any state |
| 230 | +for the event at merge time. |
| 231 | +
|
| 232 | +## Streaming |
| 233 | +
|
| 234 | +Every update that the REST endpoint would surface on the next poll |
| 235 | +also fires a `gamestate:update` event on the streaming channels: |
| 236 | +
|
| 237 | +- **SSE**: [`GET /api/v1/stream/gamestate`](/api-reference/stream#gamestate) |
| 238 | +- **WebSocket**: subscribe to `{channels: ["gamestate"]}` on |
| 239 | + [`wss://ws.sharpapi.io/ws`](/api-reference/websocket) |
| 240 | +
|
| 241 | +Streaming clients receive an initial `gamestate:snapshot` with the full |
| 242 | +current state (a flat list of event rows), then `gamestate:update` |
| 243 | +events carrying changed rows as they merge, and `gamestate:removed` |
| 244 | +events when events drop from the live set. |
| 245 | +
|
| 246 | +## Rate Limits & Quotas |
| 247 | +
|
| 248 | +Same Enterprise-tier limits as other endpoints — see |
| 249 | +[Pricing](https://sharpapi.io/pricing). `/gamestate` counts the same |
| 250 | +as other REST calls toward the per-minute quota. |
| 251 | +
|
| 252 | +## Troubleshooting |
| 253 | +
|
| 254 | +- **403 `tier_restricted`** — your key doesn't have the gamestate |
| 255 | + feature. Upgrade to Enterprise. |
| 256 | +- **Empty `data` object** — no live events in scope. This is common |
| 257 | + between events on smaller sport schedules. Poll again. |
| 258 | +- **Events with `stale: true`** — the primary book has gone silent. |
| 259 | + Scores may lag; consider filtering them out or surfacing a UI |
| 260 | + indicator. |
| 261 | +- **Events with `aggregator_stale: true`** — the SharpAPI aggregator |
| 262 | + hasn't refreshed that sport in >30s. If persistent, reach out to |
| 263 | + support; a brief bump can happen during redeploys. |
| 264 | +- **Same match appears twice under different `event_id`s** — known |
| 265 | + limitation for some regional leagues where sportsbooks use |
| 266 | + inconsistent league naming. Use `(home_team, away_team)` as a |
| 267 | + secondary dedup key on the client until the remaining alias gaps |
| 268 | + close. |
0 commit comments