Skip to Content
SDKsTypeScript

TypeScript SDK

The official @sharp-api/client TypeScript SDK gives you typed access to every SharpAPI endpoint, with SSE streaming, Zod validation, and full IDE autocomplete out of the box.

Install the official SDK: npm install @sharp-api/clientGitHub 

SDK Quick StartPermalink for this section

import { SharpAPI } from '@sharp-api/client' const api = new SharpAPI('sk_live_...') // Get odds const { data: odds } = await api.odds.get({ league: 'nba' }) // Get +EV opportunities (Pro+) const { data: ev } = await api.ev.get({ min_ev: 3 }) // Get arbitrage opportunities (Hobby+) const { data: arbs } = await api.arbitrage.get({ min_profit: 1 }) // Get middles (Pro+) const { data: middles } = await api.middles.get({ league: 'nba' }) // SSE streaming (WebSocket add-on) const stream = api.stream.odds({ league: 'nba' }) stream.on('update', ({ data }) => console.log(data)) stream.connect() // WebSocket streaming (~100ms latency) const ws = api.stream.oddsWs({ sportsbook: ['draftkings'] }) ws.on('odds:update', ({ data, source }) => console.log(source, data)) ws.connect()

REST APIPermalink for this section

Use fetch to call any REST endpoint:

const API_URL = 'https://api.sharpapi.io/api/v1'; const API_KEY = 'YOUR_API_KEY'; async function sharpApi<T>(path: string, params?: Record<string, string>): Promise<T> { const url = new URL(`${API_URL}${path}`); if (params) { for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); } const res = await fetch(url, { headers: { 'X-API-Key': API_KEY } }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return res.json(); } // Examples const odds = await sharpApi('/odds', { sport: 'basketball', league: 'nba' }); const ev = await sharpApi('/opportunities/ev', { min_ev: '3.0' }); const arbs = await sharpApi('/opportunities/arbitrage', { min_profit: '1.0' });

SSE StreamingPermalink for this section

SSE streaming delivers real-time odds updates and opportunity alerts. This section covers how to build a correct client.

Critical: odds:update events are deltas — they only contain odds that changed. Your client must maintain local state and merge updates into it. Treating each event as a full snapshot is the #1 cause of incorrect data.

Complete TypeScript ClientPermalink for this section

const API_URL = 'https://api.sharpapi.io/api/v1'; const API_KEY = 'YOUR_API_KEY'; // ─── Types ──────────────────────────────────────────────────────────────── interface OddsLine { id: string; sportsbook: string; event_id: string; sport: string; league: string; home_team: string; away_team: string; market_type: string; selection: string; selection_type: string; odds_american: number; odds_decimal: number; probability: number; line?: number; event_start_time: string; is_live: boolean; timestamp: string; player_name?: string; // Player prop markets only stat_category?: string; // Player prop markets only } interface EVOpportunity { id: string; ev_percentage: number; odds_american: number; odds_decimal: number; selection: string; market: string; sportsbook: string; game: string; sport: string; league: string; is_live: boolean; confidence_score: number; kelly_percent: number | null; possibly_stale: boolean; oldest_odds_age_seconds: number | null; warnings: string[]; detected_at: string; } interface ArbOpportunity { id: string; event_name: string; sport: string; market_type: string; profit_percent: number; possibly_stale: boolean; oldest_odds_age_seconds: number | null; warnings: string[]; legs: Array<{ sportsbook: string; selection: string; odds_american: number; odds_decimal: number; stake_percent: number; }>; detected_at: string; } interface LowHoldOpportunity { id: string; event_id: string; event_name: string; sport: string; league: string; market_type: string; line: number | null; hold_percentage: number; side1: LowHoldSide; side2: LowHoldSide; side3: LowHoldSide | null; // 3-way markets (soccer, hockey) is_live: boolean; is_alternate_line: boolean; all_books: string[]; confidence: number; odds_age_seconds: number; possibly_stale: boolean; detected_at: string; } interface LowHoldSide { selection: string; books: string[]; line: number | null; odds: { american: number; decimal: number; implied_probability: number; fair_probability: number; }; deep_links: Record<string, string>; } // ─── State Management ───────────────────────────────────────────────────── // Keyed by odds line ID (e.g. "draftkings_33483153_moneyline_PHO") const oddsMap = new Map<string, OddsLine>(); const evMap = new Map<string, EVOpportunity>(); const arbMap = new Map<string, ArbOpportunity>(); const lowHoldMap = new Map<string, LowHoldOpportunity>(); let isReady = false; // ─── Connect ────────────────────────────────────────────────────────────── const url = new URL(`${API_URL}/stream`); url.searchParams.set('channel', 'all'); url.searchParams.set('league', 'nba'); url.searchParams.set('api_key', API_KEY); const eventSource = new EventSource(url.toString()); // ─── Connection lifecycle ───────────────────────────────────────────────── eventSource.addEventListener('connected', (e) => { const data = JSON.parse(e.data); console.log(`Connected: stream ${data.stream_id}`); // On reconnect, clear local state — server sends a fresh snapshot if (data.reconnected) { oddsMap.clear(); evMap.clear(); arbMap.clear(); lowHoldMap.clear(); isReady = false; } }); // ─── Initial snapshot (chunked) ─────────────────────────────────────────── eventSource.addEventListener('snapshot', (e) => { const data = JSON.parse(e.data); // Odds snapshots: keyed by sportsbook name // e.g. { "draftkings": [OddsLine, ...], "fanduel": [OddsLine, ...] } for (const [key, value] of Object.entries(data)) { if (Array.isArray(value) && !['ev', 'arbitrage', 'middles', 'low_hold'].includes(key)) { // Odds data — key is sportsbook name for (const odds of value as OddsLine[]) { oddsMap.set(odds.id, odds); } } } // Opportunity snapshots if (data.ev) { for (const opp of data.ev as EVOpportunity[]) { evMap.set(opp.id, opp); } } if (data.arbitrage) { for (const arb of data.arbitrage as ArbOpportunity[]) { arbMap.set(arb.id, arb); } } if (data.low_hold) { for (const lh of data.low_hold as LowHoldOpportunity[]) { lowHoldMap.set(lh.id, lh); } } }); eventSource.addEventListener('snapshot:complete', (e) => { const { books, total_odds } = JSON.parse(e.data); isReady = true; console.log(`Ready: ${total_odds} odds from ${books.join(', ')}`); console.log(`${evMap.size} EV, ${arbMap.size} arb, ${lowHoldMap.size} low-hold opportunities`); }); // ─── Real-time odds updates (DELTAS — merge into local state) ───────────── eventSource.addEventListener('odds:update', (e) => { const data = JSON.parse(e.data); for (const [book, odds] of Object.entries(data)) { if (!Array.isArray(odds)) continue; for (const line of odds as OddsLine[]) { oddsMap.set(line.id, line); // upsert } } }); // ─── Odds removed (DELETE from local state) ─────────────────────────────── eventSource.addEventListener('odds:removed', (e) => { const { ids } = JSON.parse(e.data) as { source: string; ids: string[]; count: number }; for (const id of ids) { oddsMap.delete(id); } }); // ─── Opportunity events ─────────────────────────────────────────────────── eventSource.addEventListener('ev:detected', (e) => { const opps = JSON.parse(e.data) as EVOpportunity[]; for (const opp of opps) { // Skip stale opportunities if (opp.possibly_stale) continue; evMap.set(opp.id, opp); console.log(`+EV: ${opp.selection} ${opp.ev_percentage}% on ${opp.sportsbook}`); } }); eventSource.addEventListener('ev:expired', (e) => { const { expired } = JSON.parse(e.data) as { expired: string[] }; for (const id of expired) { evMap.delete(id); } }); eventSource.addEventListener('arb:detected', (e) => { const arbs = JSON.parse(e.data) as ArbOpportunity[]; for (const arb of arbs) { if (arb.possibly_stale) continue; arbMap.set(arb.id, arb); console.log(`Arb: ${arb.profit_percent}% — ${arb.event_name}`); } }); eventSource.addEventListener('arb:expired', (e) => { const { expired } = JSON.parse(e.data) as { expired: string[] }; for (const id of expired) { arbMap.delete(id); } }); eventSource.addEventListener('low_hold:detected', (e) => { const opps = JSON.parse(e.data) as LowHoldOpportunity[]; for (const opp of opps) { if (opp.possibly_stale) continue; lowHoldMap.set(opp.id, opp); console.log(`Low Hold: ${opp.hold_percentage}% — ${opp.event_name} (${opp.market_type})`); } }); eventSource.addEventListener('low_hold:expired', (e) => { const { expired } = JSON.parse(e.data) as { expired: string[] }; for (const id of expired) { lowHoldMap.delete(id); } }); // ─── Health monitoring ──────────────────────────────────────────────────── let lastHeartbeat = Date.now(); eventSource.addEventListener('heartbeat', () => { lastHeartbeat = Date.now(); }); // Check for stale connections every 60 seconds setInterval(() => { if (Date.now() - lastHeartbeat > 60_000) { console.warn('No heartbeat for 60s — reconnecting'); eventSource.close(); // Re-create EventSource (browser will auto-reconnect, // but explicit close + reconnect resets state cleanly) } }, 60_000); // ─── Error handling ─────────────────────────────────────────────────────── eventSource.addEventListener('error', (e) => { const data = JSON.parse((e as MessageEvent).data); console.warn(`Stream error: ${data.code} — ${data.message}`); }); eventSource.onerror = () => { console.log('Connection lost, auto-reconnecting...'); };

Node.js with eventsource PackagePermalink for this section

For server-side usage, install the eventsource package:

npm install eventsource
import EventSource from 'eventsource'; const es = new EventSource( 'https://api.sharpapi.io/api/v1/stream?channel=all&league=nba', { headers: { 'X-API-Key': 'YOUR_KEY' } } ); // Same event handlers as browser — see above es.addEventListener('snapshot', (e) => { /* ... */ }); es.addEventListener('odds:update', (e) => { /* ... */ }); // etc.

Odds FormatPermalink for this section

All odds values are returned in American format as the primary representation, with decimal and implied probability included:

// Every OddsLine includes all three formats: { odds_american: -110, // American odds odds_decimal: 1.909, // Decimal odds probability: 0.524 // Implied probability (0-1) }

If you need to convert between formats yourself:

function americanToDecimal(american: number): number { return american > 0 ? american / 100 + 1 : 100 / Math.abs(american) + 1; } function americanToProbability(american: number): number { return american > 0 ? 100 / (american + 100) : Math.abs(american) / (Math.abs(american) + 100); }

Staleness MetadataPermalink for this section

EV, arbitrage, and low-hold opportunity responses include staleness information to help you filter out opportunities based on stale odds:

interface EVOpportunity { // ... other fields ... possibly_stale: boolean; // true if any underlying odds may be stale oldest_odds_age_seconds: number | null; // age of the oldest odds leg warnings: string[]; // e.g. ["POTENTIALLY_STALE_ODDS", "LIVE_STALE_ODDS"] } // Filter stale opportunities eventSource.addEventListener('ev:detected', (e) => { const opps = JSON.parse(e.data) as EVOpportunity[]; for (const opp of opps) { if (opp.possibly_stale) { console.log(`Skipping stale EV: ${opp.id}`); continue; } // Process valid opportunity } });

ReconnectionPermalink for this section

When the connection drops, EventSource auto-reconnects and sends the Last-Event-ID header. On reconnect, the server delivers a fresh full snapshot (not a replay of missed events). Your client should:

  1. Detect reconnection via data.reconnected === true in the connected event
  2. Clear local state (oddsMap.clear(), evMap.clear(), etc.)
  3. Wait for the new snapshot:complete before acting on data
eventSource.addEventListener('connected', (e) => { const data = JSON.parse(e.data); if (data.reconnected) { console.log('Reconnected — clearing stale state'); oddsMap.clear(); evMap.clear(); arbMap.clear(); lowHoldMap.clear(); isReady = false; } });

Common PitfallsPermalink for this section

These are the most common mistakes when building an SSE client. Getting any of them wrong can produce phantom arbitrage or incorrect EV calculations.

1. Treating odds:update as a full snapshotPermalink for this section

odds:update events only contain odds that changed since the last event. If you replace your entire local state with each update, you’ll only see 1-2 books at a time — making every market look like an arbitrage opportunity.

Fix: Always merge updates into your Map, never replace it.

2. Ignoring odds:removed eventsPermalink for this section

When a sportsbook pulls a line (market suspended, event settled), we send odds:removed with the IDs to delete. If you don’t handle this, stale odds accumulate and create phantom arbs between removed lines and fresh ones.

Fix: Delete odds from your Map when you receive odds:removed.

3. Computing before snapshot:completePermalink for this section

The initial snapshot is chunked across multiple snapshot events. If you start computing arbs or EV during snapshot loading, you’ll have an incomplete picture of available markets.

Fix: Set a flag on snapshot:complete and only start calculations after that.

4. Not clearing state on reconnectPermalink for this section

On reconnect, the server sends a fresh full snapshot. If you don’t clear your local state first, you’ll have duplicates and stale data mixed in.

Fix: Clear all Maps when you receive connected with reconnected: true.

5. Misinterpreting odds formatPermalink for this section

If you treat American odds (-110) as decimal odds, your calculations will produce wildly incorrect results. Our API always provides both formats — use odds_decimal for math.

6. Ignoring staleness warningsPermalink for this section

EV and arbitrage opportunities include possibly_stale and oldest_odds_age_seconds fields. Opportunities flagged as stale may be based on odds that are several minutes old and no longer actionable.

Fix: Check possibly_stale before acting on any opportunity.

Last updated on