@@ -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```
5361event: 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```
6878event: snapshot
6979id: 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```
118138event: arb:expired
119139id: 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
167237eventSource .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
172242eventSource .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
177252eventSource .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
182263eventSource .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
187268eventSource .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
192283eventSource .addEventListener (' heartbeat' , () => {
@@ -203,7 +294,7 @@ eventSource.onerror = () => {
203294import EventSource from ' eventsource' ;
204295
205296const 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
215306es .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
220316es .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
235334es .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
240346es .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
245351es .onerror = (err ) => {
@@ -256,6 +362,7 @@ import json
256362url = ' https://api.sharpapi.io/api/v1/stream'
257363params = {
258364 ' channel' : ' all' ,
365+ ' sport' : ' basketball' ,
259366 ' league' : ' nba' ,
260367}
261368headers = {' 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