Skip to main content

Python SDK for the PropLine player props betting odds API

Project description

PropLine Python SDK

Official Python client for the PropLine player props API — real-time betting odds from Bovada, DraftKings, FanDuel, Pinnacle, Unibet, and PrizePicks across MLB, NBA, NHL, soccer, UFC, and more.

Installation

pip install propline

Quick Start

from propline import PropLine

client = PropLine("your_api_key")

# List available sports
sports = client.get_sports()
# [{"key": "baseball_mlb", "title": "MLB", "active": True}, ...]

# Get today's NBA games
events = client.get_events("basketball_nba")
for event in events:
    print(f"{event['away_team']} @ {event['home_team']}")

# Get player props for a game
odds = client.get_odds("basketball_nba", event_id=events[0]["id"],
    markets=["player_points", "player_rebounds", "player_assists"])

for bookmaker in odds["bookmakers"]:
    for market in bookmaker["markets"]:
        for outcome in market["outcomes"]:
            print(f"{outcome['description']} {outcome['name']} "
                  f"{outcome['point']} @ {outcome['price']}")

Get Your API Key

  1. Go to prop-line.com
  2. Enter your email
  3. Get your API key instantly — 500 requests/day, no credit card required

Available Sports

Key Sport
baseball_mlb MLB
basketball_nba NBA
basketball_ncaab College Basketball
football_ncaaf College Football
golf Golf
tennis Tennis
hockey_nhl NHL
football_nfl NFL
soccer_epl EPL
soccer_la_liga La Liga
soccer_serie_a Serie A
soccer_bundesliga Bundesliga
soccer_ligue_1 Ligue 1
soccer_mls MLS
mma_ufc UFC
boxing Boxing

Bookmakers

Every odds response returns a bookmakers array so you can compare lines across books in a single request — iterate the array to line-shop.

Key Book Coverage
bovada Bovada All 19 sports — game lines + full player props
draftkings DraftKings MLB, NBA, NHL, 6 soccer leagues — game lines + player props
fanduel FanDuel MLB, NBA, NHL, 6 soccer leagues — game lines + player props
pinnacle Pinnacle MLB (game lines + props), NBA/NHL/soccer (game lines, goalie saves)
unibet Unibet MLB/NBA/NHL + 6 soccer leagues — game lines; NBA + NHL + soccer player props (points, rebounds, assists, threes, steals, blocks, PRA, shots on goal, goalscorer, cards, BTTS, total corners)
prizepicks PrizePicks (DFS) MLB, NBA, WNBA, NHL, tennis, UFC, soccer — player props only; synthetic +100/+100 even-money pricing since DFS payouts scale with parlay correct-count, not per-pick odds. Each outcome carries dfs_odds_type (standard = the market line, goblin = easier/lower-payout, demon = harder/higher-payout). Filter to standard for the market line; goblin/demon arrive as their own per-line markets (e.g. Points (demon 27.5))
underdog Underdog Fantasy (DFS) MLB, NBA, NHL, tennis, UFC, 9 soccer leagues — player props with real two-way American prices and a payout_multiplier on boosted/discounted picks (None = standard 1.0 pick; e.g. 1.5 boost / 0.75 discount). Filter out non-null multipliers when comparing DFS lines to sportsbook consensus
from propline import PropLine, Bookmaker

client = PropLine("your_api_key")

odds = client.get_odds("baseball_mlb", event_id=events[0]["id"],
    markets=["pitcher_strikeouts"])

# Filter to a specific book
for bk in odds["bookmakers"]:
    if bk["key"] == Bookmaker.DRAFTKINGS:
        ...

# Or iterate all books
for bk in odds["bookmakers"]:
    print(f"\n{bk['title']}")
    for market in bk["markets"]:
        for o in market["outcomes"]:
            print(f"  {o['description']} {o['name']} {o['point']}: {o['price']}")
# Bovada
#   Zack Wheeler Over 6.5: -130
# DraftKings
#   Zack Wheeler Over 6.5: -125
# FanDuel
#   Zack Wheeler Over 6.5: -135

Available Markets

MLB

pitcher_strikeouts, pitcher_outs, pitcher_earned_runs, pitcher_hits_allowed, batter_hits, batter_home_runs, batter_rbis, batter_total_bases, batter_stolen_bases, batter_walks, batter_singles, batter_doubles, batter_runs, batter_2plus_hits, batter_2plus_home_runs, batter_2plus_rbis, batter_3plus_rbis

NBA

player_points, player_rebounds, player_assists, player_threes, player_steals, player_blocks, player_turnovers, player_points_rebounds, player_points_assists, player_rebounds_assists, player_points_rebounds_assists, player_double_double, player_triple_double

NHL

player_goals, player_first_goal, player_goals_2plus, player_goals_3plus, player_shots_on_goal, player_points_1plus, player_points_2plus, player_points_3plus, goalie_saves, player_blocked_shots

Soccer (EPL, La Liga, Serie A, Bundesliga, Ligue 1, MLS)

anytime_goal_scorer, first_goal_scorer, 2plus_goals, goal_or_assist, player_assists, player_2plus_assists, player_cards, both_teams_to_score, double_chance, draw_no_bet, correct_score, total_corners, total_cards

UFC / Boxing

h2h, total_rounds, fight_distance, round_betting

Game Lines (all sports)

h2h, spreads, totals (includes alt lines and team totals)

Examples

Get MLB pitcher strikeout props

from propline import PropLine

client = PropLine("your_api_key")

events = client.get_events("baseball_mlb")
for event in events:
    odds = client.get_odds("baseball_mlb", event_id=event["id"],
        markets=["pitcher_strikeouts"])

    print(f"\n{event['away_team']} @ {event['home_team']}")
    for bk in odds["bookmakers"]:
        for mkt in bk["markets"]:
            for o in mkt["outcomes"]:
                if o["point"]:
                    print(f"  {o['description']} {o['name']} {o['point']}: {o['price']}")

Filter to game-period markets

Every odds endpoint accepts a period= kwarg to scope results to first-quarter / first-half / first-period / first-N-innings markets. Omit it for full-game markets — the default behavior is unchanged.

# First-quarter NBA totals
q1 = client.get_odds(
    "basketball_nba", event_id=12345,
    markets=["totals"],
    period="q1",   # q1|q2|q3|q4 | h1|h2 | p1|p2|p3 | i1..i9 | f3|f5|f7
)

# Multiple periods in one call — pass a list or a comma-separated string
both = client.get_odds(
    "basketball_nba", event_id=12345,
    markets=["totals"],
    period=["q1", "q2"],
)

# Pass period="all" to include every period alongside the full-game row.

Every response row carries a period field so you can bucket client-side. Coverage today: Bovada / DraftKings / FanDuel / Pinnacle on NBA / NHL / MLB / soccer. Football period markets land at NFL preseason (August 2026). The same period= kwarg works on get_odds_history() and get_odds_closing() too.

Get game scores

scores = client.get_scores("baseball_mlb")
for game in scores:
    if game["status"] == "final":
        print(f"{game['away_team']} {game['away_score']}, "
              f"{game['home_team']} {game['home_score']}")

Get game context — pitchers, umpire, weather (free)

ctx = client.get_context("baseball_mlb", event_id=37464)
print(f"{ctx['away_probable_pitcher']} ({ctx['away_probable_pitcher_hand']}) @ "
      f"{ctx['home_probable_pitcher']} ({ctx['home_probable_pitcher_hand']})")
print(f"Umpire: {ctx['home_plate_umpire']}  Lineup set: {ctx['lineup_confirmed']}")
if ctx["weather"]:
    w = ctx["weather"]
    print(f"{w['temperature_f']}F, wind {w['wind_speed_mph']}mph {w['wind_direction']}, {w['conditions']}")

The conditions a prop settles under. For MLB: probable starting pitchers and their throwing hand (home_probable_pitcher_hand / away_probable_pitcher_hand, "L"/"R"/"S" — platoon-split context for every batter prop), a confirmed-lineup flag, the home-plate umpire, and first-pitch weather at outdoor / open-roof venues. For NFL & NCAAF: the venue and kickoff weather (pitcher/umpire/lineup fields are None for football). The same block is embedded in get_results(), so every graded prop carries its conditions — unique to PropLine. Free tier. Raises on 404 when no context is on file for the event yet.

Get line movement & steam (Hobby+)

mv = client.get_movement("baseball_mlb", event_id=37464)
for s in mv["steam"]:
    print(f"{s['name']} {s['consensus_direction']} "
          f"({s['books_moved']}/{s['books_quoting']} books, score {s['steam_score']})")

Line movement derived from our snapshot tick history. Per (book, market, outcome): opening line, latest line, implied-probability + point shift, direction. The steam array flags outcomes multiple books moved the same direction — the sharp-money signal across every book we poll. Unique to PropLine. Hobby+ full; free tier redacted.

Get resolution coverage summary (free)

s = client.get_resolution_summary(days=30)
print(f"{s['total_graded']:,} props graded across "
      f"{s['sports_covered']} sports in {s['days']}d")
for row in s["by_sport"][:5]:
    print(f"  {row['title']}: {row['graded']:,} ({row['events']} games)")

Get resolved prop outcomes (Pro only)

results = client.get_results("baseball_mlb", event_id=16,
    markets=["pitcher_strikeouts", "batter_hits"])

print(f"{results['away_team']} {results['away_score']}, "
      f"{results['home_team']} {results['home_score']}")

for market in results["markets"]:
    for outcome in market["outcomes"]:
        print(f"{outcome['description']} {outcome['name']} "
              f"{outcome['point']}: {outcome['resolution']} "
              f"(actual: {outcome['actual_value']})")
# Output: "Tarik Skubal (DET) Over 6.5: won (actual: 7.0)"

Get historical line movement (Hobby+)

history = client.get_odds_history("baseball_mlb", event_id=16,
    markets=["pitcher_strikeouts"])

for book in history["bookmakers"]:
    for market in book["markets"]:
        for outcome in market["outcomes"]:
            print(f"\n[{book['key']}] {outcome['description']}:")
            for snap in outcome["snapshots"]:
                print(f"  {snap['recorded_at']}: {snap['price']} @ {snap['point']}"
                      f" (book reported: {snap.get('book_updated_at') or 'n/a'})")

Each snapshot carries up to three change-detection signals: recorded_at (when our scraper saw the odds), book_updated_at (when the book itself reports the price was last set — Bovada today), and book_version (per-market monotonic counter — Pinnacle today). The gap between recorded_at and book_updated_at is per-book scraper latency; deltas in book_version between two snapshots tell you how many distinct market updates the book recorded between them, even when the visible price didn't change. See https://prop-line.com/docs#timestamps for the full semantic.

Period-historical query params

Combine any of these to scope, downsample, and de-noise:

# Just the last 30 minutes of moves before tip — and only the moments
# when the line actually changed.
moves = client.get_odds_history(
    "baseball_mlb", event_id=16,
    markets=["pitcher_strikeouts"],
    relative_from="-30m",
    relative_to="0",
    changes_only=True,
)

# One snapshot per minute for the 3 hours before commence — stable
# spacing for backtests / moving averages.
ts = client.get_odds_history(
    "baseball_mlb", event_id=16,
    markets=["pitcher_strikeouts"],
    relative_from="-3h",
    relative_to="0",
    interval="1m",   # 30s | 1m | 5m | 15m | 30m | 1h
)
  • from / to: absolute ISO timestamps (from_ in Python — from is reserved).
  • relative_from / relative_to: offsets relative to commence_time. Forms: -3h, -30m, -90s, 0. Mutually exclusive with the absolute counterpart.
  • interval: downsample to one snapshot per bucket; latest snapshot in each bucket wins.
  • changes_only: drop adjacent snapshots whose (price, point) match the previous one. Opening line is always kept.

Get closing line / CLV (Hobby+)

One call returns the last snapshot per (book, market, outcome) at or before commence_time — the canonical closing line for CLV tracking.

closing = client.get_odds_closing(
    "baseball_mlb", event_id=5885,
    markets=["pitcher_strikeouts"],
)

for book in closing["bookmakers"]:
    for m in book["markets"]:
        for o in m["outcomes"]:
            if o["description"] != "Bryan Woo" or o["name"] != "Over":
                continue
            print(f"{book['key']}: closed at {o['price']} ({o['closing_at']})")
            # Compare to your entry: -110 → closing -130 = +CLV

Get player prop history (Pro full, Free redacted)

# "Did Bryan Woo go over/under his last 10 strikeout props?"
hist = client.get_player_history("baseball_mlb", "Bryan Woo",
    market="pitcher_strikeouts", limit=10)

for e in hist["entries"]:
    print(f"{e['commence_time'][:10]} {e['bookmaker_title']}: "
          f"line {e['line']}, actual {e['actual_value']} "
          f"-> Over {e['over_result']}, Under {e['under_result']}")
# Output: "2026-04-19 DraftKings: line 6.5, actual 6.0 -> Over lost, Under won"

Get player hit-rate trends (Pro full, Free redacted)

# "How often has Aaron Judge gone over his total bases line lately?"
# Rolling Over/Under splits over the last 5/10/20/50 graded games,
# plus current streak and most-recent line/actual. Omit `market` for
# trends across every market the player has graded games in. Pass
# `dfs_odds_type="standard"|"goblin"|"demon"` to compute the trend
# against that PrizePicks flavor's line only.
trends = client.get_player_trends("baseball_mlb", "Aaron Judge",
    market="batter_total_bases")

for m in trends["markets"]:
    l10 = m["last_10"]
    streak = m["current_streak"]
    print(f"{m['market']}: line {m['recent_line']}, avg {m['avg_actual']}, "
          f"L10 {l10['over']}-{l10['under']} ({l10['over_pct']}% over), "
          f"streak {streak['length']} {streak['result']}")
# Output: "batter_total_bases: line 1.5, avg 2.02, L10 3-7 (30.0% over), streak 2 under"

Cross-book +EV (Pro)

# Find +EV plays on a single event. Pinnacle anchors the no-vig fair
# line; every other book's price gets an EV%, with +EV plays floated
# to the top of each line group.
ev = client.get_event_ev("baseball_mlb", 12345,
    markets=["pitcher_strikeouts", "batter_hits"])

for line in ev["lines"]:
    plus = [o for o in line["outcomes"] if o["is_plus_ev"]]
    if plus:
        print(f"\n{line['market_key']} {line['description']} "
              f"line={line['point']} fair={line['fair_source']}")
        for o in plus:
            print(f"  {o['book_title']:11s} {o['name']:6s} "
                  f"{o['price']:+5d}  ev=+{o['ev_pct']}%")

Bulk CSV export of resolved props (Pro)

# Save every resolved MLB strikeout prop since April 1st to disk.
client.export_resolved_props(
    sport="baseball_mlb",
    market="pitcher_strikeouts",
    since="2026-04-01T00:00:00Z",
    out_path="./mlb-strikeouts.csv",
)

# Or parse in memory with pandas for analysis.
import io
import pandas as pd
data = client.export_resolved_props(sport="baseball_mlb")
df = pd.read_csv(io.BytesIO(data))
hit_rate = (df.query("outcome_name == 'Over' and resolution == 'won'").shape[0]
            / df.query("outcome_name == 'Over'").shape[0])
print(f"Over hit rate across all MLB markets: {hit_rate:.1%}")

Full line-movement history (Historical Backfill / Enterprise)

# Every recorded snapshot (price + line, per book) — not just the close.
# The raw tick history no subscription tier can bulk-pull; exclusive to
# the one-time Historical Backfill pass and Enterprise. Page month by
# month — a full archive runs to gigabytes per sport.
client.export_odds_history(
    sport="baseball_mlb",
    since="2026-04-01T00:00:00Z",
    until="2026-05-01T00:00:00Z",
    out_path="./mlb-line-history-apr.csv",
)

Webhooks (Streaming tier)

The Streaming tier ($79/mo) pushes line_movement and resolution events to your URL in real time, with HMAC-SHA256 signing and automatic retries.

Register a subscription

wh = client.create_webhook(
    url="https://example.com/hooks/propline",
    filter_sport_key="baseball_mlb",
    filter_market_key="pitcher_strikeouts",
    min_price_change_pct=2.0,  # only fire on shifts of 2%+ (or any point change)
)

# Store wh["secret"] — this is the ONLY time it's returned.
SECRET = wh["secret"]
print(f"webhook id: {wh['id']}")

Verify incoming deliveries

Each POST carries these headers:

Header Purpose
X-PropLine-Event line_movement, resolution, or test
X-PropLine-Timestamp Unix seconds
X-PropLine-Signature HMAC-SHA256 over f"{timestamp}." + body
X-PropLine-Delivery Stable delivery id (use for idempotency)
from propline import PropLine

# In a FastAPI/Flask handler:
ok = PropLine.verify_signature(
    secret=SECRET,
    timestamp=headers["X-PropLine-Timestamp"],
    body=raw_body_bytes,
    signature=headers["X-PropLine-Signature"],
)
if not ok:
    return 401

Line-movement payload

{
  "event_type": "line_movement",
  "sport_key": "baseball_mlb",
  "event": {"id": 5070, "home_team": "Seattle Mariners", "away_team": "Texas Rangers", ...},
  "market_key": "totals",
  "player_name": null,
  "outcome_name": "Over",
  "previous": {"price_american": -750, "point": 7.0},
  "current":  {"price_american": -300, "point": 7.5},
  "price_change_pct": 60.0,
  "timestamp": "2026-04-18T03:49:00Z"
}

Resolution payload

{
  "event_type": "resolution",
  "sport_key": "baseball_mlb",
  "event": {"id": 16, "home_score": 4, "away_score": 2, "status": "final", ...},
  "market_key": "pitcher_strikeouts",
  "player_name": "Tarik Skubal (DET)",
  "outcome_name": "Over",
  "point": 6.5,
  "resolution": "won",
  "actual_value": 9.0,
  "resolved_at": "2026-04-18T06:14:22Z"
}

Manage subscriptions

for wh in client.list_webhooks():
    print(wh["id"], wh["url"], "active" if wh["active"] else "paused")

client.update_webhook(wh_id, min_price_change_pct=5.0)  # change a filter
client.test_webhook(wh_id)                              # queue a test payload
client.list_webhook_deliveries(wh_id, limit=50)         # last 50 attempts
client.delete_webhook(wh_id)                            # cascades deliveries

Error Handling

from propline import PropLine, AuthError, RateLimitError, PropLineError

client = PropLine("your_api_key")

try:
    odds = client.get_odds("baseball_mlb", event_id=1)
except AuthError:
    print("Invalid API key")
except RateLimitError:
    print("Daily limit exceeded — upgrade at prop-line.com/#pricing")
except PropLineError as e:
    print(f"API error: {e.status_code}{e.detail}")

Links

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

propline-0.25.0.tar.gz (29.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

propline-0.25.0-py3-none-any.whl (23.6 kB view details)

Uploaded Python 3

File details

Details for the file propline-0.25.0.tar.gz.

File metadata

  • Download URL: propline-0.25.0.tar.gz
  • Upload date:
  • Size: 29.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for propline-0.25.0.tar.gz
Algorithm Hash digest
SHA256 f01ab27f5846ff7e6b9b9beceb24732e5f617276b050354588ff5c82bee779e0
MD5 28c59d793ce0f66aaab431a01730c79a
BLAKE2b-256 4a7a375cd7a068f42ce6d24fc3ab3c4b8ad814cdcaa56ffbccbd578b072bdb81

See more details on using hashes here.

Provenance

The following attestation bundles were made for propline-0.25.0.tar.gz:

Publisher: publish.yml on proplineapi/propline-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file propline-0.25.0-py3-none-any.whl.

File metadata

  • Download URL: propline-0.25.0-py3-none-any.whl
  • Upload date:
  • Size: 23.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for propline-0.25.0-py3-none-any.whl
Algorithm Hash digest
SHA256 905fada4b894f0ea0cf9326c6e820a892737f9a82f1f8e17adc5546745870e76
MD5 24c5858bc32171c68cd1281c0dcfbd11
BLAKE2b-256 e431cc7c54d8a2d345299d1e237650d7eddb31447a952b976325b1d2aa1f12db

See more details on using hashes here.

Provenance

The following attestation bundles were made for propline-0.25.0-py3-none-any.whl:

Publisher: publish.yml on proplineapi/propline-python

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page