Skip to Content
StreamingWebSocket

WebSocket Streaming

Real-time odds and opportunity updates via WebSocket at wss://ws.sharpapi.io.

For full API documentation including all message types, schemas, and close codes, see the WebSocket API Reference.

SSE vs WebSocket

SharpAPI offers two streaming protocols. Both deliver the same data at the same speed.

SSEWebSocket
URLhttps://api.sharpapi.io/api/v1/streamwss://ws.sharpapi.io
DirectionServer → ClientBidirectional
Update filtersReconnect requiredSend subscribe message
ReconnectionAutomatic (Last-Event-ID)Manual (implement backoff)
Best forSimple consumers, browsersInteractive apps, dashboards

Choose SSE if you set filters once and just want data to flow. Choose WebSocket if you need to change filters on the fly or prefer a bidirectional protocol.

Quick Start

1. Connect

Use the channels parameter to subscribe only to the data you need:

// Only EV opportunities + odds — no middles, low_hold, or arbitrage const ws = new WebSocket( 'wss://ws.sharpapi.io?api_key=YOUR_KEY&channels=ev,odds&sport=basketball&sportsbook=draftkings&league=nba' );

Available channels: ev, arbitrage, middles, low_hold, odds. Omit channels to receive everything your tier supports.

Available filters: sport, sportsbook, league, market, event_id — all comma-separated.

Threshold filters: min_ev (default 2.0), min_profit (default 0.5, applies to arbitrage only, not low-hold) — filter out low-value opportunities server-side.

2. Handle Messages

ws.onmessage = (event) => { const msg = JSON.parse(event.data); switch (msg.type) { case 'connected': console.log(msg.message); // "Welcome to SharpAPI real-time odds stream" break; case 'opportunities_snapshot': if (msg.ev) console.log('EV:', msg.ev); // EV opportunity snapshot break; case 'initial': console.log('Odds:', msg.data); // Per-sportsbook odds snapshot break; case 'snapshot:complete': console.log('All initial data loaded'); // Safe to hide loading state break; case 'odds:update': console.log(msg.source, msg.data); // Incremental odds update break; case 'ev:detected': console.log('+EV:', msg.data); // New +EV opportunities (Pro+) break; } };

3. Keep Alive

setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); } }, 25000);

4. Update Channels & Filters (No Reconnect Needed)

ws.send(JSON.stringify({ type: 'subscribe', channels: ['low_hold'], filters: { sports: ['football'], sportsbooks: ['fanduel', 'betmgm'], leagues: ['nfl'] } }));

Message Flow

On connect you receive messages in order:

  1. connected — Welcome message with your tier, features, and active channels
  2. subscribed — Confirmation of active channels and filters
  3. opportunities_snapshot — One per subscribed opportunity channel (ev, arbitrage, middles, low_hold)
  4. initial — Per-sportsbook odds snapshot (only if odds channel is subscribed)
  5. snapshot:complete — Signals all initial data has been sent

Snapshot chunking: Large snapshots are split across multiple frames to prevent backpressure. Odds snapshots are chunked at 1,000 items per frame, and opportunity snapshots at 300 items per frame. Wait for snapshot:complete before treating the initial data as fully loaded.

After that, you receive incremental updates for your subscribed channels:

  • odds:update — Odds changed for a sportsbook (requires odds channel)
  • odds:removed — Odds removed by a sportsbook (requires odds channel)
  • ev:detected / ev:expired — +EV opportunities (requires ev channel)
  • arb:detected / arb:expired — Arbitrage opportunities (requires arbitrage channel)
  • middles:detected / middles:expired — Middle opportunities (requires middles channel)
  • low_hold:detected / low_hold:expired — Low-hold opportunities (requires low_hold channel)
  • heartbeat — Server keepalive every 30 seconds

Reconnection

Unlike SSE, WebSocket does not auto-reconnect. Implement exponential backoff:

let reconnectDelay = 1000; let lastGlobalSeq = 0; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === 'connected') lastGlobalSeq = msg.global_seq; if (msg.global_seq) lastGlobalSeq = msg.global_seq; }; ws.onclose = (event) => { if (event.code === 1000) return; // intentional close setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, 30000); // Use resume + from_seq for gap-free reconnection (2-min replay buffer) connect({ resume: true, from_seq: lastGlobalSeq }); }, reconnectDelay); };

Reset reconnectDelay to 1000 on successful connection.

The server keeps a 2-minute replay buffer. Pass resume=true&from_seq=N on reconnect to skip the full snapshot and replay only missed events. For longer outages, omit these params for a full snapshot. See Reconnection with Replay for details.

Close Codes

CodeMeaningAction
1000Normal closeNo action needed
4001Bad or missing API keyFix your API key
4003No streaming accessPurchase WebSocket add-on
4029Too many connectionsClose unused connections first

Full API Reference

For complete documentation including all message schemas, error handling, and multi-language examples, see the WebSocket API Reference.

Last updated on