Skip to content

Commit 07abf54

Browse files
Mlaz-codeclaude
andcommitted
docs(gamestate): rewrite reference page + OpenAPI spec + stream doc
The 2026-04-22 Cross-Repo Audit deleted content/en/api-reference/ gamestate.mdx because the previous version had 6+ accuracy issues (camelCase vs actual snake_case, fabricated query params, wrong envelope, non-existent error code). The audit also left the OpenAPI spec in public/openapi.json pointing at a broken `/gamestate` response shape and an EventGameState schema missing 15+ fields that the endpoint actually emits. The 2026-04-23 comprehensive audit captured the real wire format (Agent D's schema catalog in /tmp/gs-bench/ COMPREHENSIVE_AUDIT.md); this commit replaces the docs against that catalog. Changes: - content/en/api-reference/gamestate.mdx — NEW. Full reference: auth (Enterprise-only), path parameters, exact response envelope, field catalog grouped by always-present / temporal / situational / sport-specific / freshness, cURL/JS/Python examples, a realistic example response pulled from live production, cross-book merge model description, pointers to SSE + WS streaming channels, rate limits, troubleshooting. - content/en/api-reference/_meta.js — add `gamestate: "Live Game State"` under a new "Live Game State" separator between Splits and Opportunities. - content/en/api-reference/stream.mdx — add `gamestate` to the channel param description, the channels table (with link to the new gamestate page), and the convenience-routes table (/stream/gamestate alias added in sharp-api-go commit 8308796). - public/openapi.json — replace the `/gamestate` operation (was "List game state sports" returning a wrong `{sports:[...]}` shape) with the real envelope `{data:{sport:{event_id:EventGameState}}, updated_at}`. Same envelope on `/gamestate/{sport}`. Rewrite EventGameState to carry every field the endpoint emits: home_team/away_team/sport/ league (previously missing and required), primary_book/book_count (aggregator metadata, previously missing), game_period/game_clock (renamed from period/clock to match actual field names), possession/ server/last_play, sport-specific (fouls/corners/yellow_cards/ red_cards/power_play/sets/hits/pitchers/wickets/overs/batting_team), plus the stale + aggregator_stale freshness flags (the latter newly exposed to REST in commit d7f8ac0 on sharp-api-go). required[] now lists the 9 always-present fields; everything else is optional with presence tied to sport coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f372f38 commit 07abf54

4 files changed

Lines changed: 471 additions & 65 deletions

File tree

content/en/api-reference/_meta.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export default {
3737
title: "Betting Splits",
3838
},
3939
splits: "Betting Splits",
40+
"---LiveGameState": {
41+
type: "separator",
42+
title: "Live Game State",
43+
},
44+
gamestate: "Live Game State",
4045
"---Opportunities": {
4146
type: "separator",
4247
title: "Opportunities",
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
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 with2 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.

content/en/api-reference/stream.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ https://api.sharpapi.io/api/v1/stream?api_key=sk_live_your_key
2929

3030
| Parameter | Type | Default | Description |
3131
|-----------|------|---------|-------------|
32-
| `channel` | string | `opportunities` | What to stream: `odds`, `opportunities`, or `all` |
32+
| `channel` | string | `opportunities` | What to stream: `odds`, `opportunities`, `gamestate` (Enterprise only), or `all` |
3333
| `sport` | string | all | Filter by sport(s), comma-separated (e.g. `basketball`, `football`, `ice_hockey`) |
3434
| `sportsbook` | string | tier-allowed | Filter by sportsbook(s), comma-separated |
3535
| `league` | string | all | Filter by league(s), comma-separated |
@@ -46,6 +46,7 @@ https://api.sharpapi.io/api/v1/stream?api_key=sk_live_your_key
4646
|---------|-----------------|----------|
4747
| `odds` | `snapshot`, `odds:update`, `odds:removed`, `heartbeat` | Track odds movements |
4848
| `opportunities` | `snapshot`, `ev:detected/expired`, `arb:detected/expired`, `middles:detected/expired`, `low_hold:detected/expired`, `heartbeat` | Alert on opportunities |
49+
| `gamestate` | `gamestate:snapshot`, `gamestate:update`, `gamestate:removed`, `heartbeat` | Live scores, periods, clocks, and situational data per event. **Enterprise tier only.** See [Live Game State](/api-reference/gamestate) for the full field catalog. |
4950
| `all` | All event types | Full real-time picture |
5051

5152
### Convenience Routes
@@ -54,6 +55,8 @@ https://api.sharpapi.io/api/v1/stream?api_key=sk_live_your_key
5455
|-------|---------------|
5556
| `GET /api/v1/stream/odds` | `/api/v1/stream?channel=odds` |
5657
| `GET /api/v1/stream/opportunities` | `/api/v1/stream?channel=opportunities` |
58+
| `GET /api/v1/stream/gamestate` | `/api/v1/stream?channel=gamestate` |
59+
| `GET /api/v1/stream/all` | `/api/v1/stream?channel=all` |
5760
| `GET /api/v1/stream/events/:eventId` | `/api/v1/stream?channel=odds&event=:eventId` |
5861

5962
## SSE Event Types

0 commit comments

Comments
 (0)