Skip to Content
API ReferenceSSE Stream

Unified Stream

GET /api/v1/stream — Real-time odds and opportunity updates via Server-Sent Events (SSE).

Requires WebSocket Add-on ($99/mo) on any paid tier, or Enterprise (included). Free tier does not support streaming.

Authentication

Pass your API key via header or query parameter:

# Header (recommended for server-side) curl -H "X-API-Key: sk_live_your_key" \ https://api.sharpapi.io/api/v1/stream # Query param (required for browser EventSource) https://api.sharpapi.io/api/v1/stream?api_key=sk_live_your_key

Query Parameters

ParameterTypeDefaultDescription
channelstringopportunitiesWhat to stream: odds, opportunities, or all
sportstringallFilter by sport(s), comma-separated (e.g. basketball, football, ice_hockey)
sportsbookstringtier-allowedFilter by sportsbook(s), comma-separated
leaguestringallFilter by league(s), comma-separated
eventstringallFilter by event ID(s), comma-separated
marketstringallFilter by market type(s), comma-separated (e.g. moneyline, point_spread, total_points, player_points)
min_evnumber2.0Minimum EV percentage for +EV opportunity events
min_profitnumber0.5Minimum profit percentage for arbitrage events only (does not apply to low-hold filtering)
statestringpaUS state code for generating sportsbook deep links in opportunity events (e.g., nj, ny, il)
api_keystringAPI key (alternative to header auth for browser EventSource)

Channel Options

ChannelEvents DeliveredUse Case
oddssnapshot, odds:update, odds:removed, heartbeatTrack odds movements
opportunitiessnapshot, ev:detected/expired, arb:detected/expired, middles:detected/expired, low_hold:detected/expired, heartbeatAlert on opportunities
allAll event typesFull real-time picture

Convenience Routes

RouteEquivalent To
GET /api/v1/stream/odds/api/v1/stream?channel=odds
GET /api/v1/stream/opportunities/api/v1/stream?channel=opportunities
GET /api/v1/stream/events/:eventId/api/v1/stream?channel=odds&event=:eventId

SSE Event Types

connected

Sent immediately when the stream is established.

event: connected data: {"stream_id":"stream_1704960637000","channel":"all","filters":{"sportsbook":null,"sport":["basketball"],"league":["nba"],"event":null,"market":null},"reconnected":false}
FieldTypeDescription
stream_idstringUnique stream identifier
channelstringEcho of requested channel (odds, opportunities, or all)
filtersobjectEcho of active filters
reconnectedbooleantrue if this is a reconnection via Last-Event-ID
trialobject | undefinedPresent if user is on a streaming trial. Contains active, expires_at, remaining_hours, max_streams

snapshot

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).

event: snapshot id: evt_00001 data: {"draftkings":[...],"timestamp":"2026-01-26T02:10:37.846Z"}

snapshot:complete

Signals all initial snapshots have been sent. Safe to hide loading states after receiving this.

event: snapshot:complete id: evt_00005 data: {"status":"ready","books":["draftkings","fanduel"],"total_odds":3200}

odds:update

Fired when odds change for a sportsbook. Only sent on odds or all channels.

event: odds:update id: evt_00042 data: {"sportsbook":"draftkings","sport":"nba","league":"nba","event_id":"evt_abc123","home_team":"PHI 76ers","away_team":"PHO Suns","markets":[{"market_type":"moneyline","selections":[{"name":"PHO Suns","odds":{"american":-155,"decimal":1.645,"probability":0.608}}]}],"timestamp":"2026-01-26T02:10:38.123Z"}

ev:detected

A new positive expected value opportunity has been found. Only sent on opportunities or all channels.

event: ev:detected id: evt_00043 data: [{"id":"a1b2c3d4e5f6","game_id":"nba_phosuns_phi76ers_2026-02-08","ev_percent":4.35,"odds_american":-105,"odds_decimal":1.952,"no_vig_odds":-101,"selection":"PHO Suns -3.5","market":"point_spread","line":-3.5,"sportsbook":"draftkings","game":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","home_team":"PHI 76ers","away_team":"PHO Suns","start_time":"2026-02-08T19:00:00.000Z","is_live":false,"confidence_score":72,"kelly_fraction":0.038,"book_count":4,"detected_at":"2026-02-08T18:47:20.000Z"}]

ev:expired

A previously detected +EV opportunity is no longer available.

event: ev:expired id: evt_00044 data: {"expired":["a1b2c3d4e5f6"],"timestamp":"2026-02-08T18:47:25.000Z"}

arb:detected

A new arbitrage opportunity has been found. Only sent on opportunities or all channels.

event: arb:detected id: evt_00045 data: [{"id":"61c501b83ce932d1","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","market_type":"moneyline","line":null,"profit_percent":2.8,"implied_total":97.2,"is_live":false,"legs":[{"sportsbook":"draftkings","selection":"PHO Suns","odds_american":150,"odds_decimal":2.5,"implied_probability":0.4,"stake_percent":41.4},{"sportsbook":"fanduel","selection":"PHI 76ers","odds_american":-130,"odds_decimal":1.769,"implied_probability":0.5652,"stake_percent":58.6}],"detected_at":"2026-02-08T18:47:21.000Z"}]

arb:expired

A previously detected arbitrage opportunity is no longer available.

event: arb:expired id: evt_00046 data: {"expired":["evt_abc123:moneyline:opp_a1b2c3"],"timestamp":"2026-01-26T02:10:39.500Z"}

middles:detected

A new middle opportunity has been found. Only sent on opportunities or all channels.

event: middles:detected id: evt_00047 data: [{"id":"middle_abc123","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","market_type":"player_points","side1":{"book":"draftkings","selection":"Over 22.5","line":22.5,"odds":{"american":-110,"decimal":1.909,"probability":0.5238,"fair_probability":0.51},"stake_percent":50,"odds_age_seconds":2.1,"deep_link":null},"side2":{"book":"fanduel","selection":"Under 23.5","line":23.5,"odds":{"american":-105,"decimal":1.952,"probability":0.5122,"fair_probability":0.49},"stake_percent":50,"odds_age_seconds":1.5,"deep_link":null},"middle_size":1,"middle_numbers":[23],"middle_probability":0.12,"expected_value":3.5,"quality_score":85,"detected_at":"2026-02-08T18:47:22.000Z"}]

middles:expired

A previously detected middle opportunity is no longer available.

event: middles:expired id: evt_00048 data: {"expired":["middle_abc123"]}

low_hold:detected

A new low-hold opportunity has been found. Only sent on opportunities or all channels.

event: low_hold:detected id: evt_00049 data: [{"id":"lowhold_abc123","event_id":"nba_phosuns_phi76ers_2026-02-08","event_name":"PHO Suns @ PHI 76ers","sport":"basketball","league":"nba","market_type":"moneyline","line":null,"home_team":"PHI 76ers","away_team":"PHO Suns","start_time":"2026-02-08T19:00:00.000Z","hold_percentage":1.2,"is_live":false,"all_books":["draftkings","fanduel"],"side1":{"selection":"PHO Suns","books":["draftkings"],"line":null,"odds":{"american":-108,"decimal":1.926,"implied_probability":0.5192,"fair_probability":0.5096},"deep_links":{"draftkings":"https://sportsbook.draftkings.com/event/..."}},"side2":{"selection":"PHI 76ers","books":["fanduel"],"line":null,"odds":{"american":110,"decimal":2.1,"implied_probability":0.4762,"fair_probability":0.4904},"deep_links":{"fanduel":"https://sportsbook.fanduel.com/event/..."}},"detected_at":"2026-02-08T18:47:22.000Z"}]

low_hold:expired

A previously detected low-hold opportunity is no longer available.

event: low_hold:expired id: evt_00050 data: {"expired":["lowhold_abc123"]}

odds:removed

Odds removed by a sportsbook (e.g. market taken down, event settled). Only sent on odds or all channels.

event: odds:removed id: evt_00051 data: {"source":"draftkings","ids":["odd_123","odd_456"],"count":2}

heartbeat

Keep-alive sent every 30 seconds. If you do not receive a heartbeat within 60 seconds, the connection may be stale.

event: heartbeat data: {"timestamp":"2026-01-26T02:11:07.846Z"}

error

Recoverable error on the stream. The connection remains open.

event: error data: {"code":"upstream_error","message":"Temporary issue fetching DraftKings data. Will retry."}

Reconnection

SSE supports automatic reconnection via the Last-Event-ID header. Each event includes an id field. When the client reconnects, the server delivers a fresh full snapshot — not a replay of individual missed events. This means your client receives a complete, up-to-date picture on every reconnect.

// Browsers handle this automatically with EventSource. // For custom clients, set the header on reconnect: const headers = { 'X-API-Key': 'YOUR_KEY', 'Last-Event-ID': 'evt_00042' };

On reconnect, clear your local state before processing the new snapshot. The connected event includes "reconnected": true so you can detect this. If you don’t clear state, stale odds from the previous session will mix with fresh data.

Browser EventSource handles Last-Event-ID and reconnection automatically. No extra code needed for the reconnect itself, but you must handle clearing state on the client side.

Code Examples

const eventSource = new EventSource( 'https://api.sharpapi.io/api/v1/stream?channel=all&league=nba&api_key=YOUR_KEY' ); eventSource.addEventListener('connected', (e) => { const { stream_id, channel, filters } = JSON.parse(e.data); console.log(`Stream ${stream_id} connected (${channel}) with filters:`, filters); }); eventSource.addEventListener('snapshot', (e) => { const data = JSON.parse(e.data); console.log('Snapshot chunk received'); }); eventSource.addEventListener('snapshot:complete', (e) => { const { books, total_odds } = JSON.parse(e.data); console.log(`Snapshot complete: ${total_odds} odds from ${books.join(', ')}`); }); eventSource.addEventListener('odds:update', (e) => { const update = JSON.parse(e.data); const books = Object.keys(update).filter(k => k !== 'timestamp'); console.log(`Odds updated for: ${books.join(', ')}`); }); eventSource.addEventListener('odds:removed', (e) => { const { source, ids } = JSON.parse(e.data); console.log(`${source}: ${ids.length} odds removed`); }); eventSource.addEventListener('ev:detected', (e) => { const opps = JSON.parse(e.data); opps.forEach(opp => console.log(`+EV: ${opp.selection} at ${opp.ev_percent}%`)); }); eventSource.addEventListener('arb:detected', (e) => { const arbs = JSON.parse(e.data); arbs.forEach(arb => console.log(`Arb: ${arb.profit_percent}% profit`)); }); eventSource.addEventListener('middles:detected', (e) => { const middles = JSON.parse(e.data); middles.forEach(m => console.log(`Middle: ${m.event_name} — EV ${m.expected_value}%`)); }); eventSource.addEventListener('low_hold:detected', (e) => { const holds = JSON.parse(e.data); holds.forEach(h => console.log(`Low hold: ${h.hold_percentage}%`)); }); eventSource.addEventListener('heartbeat', () => { console.log('Connection alive'); }); eventSource.onerror = () => { console.log('Connection lost, auto-reconnecting...'); };

Concurrent Stream Limits

Each open SSE connection counts as one stream against your limit.

PlanMax Concurrent Streams
WebSocket Add-on ($99/mo)10
EnterpriseCustom

Exceeding your stream limit returns a 429 error with code stream_limit_exceeded. Close unused streams before opening new ones.

Managing Streams

  • Each unique GET /api/v1/stream connection counts as one stream
  • Closing the HTTP connection (or calling eventSource.close()) frees the slot immediately
  • Use broader filters on fewer streams rather than many narrow streams
  • The connected event payload includes your stream_id for tracking

Error Handling

Stream-Level Errors

Errors sent as SSE events are recoverable — the connection stays open:

event: error data: {"code":"upstream_error","message":"Temporary issue fetching data. Will retry."}

Connection-Level Errors

These close the connection. Handle them in onerror:

Error CodeHTTP StatusDescriptionResolution
stream_limit_exceeded429Too many concurrent streamsClose unused streams
tier_restricted403Streaming not available on your tierAdd WebSocket add-on
unauthorized401Invalid API keyCheck your API key
invalid_request400Invalid filter parametersCheck query params

Best Practices

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