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_keyQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
channel | string | opportunities | What to stream: odds, opportunities, or all |
sport | string | all | Filter by sport(s), comma-separated (e.g. basketball, football, ice_hockey) |
sportsbook | string | tier-allowed | Filter by sportsbook(s), comma-separated |
league | string | all | Filter by league(s), comma-separated |
event | string | all | Filter by event ID(s), comma-separated |
market | string | all | Filter by market type(s), comma-separated (e.g. moneyline, point_spread, total_points, player_points) |
min_ev | number | 2.0 | Minimum EV percentage for +EV opportunity events |
min_profit | number | 0.5 | Minimum profit percentage for arbitrage events only (does not apply to low-hold filtering) |
state | string | pa | US state code for generating sportsbook deep links in opportunity events (e.g., nj, ny, il) |
api_key | string | — | API key (alternative to header auth for browser EventSource) |
Channel Options
| Channel | Events Delivered | Use Case |
|---|---|---|
odds | snapshot, odds:update, odds:removed, heartbeat | Track odds movements |
opportunities | snapshot, ev:detected/expired, arb:detected/expired, middles:detected/expired, low_hold:detected/expired, heartbeat | Alert on opportunities |
all | All event types | Full real-time picture |
Convenience Routes
| Route | Equivalent 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}| Field | Type | Description |
|---|---|---|
stream_id | string | Unique stream identifier |
channel | string | Echo of requested channel (odds, opportunities, or all) |
filters | object | Echo of active filters |
reconnected | boolean | true if this is a reconnection via Last-Event-ID |
trial | object | undefined | Present 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
Browser
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.
| Plan | Max Concurrent Streams |
|---|---|
| WebSocket Add-on ($99/mo) | 10 |
| Enterprise | Custom |
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/streamconnection 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
connectedevent payload includes yourstream_idfor 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 Code | HTTP Status | Description | Resolution |
|---|---|---|---|
stream_limit_exceeded | 429 | Too many concurrent streams | Close unused streams |
tier_restricted | 403 | Streaming not available on your tier | Add WebSocket add-on |
unauthorized | 401 | Invalid API key | Check your API key |
invalid_request | 400 | Invalid filter parameters | Check query params |
Best Practices
- Use the right channel —
channel=oddsfor odds only,channel=opportunitiesfor opportunities only,channel=allfor everything - Use filters to reduce bandwidth — Pass
sport,league,sportsbook,market, andeventparams to narrow data - Set thresholds — Use
min_evandmin_profitto filter out low-value opportunities server-side - Wait for
snapshot:complete— This signals all initial data has been sent. Hide loading states after receiving it - Handle
odds:removed— Remove odds from local state when received to avoid showing stale data - Handle reconnection gracefully —
EventSourceauto-reconnects, but reset local state when you receive a newsnapshotevent - Process updates asynchronously — Do not block the event handler; queue updates for background processing
- Monitor heartbeats — If no heartbeat arrives within 60 seconds, consider the connection stale and reconnect
- Close unused streams — Each open stream counts against your concurrent limit
- Use
Last-Event-ID— Enables the server to replay missed events after a reconnection