Skip to content

Commit 5770d74

Browse files
Mlaz-codeclaude
andcommitted
docs(streaming): comprehensive update to WebSocket and SSE docs
WebSocket: add missing query params (market, event_id, min_ev, min_profit, resume, from_seq), document seq/global_seq sequence tracking, add odds_removed message type, add reconnection with replay section, update all message schemas with seq fields and additional metadata. SSE: fix default channel (opportunities not odds), add missing event types (middles:detected/expired, low_hold:detected/expired, odds:removed, snapshot:complete), add convenience routes, add trial info to connected event, update code examples with correct field names and new events. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bf39b95 commit 5770d74

4 files changed

Lines changed: 342 additions & 74 deletions

File tree

content/api-reference/stream.mdx

Lines changed: 179 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,32 @@ https://api.sharpapi.io/api/v1/stream?api_key=sk_live_your_key
2525

2626
| Parameter | Type | Default | Description |
2727
|-----------|------|---------|-------------|
28-
| `channel` | string | `odds` | What to stream: `odds`, `opportunities`, or `all` |
29-
| `sportsbook` | string | - | Filter by sportsbook(s), comma-separated |
30-
| `sport` | string | - | Filter by sport(s), comma-separated |
31-
| `league` | string | - | Filter by league(s), comma-separated |
32-
| `event` | string | - | Filter by event ID(s), comma-separated |
33-
| `market` | string | - | Filter by market type(s), comma-separated |
34-
| `min_ev` | number | - | Minimum EV percentage for opportunity events |
35-
| `min_profit` | number | - | Minimum profit percentage for arbitrage events |
36-
| `api_key` | string | - | API key (alternative to header auth for browser `EventSource`) |
28+
| `channel` | string | `opportunities` | What to stream: `odds`, `opportunities`, or `all` |
29+
| `sport` | string | all | Filter by sport(s), comma-separated (e.g. `basketball`, `football`, `ice_hockey`) |
30+
| `sportsbook` | string | tier-allowed | Filter by sportsbook(s), comma-separated |
31+
| `league` | string | all | Filter by league(s), comma-separated |
32+
| `event` | string | all | Filter by event ID(s), comma-separated |
33+
| `market` | string | all | Filter by market type(s), comma-separated (e.g. `moneyline`, `point_spread`, `total_points`, `player_points`) |
34+
| `min_ev` | number | 2.0 | Minimum EV percentage for +EV opportunity events |
35+
| `min_profit` | number | 0.5 | Minimum profit percentage for arbitrage and low-hold events |
36+
| `api_key` | string | | API key (alternative to header auth for browser `EventSource`) |
3737

3838
### Channel Options
3939

4040
| Channel | Events Delivered | Use Case |
4141
|---------|-----------------|----------|
42-
| `odds` | `snapshot`, `odds:update`, `heartbeat` | Track odds movements |
43-
| `opportunities` | `snapshot`, `ev:detected`, `ev:expired`, `arb:detected`, `arb:expired`, `heartbeat` | Alert on +EV and arbitrage |
42+
| `odds` | `snapshot`, `odds:update`, `odds:removed`, `heartbeat` | Track odds movements |
43+
| `opportunities` | `snapshot`, `ev:detected/expired`, `arb:detected/expired`, `middles:detected/expired`, `low_hold:detected/expired`, `heartbeat` | Alert on opportunities |
4444
| `all` | All event types | Full real-time picture |
4545

46+
### Convenience Routes
47+
48+
| Route | Equivalent To |
49+
|-------|---------------|
50+
| `GET /api/v1/stream/odds` | `/api/v1/stream?channel=odds` |
51+
| `GET /api/v1/stream/opportunities` | `/api/v1/stream?channel=opportunities` |
52+
| `GET /api/v1/stream/events/:eventId` | `/api/v1/stream?channel=odds&event=:eventId` |
53+
4654
## SSE Event Types
4755

4856
### `connected`
@@ -51,23 +59,35 @@ Sent immediately when the stream is established.
5159

5260
```
5361
event: connected
54-
data: {"stream_id":"str_a1b2c3","filters":{"channel":"all","league":"nba"},"reconnected":false}
62+
data: {"stream_id":"stream_1704960637000","channel":"all","filters":{"sportsbook":null,"sport":["basketball"],"league":["nba"],"event":null,"market":null},"reconnected":false}
5563
```
5664

5765
| Field | Type | Description |
5866
|-------|------|-------------|
5967
| `stream_id` | string | Unique stream identifier |
68+
| `channel` | string | Echo of requested channel (`odds`, `opportunities`, or `all`) |
6069
| `filters` | object | Echo of active filters |
6170
| `reconnected` | boolean | `true` if this is a reconnection via `Last-Event-ID` |
71+
| `trial` | object \| undefined | Present if user is on a streaming trial. Contains `active`, `expires_at`, `remaining_hours`, `max_streams` |
6272

6373
### `snapshot`
6474

65-
Full data dump sent after `connected`. Contains all current odds or opportunities matching your filters.
75+
Full data dump sent after `connected`. Contains all current odds or opportunities matching your filters. Large datasets are chunked across multiple `snapshot` events (up to 1000 items each).
6676

6777
```
6878
event: snapshot
6979
id: evt_00001
70-
data: {"odds":[...],"opportunities":[...],"timestamp":"2026-01-26T02:10:37.846Z"}
80+
data: {"draftkings":[...],"timestamp":"2026-01-26T02:10:37.846Z"}
81+
```
82+
83+
### `snapshot:complete`
84+
85+
Signals all initial snapshots have been sent. Safe to hide loading states after receiving this.
86+
87+
```
88+
event: snapshot:complete
89+
id: evt_00005
90+
data: {"status":"ready","books":["draftkings","fanduel"],"total_odds":3200}
7191
```
7292

7393
### `odds:update`
@@ -117,7 +137,57 @@ A previously detected arbitrage opportunity is no longer available.
117137
```
118138
event: arb:expired
119139
id: evt_00046
120-
data: {"opportunity_id":"opp_a1b2c3","reason":"odds_moved","timestamp":"2026-01-26T02:10:39.500Z"}
140+
data: {"expired":["evt_abc123:moneyline:opp_a1b2c3"],"timestamp":"2026-01-26T02:10:39.500Z"}
141+
```
142+
143+
### `middles:detected`
144+
145+
A new middle opportunity has been found. Only sent on `opportunities` or `all` channels.
146+
147+
```
148+
event: middles:detected
149+
id: evt_00047
150+
data: [{"hash_id":"middle_abc123","game_id":"evt_abc123","eventName":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","marketType":"player_points","selection1":{"book":"draftkings","selection":"Over 22.5","odds":-110},"selection2":{"book":"fanduel","selection":"Under 23.5","odds":-105},"middleProbability":0.12,"expectedValue":3.5,"qualityScore":85}]
151+
```
152+
153+
### `middles:expired`
154+
155+
A previously detected middle opportunity is no longer available.
156+
157+
```
158+
event: middles:expired
159+
id: evt_00048
160+
data: {"expired":["middle_abc123"]}
161+
```
162+
163+
### `low_hold:detected`
164+
165+
A new low-hold opportunity has been found. Only sent on `opportunities` or `all` channels.
166+
167+
```
168+
event: low_hold:detected
169+
id: evt_00049
170+
data: [{"hash_id":"lowhold_abc123","hold_id":"32825:moneyline:0","hold_percentage":1.2,"normalized_market":"moneyline","game_id":"evt_abc123","sport":"basketball","league":"nba","home_team":"PHI 76ers","away_team":"PHO Suns","is_live":false,"all_books":["draftkings","fanduel"],"side1":{"selection":"PHO Suns","price":-108,"books":["draftkings"]},"side2":{"selection":"PHI 76ers","price":110,"books":["fanduel"]}}]
171+
```
172+
173+
### `low_hold:expired`
174+
175+
A previously detected low-hold opportunity is no longer available.
176+
177+
```
178+
event: low_hold:expired
179+
id: evt_00050
180+
data: {"expired":["lowhold_abc123"]}
181+
```
182+
183+
### `odds:removed`
184+
185+
Odds removed by a sportsbook (e.g. market taken down, event settled). Only sent on `odds` or `all` channels.
186+
187+
```
188+
event: odds:removed
189+
id: evt_00051
190+
data: {"source":"draftkings","ids":["odd_123","odd_456"],"count":2}
121191
```
122192

123193
### `heartbeat`
@@ -165,28 +235,49 @@ const eventSource = new EventSource(
165235
);
166236

167237
eventSource.addEventListener('connected', (e) => {
168-
const { stream_id, filters } = JSON.parse(e.data);
169-
console.log(`Stream ${stream_id} connected with filters:`, filters);
238+
const { stream_id, channel, filters } = JSON.parse(e.data);
239+
console.log(`Stream ${stream_id} connected (${channel}) with filters:`, filters);
170240
});
171241

172242
eventSource.addEventListener('snapshot', (e) => {
173-
const { odds, opportunities } = JSON.parse(e.data);
174-
console.log(`Snapshot: ${odds?.length ?? 0} odds, ${opportunities?.length ?? 0} opportunities`);
243+
const data = JSON.parse(e.data);
244+
console.log('Snapshot chunk received');
245+
});
246+
247+
eventSource.addEventListener('snapshot:complete', (e) => {
248+
const { books, total_odds } = JSON.parse(e.data);
249+
console.log(`Snapshot complete: ${total_odds} odds from ${books.join(', ')}`);
175250
});
176251

177252
eventSource.addEventListener('odds:update', (e) => {
178253
const update = JSON.parse(e.data);
179-
console.log(`${update.sportsbook}: odds updated for ${update.away_team} @ ${update.home_team}`);
254+
const books = Object.keys(update).filter(k => k !== 'timestamp');
255+
console.log(`Odds updated for: ${books.join(', ')}`);
256+
});
257+
258+
eventSource.addEventListener('odds:removed', (e) => {
259+
const { source, ids } = JSON.parse(e.data);
260+
console.log(`${source}: ${ids.length} odds removed`);
180261
});
181262

182263
eventSource.addEventListener('ev:detected', (e) => {
183-
const opp = JSON.parse(e.data);
184-
console.log(`+EV found: ${opp.selection} at ${opp.ev_percent}% EV`);
264+
const opps = JSON.parse(e.data);
265+
opps.forEach(opp => console.log(`+EV: ${opp.selection} at ${opp.ev_percentage}%`));
185266
});
186267

187268
eventSource.addEventListener('arb:detected', (e) => {
188-
const arb = JSON.parse(e.data);
189-
console.log(`Arb found: ${arb.profit_percent}% profit`);
269+
const arbs = JSON.parse(e.data);
270+
arbs.forEach(arb => console.log(`Arb: ${arb.percentage}% profit`));
271+
});
272+
273+
eventSource.addEventListener('middles:detected', (e) => {
274+
const middles = JSON.parse(e.data);
275+
middles.forEach(m => console.log(`Middle: ${m.eventName} — EV ${m.expectedValue}%`));
276+
});
277+
278+
eventSource.addEventListener('low_hold:detected', (e) => {
279+
const holds = JSON.parse(e.data);
280+
holds.forEach(h => console.log(`Low hold: ${h.hold_percentage}%`));
190281
});
191282

192283
eventSource.addEventListener('heartbeat', () => {
@@ -203,7 +294,7 @@ eventSource.onerror = () => {
203294
import EventSource from 'eventsource';
204295

205296
const es = new EventSource(
206-
'https://api.sharpapi.io/api/v1/stream?channel=all&league=nba',
297+
'https://api.sharpapi.io/api/v1/stream?channel=all&sport=basketball&league=nba',
207298
{ headers: { 'X-API-Key': 'YOUR_KEY' } }
208299
);
209300

@@ -214,32 +305,47 @@ es.addEventListener('connected', (e) => {
214305

215306
es.addEventListener('snapshot', (e) => {
216307
const data = JSON.parse(e.data);
217-
console.log(`Received snapshot: ${data.odds?.length ?? 0} odds`);
308+
console.log('Snapshot chunk received');
309+
});
310+
311+
es.addEventListener('snapshot:complete', (e) => {
312+
const { books, total_odds } = JSON.parse(e.data);
313+
console.log(`Ready: ${total_odds} odds from ${books.join(', ')}`);
218314
});
219315

220316
es.addEventListener('odds:update', (e) => {
221317
const update = JSON.parse(e.data);
222-
console.log(`${update.sportsbook}: ${update.markets.length} markets updated`);
318+
const books = Object.keys(update).filter(k => k !== 'timestamp');
319+
console.log(`Odds updated: ${books.join(', ')}`);
223320
});
224321

225-
es.addEventListener('ev:detected', (e) => {
226-
const opp = JSON.parse(e.data);
227-
console.log(`+EV: ${opp.selection} — ${opp.ev_percent}% EV (Kelly: ${opp.kelly_percent}%)`);
322+
es.addEventListener('odds:removed', (e) => {
323+
const { source, count } = JSON.parse(e.data);
324+
console.log(`${source}: ${count} odds removed`);
228325
});
229326

230-
es.addEventListener('arb:detected', (e) => {
231-
const arb = JSON.parse(e.data);
232-
console.log(`Arb: ${arb.profit_percent}% across ${arb.legs.length} legs`);
327+
es.addEventListener('ev:detected', (e) => {
328+
const opps = JSON.parse(e.data);
329+
opps.forEach(opp =>
330+
console.log(`+EV: ${opp.selection}${opp.ev_percentage}% EV`)
331+
);
233332
});
234333

235334
es.addEventListener('ev:expired', (e) => {
236-
const { opportunity_id, reason } = JSON.parse(e.data);
237-
console.log(`EV expired: ${opportunity_id} (${reason})`);
335+
const { expired } = JSON.parse(e.data);
336+
console.log(`EV expired: ${expired.length} opportunities`);
337+
});
338+
339+
es.addEventListener('arb:detected', (e) => {
340+
const arbs = JSON.parse(e.data);
341+
arbs.forEach(arb =>
342+
console.log(`Arb: ${arb.percentage}% across ${arb.legs.length} legs`)
343+
);
238344
});
239345

240346
es.addEventListener('arb:expired', (e) => {
241-
const { opportunity_id, reason } = JSON.parse(e.data);
242-
console.log(`Arb expired: ${opportunity_id} (${reason})`);
347+
const { expired } = JSON.parse(e.data);
348+
console.log(`Arb expired: ${expired.length} opportunities`);
243349
});
244350

245351
es.onerror = (err) => {
@@ -256,6 +362,7 @@ import json
256362
url = 'https://api.sharpapi.io/api/v1/stream'
257363
params = {
258364
'channel': 'all',
365+
'sport': 'basketball',
259366
'league': 'nba',
260367
}
261368
headers = {'X-API-Key': 'YOUR_KEY'}
@@ -267,30 +374,45 @@ for event in client.events():
267374
data = json.loads(event.data) if event.data else {}
268375

269376
if event.event == 'connected':
270-
print(f"Stream {data['stream_id']} connected")
377+
print(f"Stream {data['stream_id']} connected ({data['channel']})")
271378

272379
elif event.event == 'snapshot':
273-
odds = data.get('odds', [])
274-
opps = data.get('opportunities', [])
275-
print(f"Snapshot: {len(odds)} odds, {len(opps)} opportunities")
380+
print("Snapshot chunk received")
381+
382+
elif event.event == 'snapshot:complete':
383+
print(f"Ready: {data['total_odds']} odds from {', '.join(data['books'])}")
276384

277385
elif event.event == 'odds:update':
278-
print(f"{data['sportsbook']}: odds updated for {data['event_id']}")
386+
books = [k for k in data if k != 'timestamp']
387+
print(f"Odds updated: {', '.join(books)}")
388+
389+
elif event.event == 'odds:removed':
390+
print(f"{data['source']}: {data['count']} odds removed")
279391

280392
elif event.event == 'ev:detected':
281-
print(f"+EV: {data['selection']} at {data['ev_percent']}% EV")
393+
for opp in data:
394+
print(f"+EV: {opp['selection']} at {opp['ev_percentage']}%")
282395

283396
elif event.event == 'ev:expired':
284-
print(f"EV expired: {data['opportunity_id']}")
397+
print(f"EV expired: {len(data['expired'])} opportunities")
285398

286399
elif event.event == 'arb:detected':
287-
print(f"Arb: {data['profit_percent']}% profit")
400+
for arb in data:
401+
print(f"Arb: {arb['percentage']}% profit")
288402

289403
elif event.event == 'arb:expired':
290-
print(f"Arb expired: {data['opportunity_id']}")
404+
print(f"Arb expired: {len(data['expired'])} opportunities")
405+
406+
elif event.event == 'middles:detected':
407+
for m in data:
408+
print(f"Middle: {m.get('eventName', '?')} — EV {m.get('expectedValue', '?')}%")
409+
410+
elif event.event == 'low_hold:detected':
411+
for h in data:
412+
print(f"Low hold: {h['hold_percentage']}%")
291413

292414
elif event.event == 'heartbeat':
293-
print('Heartbeat received')
415+
pass # silent keepalive
294416
```
295417
</Tabs.Tab>
296418
</Tabs>
@@ -339,9 +461,13 @@ These close the connection. Handle them in `onerror`:
339461

340462
## Best Practices
341463

342-
1. **Use filters to reduce bandwidth** -- Pass `league`, `sportsbook`, and `channel` params to receive only what you need
343-
2. **Handle reconnection gracefully** -- `EventSource` auto-reconnects, but reset local state when you receive a new `snapshot` event
344-
3. **Process updates asynchronously** -- Do not block the event handler; queue updates for background processing
345-
4. **Monitor heartbeats** -- If no heartbeat arrives within 60 seconds, consider the connection stale and reconnect
346-
5. **Close unused streams** -- Each open stream counts against your concurrent limit
347-
6. **Use `Last-Event-ID`** -- Enables the server to replay missed events after a reconnection
464+
1. **Use the right channel** -- `channel=odds` for odds only, `channel=opportunities` for opportunities only, `channel=all` for everything
465+
2. **Use filters to reduce bandwidth** -- Pass `sport`, `league`, `sportsbook`, `market`, and `event` params to narrow data
466+
3. **Set thresholds** -- Use `min_ev` and `min_profit` to filter out low-value opportunities server-side
467+
4. **Wait for `snapshot:complete`** -- This signals all initial data has been sent. Hide loading states after receiving it
468+
5. **Handle `odds:removed`** -- Remove odds from local state when received to avoid showing stale data
469+
6. **Handle reconnection gracefully** -- `EventSource` auto-reconnects, but reset local state when you receive a new `snapshot` event
470+
7. **Process updates asynchronously** -- Do not block the event handler; queue updates for background processing
471+
8. **Monitor heartbeats** -- If no heartbeat arrives within 60 seconds, consider the connection stale and reconnect
472+
9. **Close unused streams** -- Each open stream counts against your concurrent limit
473+
10. **Use `Last-Event-ID`** -- Enables the server to replay missed events after a reconnection

0 commit comments

Comments
 (0)