Skip to Content
SDKsPython

Python SDK

The official sharpapi Python package provides typed access to all SharpAPI endpoints with Pydantic models, SSE streaming, and full IDE autocomplete.

InstallationPermalink for this section

pip install sharpapi

With optional pandas support:

pip install sharpapi[pandas]

Requires Python 3.9+.

Quick StartPermalink for this section

from sharpapi import SharpAPI client = SharpAPI("sk_live_xxx") # Arbitrage opportunities arbs = client.arbitrage.get(min_profit=1.0, league="nba") for arb in arbs.data: print(f"{arb.profit_percent:.2f}% profit — {arb.event_name}") for leg in arb.legs: print(f" {leg.sportsbook}: {leg.selection} @ {leg.odds_american} ({leg.stake_percent:.1f}%)") # +EV opportunities evs = client.ev.get(min_ev=3.0, sport="basketball") for opp in evs.data: print(f"+{opp.ev_percentage:.1f}% EV on {opp.selection} @ {opp.sportsbook}") if opp.kelly_percent: print(f" Kelly: {opp.kelly_percent:.2f}%, Confidence: {opp.confidence_score}") # Best odds across sportsbooks odds = client.odds.best(league="nba", market="moneyline") for line in odds.data: print(f"{line.home_team} vs {line.away_team}: {line.selection} {line.odds_american}")

ResourcesPermalink for this section

OddsPermalink for this section

# Full snapshot with filters client.odds.get(sport="basketball", league="nba", limit=100) # Best odds per selection across books client.odds.best(league="nfl", market="moneyline") # Side-by-side comparison for an event client.odds.comparison(event_id="nba_celtics_lakers_2026-02-08_b3") # Batch lookup client.odds.batch(event_ids=["nba_celtics_lakers_2026-02-08_b3", "nfl_bills_chiefs_2026-02-17_b2"])

+EV Opportunities (Pro+)Permalink for this section

evs = client.ev.get( min_ev=2.0, sportsbook="draftkings", league="nba", sort="-ev", # Highest EV first max_odds_age=60, # Only fresh odds limit=50, ) for opp in evs.data: print(f"+{opp.ev_percentage:.1f}% on {opp.selection} @ {opp.sportsbook}") print(f" Fair probability: {opp.fair_probability}") print(f" Devig: {opp.devig_method} via {opp.sharp_book}") print(f" Kelly: {opp.kelly_percent:.2f}%") print(f" Confidence: {opp.confidence_score}/100")

Arbitrage (Hobby+)Permalink for this section

arbs = client.arbitrage.get( min_profit=0.5, sport="basketball", group="best", # One per event+market max_odds_age=30, # Only fresh odds ) for arb in arbs.data: if arb.possibly_stale: continue # Skip stale opportunities print(f"{arb.profit_percent:.2f}% — {arb.event_name}") if arb.game_state: gs = arb.game_state print(f" Live: {gs.period} {gs.clock} ({gs.score_home}-{gs.score_away})") for leg in arb.legs: print(f" {leg.sportsbook}: {leg.selection} @ {leg.odds_american} → stake {leg.stake_percent:.1f}%")

Middles (Pro+)Permalink for this section

middles = client.middles.get(sport="football", min_size=3.0, sort="quality") for mid in middles.data: print(f"{mid.event_name} — gap: {mid.middle_size} pts") if mid.side1 and mid.side2: print(f" {mid.side1.book}: {mid.side1.selection} {mid.side1.line} @ {mid.side1.odds.american}") print(f" {mid.side2.book}: {mid.side2.selection} +{mid.side2.line} @ {mid.side2.odds.american}") print(f" Hit probability: {mid.middle_probability:.1%}") print(f" Expected value: ${mid.expected_value:.2f}") print(f" Key numbers: {mid.key_numbers}")

Low HoldPermalink for this section

low_holds = client.low_hold.get(max_hold=2.0, sport="basketball") for lh in low_holds.data: print(f"{lh.event_name}: {lh.hold_percentage:.2f}% hold")

Reference DataPermalink for this section

client.sports.list() client.leagues.list(sport="basketball") client.sportsbooks.list() client.events.list(league="nba", live=True) client.events.search("Lakers")

AccountPermalink for this section

info = client.account.me() print(f"Tier: {info.key['tier']}") print(f"Rate limit: {info.limits.requests_per_minute} req/min") print(f"Features: EV={info.features.ev}, Arb={info.features.arbitrage}")

SSE StreamingPermalink for this section

Real-time streaming with handler-based or iterator-based patterns.

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 will produce incorrect data.

Handler-Based (Decorator Pattern)Permalink for this section

stream = client.stream.opportunities(league="nba", min_ev=3.0) @stream.on("ev:detected") def on_ev(data): # data is {"opportunities": [...], "count": N, "type": "ev"}. An item is new, # or an updated version of one already sent (same id) for opp in data["opportunities"]: if not opp.get("possibly_stale"): print(f"+EV: {opp['selection']} {opp['ev_percentage']}% @ {opp['sportsbook']}") @stream.on("arb:detected") def on_arb(data): for arb in data["opportunities"]: print(f"Arb: {arb['profit_percent']}% — {arb['event_name']}") @stream.on("snapshot:complete") def on_ready(data): print(f"Ready: snapshot complete at {data['timestamp']}") stream.connect() # Blocks, processing events

Iterator-BasedPermalink for this section

stream = client.stream.all(sport="basketball") for event_type, data in stream.iter_events(): if event_type == "ev:detected": for opp in data["opportunities"]: print(f"+EV: {opp['ev_percentage']}%") elif event_type == "arb:detected": for arb in data["opportunities"]: print(f"Arb: {arb['profit_percent']}%") elif event_type == "snapshot:complete": print("Stream ready")

Full State ManagementPermalink for this section

The all channel carries the same odds frames as the odds channel: snapshot chunks and odds:update deltas both hold their rows in an odds array, never keyed by sportsbook: every snapshot row, and any odds:update row sent in full, carries its sportsbook; a compact odds:update row does not, so read the book from the envelope’s book field. On all you additionally receive opportunity snapshot chunks — keyed ev, arbitrage, middles, low_hold — and, if your plan includes game state, gamestate:snapshot, which this example ignores. Every frame is listed in the Stream endpoint reference.

odds_map = {} # Keyed by odds line ID resuming = False # True between a resumed connect and the frame that settles it stream = client.stream.all(league="nba") @stream.on("connected") def on_connected(data): # "resumed": True (odds channel only) means the replay STARTED, not that it # finished — the server can still fall back to a full snapshot, so do not # clear here. Anything else: a full snapshot follows, so clear now. global resuming # rebinding a module-level name inside a handler needs `global` resuming = data.get("resumed") is True if not resuming: odds_map.clear() @stream.on("snapshot") def on_snapshot(data): global resuming if resuming: # The server abandoned the resume after the replayed frames: this # snapshot is the new baseline (snapshot:complete reports "full_resync"). odds_map.clear() resuming = False # Odds rows arrive under "odds", here and in odds:update; an opportunity # chunk carries "ev" / "arbitrage" / "middles" / "low_hold" instead and is skipped. for odds in data.get("odds", []): odds_map[odds["id"]] = odds @stream.on("snapshot:complete") def on_snapshot_complete(data): # A full resync matching no rows sends no chunks at all, so clear here too. global resuming if data.get("mode") == "full_resync" and resuming: odds_map.clear() resuming = False @stream.on("odds:update") def on_update(data): # DELTA — merge each row into the one you hold. A delta carries only the # fields that can change, so replacing the stored row would drop the rest. for delta in data.get("odds", []): row = odds_map.get(delta["id"]) if row is not None: row.update(delta) elif "sportsbook" in delta: # A line that opened after your snapshot arrives as a full row odds_map[delta["id"]] = delta @stream.on("odds:removed") def on_removed(data): for odds_id in data.get("ids", []): odds_map.pop(odds_id, None) stream.connect()

Stream ChannelsPermalink for this section

# Odds only client.stream.odds(league="nba") # Opportunities only (EV + arb + middles) client.stream.opportunities(min_ev=3.0) # Everything client.stream.all(sport="basketball") # Single event client.stream.event("nba_celtics_lakers_2026-02-08_b3")

Data QualityPermalink for this section

Every opportunity includes staleness metadata:

arbs = client.arbitrage.get() for arb in arbs.data: # Skip stale or suspicious opportunities if arb.possibly_stale: print(f"Stale ({arb.oldest_odds_age_seconds}s old)") continue if "LIVE_HIGH_PROFIT_SUSPICIOUS" in arb.warnings: print("Phantom arb — skipping") continue print(f"Actionable: {arb.profit_percent}%")

Rate LimitsPermalink for this section

Rate limit info is available after every request:

response = client.odds.get() rl = client.rate_limit print(f"{rl.remaining}/{rl.limit} requests remaining (tier: {rl.tier})")

Error HandlingPermalink for this section

from sharpapi import ( SharpAPI, AuthenticationError, TierRestrictedError, RateLimitedError, ) client = SharpAPI("sk_live_xxx") try: evs = client.ev.get() except AuthenticationError: print("Invalid API key") except TierRestrictedError as e: print(f"Upgrade to {e.required_tier} for this feature") except RateLimitedError as e: print(f"Rate limited — retry after {e.retry_after}s")

Odds Conversion UtilitiesPermalink for this section

from sharpapi import american_to_decimal, american_to_probability, decimal_to_american american_to_decimal(-110) # 1.909 american_to_decimal(150) # 2.5 american_to_probability(-110) # 0.524 decimal_to_american(2.5) # 150

Context ManagerPermalink for this section

with SharpAPI("sk_live_xxx") as client: arbs = client.arbitrage.get() # HTTP client automatically closed on exit

Type SafetyPermalink for this section

All responses use Pydantic models with full type hints:

from sharpapi import EVOpportunity, ArbitrageOpportunity # IDE autocomplete works on all fields arbs = client.arbitrage.get() arb: ArbitrageOpportunity = arbs.data[0] arb.profit_percent # float arb.legs[0].sportsbook # str arb.game_state.period # str | None
Last updated on