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_percent:.1f}% EV on {opp.selection} @ {opp.sportsbook}")
if opp.kelly_fraction:
print(f" Kelly: {opp.kelly_fraction:.1%}, 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="evt_abc123")
# Batch lookup
client.odds.batch(event_ids=["evt_abc123", "evt_def456"])+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_percent:.1f}% on {opp.selection} @ {opp.sportsbook}")
print(f" Fair probability: {opp.fair_probability}")
print(f" Devig: {opp.devig_method} via {opp.devig_book}")
print(f" Kelly: {opp.kelly_fraction:.1%}")
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):
for opp in data:
if not opp.get("possibly_stale"):
print(f"+EV: {opp['selection']} {opp['ev_percent']}% @ {opp['sportsbook']}")
@stream.on("arb:detected")
def on_arb(data):
for arb in data:
print(f"Arb: {arb['profit_percent']}% — {arb['event_name']}")
@stream.on("snapshot:complete")
def on_ready(data):
print(f"Ready: {data['total_odds']} odds from {', '.join(data['books'])}")
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:
print(f"+EV: {opp['ev_percent']}%")
elif event_type == "arb:detected":
for arb in data:
print(f"Arb: {arb['profit_percent']}%")
elif event_type == "snapshot:complete":
print("Stream ready")Full State Management
odds_map = {} # Keyed by odds line ID
stream = client.stream.all(league="nba")
@stream.on("snapshot")
def on_snapshot(data):
for key, value in data.items():
if isinstance(value, list) and key not in ("ev", "arbitrage", "middles", "low_hold"):
for odds in value:
odds_map[odds["id"]] = odds
@stream.on("odds:update")
def on_update(data):
# DELTA — merge into local state
for book, odds_list in data.items():
if isinstance(odds_list, list):
for odds in odds_list:
odds_map[odds["id"]] = odds
@stream.on("odds:removed")
def on_removed(data):
for odds_id in data.get("ids", []):
odds_map.pop(odds_id, None)
@stream.on("connected")
def on_connected(data):
if data.get("reconnected"):
odds_map.clear() # Clear stale state on reconnect
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("evt_abc123")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 | NoneLast updated on