Python SDK
The official sharpapi Python package provides typed access to all SharpAPI endpoints with Pydantic models, SSE streaming, and full IDE autocomplete.
Installation
pip install sharpapiWith optional pandas support:
pip install sharpapi[pandas]Requires Python 3.9+.
Quick Start
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}")Resources
Odds
# 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+)
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+)
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+)
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 Hold
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 Data
client.sports.list()
client.leagues.list(sport="basketball")
client.sportsbooks.list()
client.events.list(league="nba", live=True)
client.events.search("Lakers")Account
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 Streaming
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)
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 eventsIterator-Based
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 Management
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 Channels
# 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 Quality
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 Limits
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 Handling
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 Utilities
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) # 150Context Manager
with SharpAPI("sk_live_xxx") as client:
arbs = client.arbitrage.get()
# HTTP client automatically closed on exitType Safety
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