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.
| SSE | WebSocket | |
|---|---|---|
| URL | https://api.sharpapi.io/api/v1/stream | wss://ws.sharpapi.io |
| Direction | Server → Client | Bidirectional |
| Update filters | Reconnect required | Send subscribe message |
| Reconnection | Automatic (Last-Event-ID) | Manual (implement backoff) |
| Best for | Simple consumers, browsers | Interactive 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:
connected— Welcome message with your tier, features, and active channelssubscribed— Confirmation of active channels and filtersopportunities_snapshot— One per subscribed opportunity channel (ev, arbitrage, middles, low_hold)initial— Per-sportsbook odds snapshot (only ifoddschannel is subscribed)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 (requiresoddschannel)odds:removed— Odds removed by a sportsbook (requiresoddschannel)ev:detected/ev:expired— +EV opportunities (requiresevchannel)arb:detected/arb:expired— Arbitrage opportunities (requiresarbitragechannel)middles:detected/middles:expired— Middle opportunities (requiresmiddleschannel)low_hold:detected/low_hold:expired— Low-hold opportunities (requireslow_holdchannel)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
| Code | Meaning | Action |
|---|---|---|
1000 | Normal close | No action needed |
4001 | Bad or missing API key | Fix your API key |
4003 | No streaming access | Purchase WebSocket add-on |
4029 | Too many connections | Close unused connections first |
Full API Reference
For complete documentation including all message schemas, error handling, and multi-language examples, see the WebSocket API Reference.