Skip to Content
StreamingOverview

Streaming Overview

Real-time odds and opportunity updates via SSE or WebSocket.

SharpAPI offers two streaming protocols: SSE (Server-Sent Events) over HTTP and WebSocket for bidirectional communication. Both deliver the same real-time data — choose based on your use case.

Why Streaming?Permalink for this section

With REST you see a change on your next poll. Streaming pushes changed rows to you as they happen.

ApproachUpdate deliveryBandwidthUse Case
REST pollingOn your next pollHigh (full payload each poll)Casual browsing, dashboards
SSE streamingPushed on changeLow (only deltas)Live betting, alerts, automated systems
WebSocketPushed on changeLow (only deltas)Bidirectional comms, dynamic filter updates

Key BenefitsPermalink for this section

  • No poll interval — Updates are pushed as soon as our pipeline detects a change, instead of waiting for your next request
  • Lower bandwidth — Only changed data is sent, not full snapshots on every poll
  • Two protocols — SSE for simplicity, WebSocket for bidirectional control
  • Automatic reconnection — SSE reconnects natively; WebSocket with simple retry logic

How SSE WorksPermalink for this section

Server-Sent Events is a W3C standard for server-to-client streaming over HTTP:

Client Server | | |--- GET /api/v1/stream ------->| | | |<-- event: connected ----------| Stream established |<-- event: snapshot -----------| Full current state |<-- event: odds:update --------| Delta update |<-- event: odds:update --------| Delta update |<-- event: ev:detected --------| Opportunity (new or updated) |<-- event: heartbeat ----------| Keep-alive (every 30s) | ... |

SSE automatically reconnects on disconnect, and the server uses Last-Event-ID to try to replay missed odds events. Resume is best-effort: when the server can’t replay (longer outages, server maintenance), it says so explicitly and re-baselines you with a fresh snapshot. See the Streaming Reliability Contract for the exact semantics your client must handle.

RequirementsPermalink for this section

TierStreaming Access
FreeNot available
Hobby + Add-on ($99/mo)1 stream (newer-wins displacement)
Pro + Add-on ($99/mo)1 stream (newer-wins displacement)
Sharp + Add-on ($99/mo)1 stream (newer-wins displacement)
EnterpriseIncluded (custom limits)

Newer-wins displacement: opening a stream beyond your cap closes the oldest one. One well-managed connection is sufficient for most use cases — see Single-Connection Patterns for techniques. Fleet deployments needing multiple simultaneous streams can request a higher per-key limit, a paid add-on on any paid tier, via sales.

Quick StartPermalink for this section

BrowserPermalink for this section

// Local odds map — snapshot fills it, deltas merge into it const oddsMap = new Map(); 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, resumed } = JSON.parse(e.data); // A full snapshot follows every connect except a resume (resumed === true, // `odds` channel only), which replays missed deltas into the state you hold. // A started resume can still fall back to a full snapshot; this short example // leaves that out — the Streaming Reliability Contract has the complete client. if (resumed !== true) oddsMap.clear(); console.log('Stream connected:', stream_id); }); eventSource.addEventListener('snapshot', (e) => { const { odds } = JSON.parse(e.data); if (!odds) return; // opportunity chunks carry `ev` / `arbitrage` / `middles` / `low_hold` instead for (const odd of odds) oddsMap.set(odd.id, odd); console.log('Snapshot chunk:', odds.length, 'odds'); }); eventSource.addEventListener('odds:update', (e) => { const { odds, book } = JSON.parse(e.data); // Delta only contains dynamic fields — merge into local state by ID for (const delta of odds) { const full = oddsMap.get(delta.id); if (full) Object.assign(full, delta); else oddsMap.set(delta.id, delta); } console.log(`${book}: ${odds.length} odds updated`); }); eventSource.addEventListener('ev:detected', (e) => { // {opportunities, count, type}: new opportunities, or updated versions of ones already sent (same `id`) const { opportunities: opps } = JSON.parse(e.data); opps.forEach(opp => console.log(`+EV: ${opp.selection} at ${opp.ev_percentage}% EV`)); }); eventSource.addEventListener('heartbeat', () => { console.log('Connection alive'); }); eventSource.onerror = () => { console.log('Connection lost, auto-reconnecting...'); };

Node.jsPermalink for this section

import EventSource from 'eventsource'; const oddsMap = new Map(); const es = new EventSource( 'https://api.sharpapi.io/api/v1/stream?channel=odds&league=nba', { headers: { 'X-API-Key': 'YOUR_KEY' } } ); es.addEventListener('snapshot', (e) => { const { odds } = JSON.parse(e.data); for (const odd of odds) oddsMap.set(odd.id, odd); console.log(`Received ${odds.length} initial odds`); }); es.addEventListener('connected', (e) => { const { resumed } = JSON.parse(e.data); if (resumed !== true) oddsMap.clear(); }); es.addEventListener('odds:update', (e) => { const { odds, book } = JSON.parse(e.data); // Merge compact deltas into local state for (const delta of odds) { const full = oddsMap.get(delta.id); if (full) Object.assign(full, delta); else oddsMap.set(delta.id, delta); } console.log(`${book}: ${odds.length} odds updated`); });

Track rows by id, not by a composite key.

The id field on every snapshot row and every odds:update delta is the stable identifier for that (event, sportsbook, market, selection) tuple — it does not change when the line moves. For asian-handicap / team-total / over-under markets, the line field updates in place under the same id, so building a composite key like (event_id, market_type, selection, line) makes your local map miss every line-change.

// ✗ Wrong — composite key breaks on line updates (e.g. asian_handicap -0.5 → -0.75 // produces a different composite key, and your tracking map loses the row) const key = `${o.event_id}|${o.market_type}|${o.selection}|${o.line ?? ''}` oddsMap.set(key, o) // ✓ Right — `id` is stable across line updates oddsMap.set(o.id, o)

This affects asian-handicap, team-total, over/under, and any market where the line can move. Moneyline / win-only markets have line=null so composite keys happen to work for them — but the bug only surfaces the moment your client subscribes to a line-bearing market.

PythonPermalink for this section

import sseclient import requests import json url = 'https://api.sharpapi.io/api/v1/stream' params = {'channel': 'all', 'league': 'nba'} headers = {'X-API-Key': 'YOUR_KEY'} odds_map: dict[str, dict] = {} response = requests.get(url, params=params, headers=headers, stream=True) client = sseclient.SSEClient(response) for event in client.events(): data = json.loads(event.data) if event.data else {} if event.event == 'connected': # A full snapshot follows every connect except a resume (resumed == True, # odds channel only), which replays deltas into your existing state. # A started resume can still fall back to a full snapshot; this short # example leaves that out — see the Streaming Reliability Contract. if data.get('resumed') is not True: odds_map.clear() print(f"Stream {data['stream_id']} connected") elif event.event == 'snapshot': for odd in data.get('odds', []): odds_map[odd['id']] = odd print(f"Snapshot: {data['count']} odds") elif event.event == 'odds:update': # Delta only has dynamic fields — merge by ID for delta in data.get('odds', []): existing = odds_map.get(delta['id']) if existing: existing.update(delta) else: odds_map[delta['id']] = delta print(f"{data['book']}: {data['count']} odds updated") elif event.event == 'ev:detected': # {opportunities, count, type}: new, or updated versions of ones already sent (same id) for opp in data['opportunities']: print(f"+EV: {opp['selection']} at {opp['ev_percentage']}%")

Event TypesPermalink for this section

EventDescription
connectedStream established, returns stream ID, active filters, and channels
snapshot / opportunities_snapshotFull odds/opportunities data dump (all fields)
snapshot:completeAll initial data has been sent
odds:updateCompact delta — only dynamic fields (id, odds_american, odds_decimal, odds_probability, line, is_live, timestamp). Merge by id into snapshot state. A row for an id not yet in your map arrives in full — store it.
odds:removedOdds removed by a sportsbook
ev:detectedNew +EV opportunity, or an updated version of one already sent (same id) — upsert by id
ev:expired+EV opportunity no longer available
arb:detectedNew arbitrage opportunity, or an updated version of one already sent (same id)
arb:expiredArbitrage opportunity no longer available
middles:detectedNew middle opportunity, or an updated version of one already sent (same id). See Middles Summary for aggregated stats
middles:expiredMiddle opportunity no longer available
low_hold:detectedNew low-hold opportunity, or an updated version of one already sent (same id)
low_hold:expiredLow-hold opportunity no longer available
heartbeatKeep-alive sent every 30 seconds — carries seq/global_seq so you can detect a frozen subscription (see reliability contract)
resync_requiredDeltas were skipped while your client consumed too slowly — refetch /odds or reconnect
displacedA newer connection on the same API key took the stream slot — this stream closes; do not auto-reconnect
errorRecoverable errors keep the connection open. Entitlement changes (tier_restricted, invalid_api_key) arrive as an error and then the stream closes — reconnect to re-authorize

SSE vs WebSocketPermalink for this section

FeatureSSEWebSocket
ProtocolHTTP (one-way)ws:// / wss:// (bidirectional)
EndpointGET /api/v1/streamwss://ws.sharpapi.io
Channel subscriptionsSet once via query paramsUpdate anytime via subscribe message
Filter updatesReconnect with new paramsSend subscribe message
ReconnectionAutomatic (Last-Event-ID)Manual (with backoff)
Resumable channels (best-effort)odds onlyodds, opportunities, gamestate, closing_line
Browser supportNative EventSourceNative WebSocket
Best forSimple consumers, SSRDynamic filters, two-way comms

Both protocols deliver the same event types and data payloads.

Full API ReferencePermalink for this section

SDKs with Streaming SupportPermalink for this section

  • TypeScript SDK — Built-in SSE client with type-safe event handlers
  • Python SDK — Handler-based and iterator-based streaming patterns

Example ProjectsPermalink for this section

Last updated on