cookbook
Recipes
Task-oriented examples for the PropLine API. Each recipe maps a real question (“find +EV plays for tonight,” “track CLV on a placed bet,” “scan for arbitrage”) to working code. Most recipes use the Python SDK; curl equivalents work the same way.
Install: pip install propline (Python) or npm install propline (Node). Get an API key at prop-line.com.
Cross-book +EV
No-vig fair lines from a sharp anchor (Pinnacle preferred, Bovada fallback), then EV% computed for every other book at the same line. PropLine derives this server-side; the-odds-api and most competitors leave it as a client-side exercise.
Find tonight's top +EV plays for one event
ProWhich prices on this event are mispriced relative to the sharp consensus?
GET /v1/sports/{sport}/events/{id}/ev
from propline import PropLine
client = PropLine(api_key="YOUR_KEY")
ev = client.get_event_ev("baseball_mlb", event_id=12345)
for line in ev["lines"]:
plus = sorted(
(o for o in line["outcomes"] if o["is_plus_ev"]),
key=lambda o: -o["ev_pct"],
)
if plus:
print(f"{line['description']} {line['point']}")
for o in plus[:3]:
print(f" {o['book_title']:<12} {o['name']:<8} {o['price']:+5} EV {o['ev_pct']:+.2f}%")Slate-wide +EV scan across every event tonight
ProShow every +EV play across the whole MLB slate, sorted by EV%.
GET /v1/sports/{sport}/events → /v1/sports/{sport}/events/{id}/ev
events = client.list_events("baseball_mlb")
plays = []
for e in events:
ev = client.get_event_ev("baseball_mlb", e["id"])
for line in ev["lines"]:
for o in line["outcomes"]:
if o["is_plus_ev"]:
plays.append((o["ev_pct"], e, line, o))
plays.sort(reverse=True, key=lambda p: p[0])
for ev_pct, e, line, o in plays[:25]:
print(f"{e['home_team']} vs {e['away_team']:<25} "
f"{line['description']:<25} {o['book_title']:<10} "
f"{o['name']:<8} {o['price']:+5} EV {ev_pct:+.2f}%")Filter +EV plays to your books
ProI only have accounts at DraftKings and FanDuel — show only those.
GET /v1/sports/{sport}/events/{id}/ev
MY_BOOKS = {"draftkings", "fanduel"}
ev = client.get_event_ev("baseball_mlb", event_id=12345)
for line in ev["lines"]:
keep = [o for o in line["outcomes"]
if o["book"] in MY_BOOKS and o["is_plus_ev"]]
for o in keep:
print(f"{line['description']:<30} {o['book']:<12} "
f"{o['name']:<8} {o['price']:+5} EV {o['ev_pct']:+.2f}%")Resolution & CLV tracking
PropLine grades every prop against real box scores after games close — strikeouts, hits, points, etc. Combined with snapshot history, this lets you compute closing-line value (CLV) for any bet you've placed.
Did my pick win, lose, or push?
ProLook up the resolution and the actual stat for one prop.
GET /v1/sports/{sport}/events/{id}/results
results = client.get_event_results("baseball_mlb", event_id=5885)
for market in results["markets"]:
if market["key"] != "pitcher_strikeouts": continue
for o in market["outcomes"]:
print(f"{o['description']} {o['name']} {market['line']}: "
f"{o['resolution']} (actual {o['actual_value']})")
# Bryan Woo Over 6.5: lost (actual 6.0)
# Bryan Woo Under 6.5: won (actual 6.0)Compute CLV for a placed bet
ProI bet Over 6.5 strikeouts at -110 two hours before first pitch. How did I do vs the closing line?
GET /v1/sports/{sport}/events/{id}/odds/history
history = client.get_odds_history(
"baseball_mlb", event_id=5885, markets=["pitcher_strikeouts"]
)
# Find the latest pre-game snapshot for the matching outcome
for book in history["bookmakers"]:
for m in book["markets"]:
for o in m["outcomes"]:
if o["description"] != "Bryan Woo" or o["name"] != "Over": continue
snaps = sorted(o["snapshots"], key=lambda s: s["recorded_at"])
close = snaps[-1] if snaps else None
if not close: continue
print(f"{book['key']}: closed at {close['price']} "
f"(book reported: {close['book_updated_at'] or 'n/a'})")
# Compare to your entry: -110 → closing -130 = +CLVHit rate by market type
What % of Over plays on pitcher_strikeouts hit, league-wide last 30 days?
GET /v1/markets/hit-rates?days=30
import httpx
r = httpx.get(
"https://api.prop-line.com/v1/markets/hit-rates",
params={"days": 30, "apiKey": "YOUR_KEY"},
)
for row in r.json()["markets"]:
if row["key"] == "pitcher_strikeouts":
print(f"Over hit rate (30d): {row['won']}/{row['total']} "
f"({100*row['won']/row['total']:.1f}%)")Arbitrage scanning
When one book offers Over +120 and another Under +110 on the same line, both sides cover. PropLine returns every book's price on the same canonical (event, market, line), so the scan is just a comparison.
Two-way arbitrage on Over/Under props
Find player props where Over and Under at different books guarantee a profit.
GET /v1/sports/{sport}/events/{id}/odds
def implied(price):
return 100 / (price + 100) if price > 0 else -price / (-price + 100)
odds = client.get_odds("baseball_mlb", event_id=12345,
markets=["pitcher_strikeouts"])
# Group by (player, line) across books
by_key = {}
for book in odds["bookmakers"]:
for m in book["markets"]:
for o in m["outcomes"]:
key = (o["description"], o["point"], o["name"])
by_key.setdefault(key, []).append((book["key"], o["price"]))
# Find Over/Under pairs where best Over + best Under < 1.0 implied
for (player, line, _), entries in by_key.items():
pass # see propline-arb-finder repo for full implementationReference implementation: proplineapi/propline-arb-finder.
Cross-book moneyline arbs
Two-way (h2h) game-line arbitrage scan.
GET /v1/sports/{sport}/odds?markets=h2h
odds = client.get_odds("baseball_mlb", markets="h2h")
for ev in odds:
by_team = {} # team_name -> list of (book, price)
for book in ev["bookmakers"]:
for o in book["markets"][0]["outcomes"]:
by_team.setdefault(o["name"], []).append((book["key"], o["price"]))
if len(by_team) != 2: continue
teamA, teamB = list(by_team.keys())
bestA = max(by_team[teamA], key=lambda x: x[1])
bestB = max(by_team[teamB], key=lambda x: x[1])
impl = implied(bestA[1]) + implied(bestB[1])
if impl < 1.0:
print(f"{ev['home_team']} vs {ev['away_team']}: "
f"{bestA[0]} {teamA} {bestA[1]:+} + "
f"{bestB[0]} {teamB} {bestB[1]:+} ({100*(1-impl):.2f}% edge)")Line shopping
Cross-book best prices for the same prop. Useful both for bettors picking the best venue and for builders surfacing savings to end users.
Best Over price across books for one player
Where can I get the best price on Bryan Woo Over 6.5 strikeouts?
GET /v1/sports/{sport}/events/{id}/odds
odds = client.get_odds("baseball_mlb", event_id=12345,
markets=["pitcher_strikeouts"])
best = []
for book in odds["bookmakers"]:
for m in book["markets"]:
for o in m["outcomes"]:
if o["description"] == "Bryan Woo" \
and o["name"] == "Over" and o["point"] == 6.5:
best.append((book["key"], o["price"]))
best.sort(key=lambda x: -x[1])
for book, price in best:
print(f"{book:<12} {price:+}")Spot reduced-juice props
Find pitcher_strikeouts O/U pairs where the combined juice is under 5%.
GET /v1/sports/{sport}/odds
odds = client.get_odds("baseball_mlb", markets=["pitcher_strikeouts"])
for ev in odds:
for book in ev["bookmakers"]:
for m in book["markets"]:
pairs = {} # (player, line) -> {Over: price, Under: price}
for o in m["outcomes"]:
k = (o["description"], o["point"])
pairs.setdefault(k, {})[o["name"]] = o["price"]
for (player, line), sides in pairs.items():
if "Over" in sides and "Under" in sides:
juice = (implied(sides["Over"])
+ implied(sides["Under"]) - 1) * 100
if juice < 5:
print(f"{book['key']:<12} {player:<25} {line}: "
f"O {sides['Over']:+} / U {sides['Under']:+} "
f" juice {juice:.2f}%")Player prop history & hit rates
Backtest strategies player-by-player using past resolved props with full snapshot history.
Hit rate for one player on one market
ProDid Bryan Woo cover his strikeout Over in his last 10 starts?
GET /v1/sports/{sport}/players/{name}/history
hist = client.get_player_history(
"baseball_mlb", "Bryan Woo",
market="pitcher_strikeouts", limit=10
)
won = sum(1 for e in hist["entries"] if e["over_result"] == "won")
print(f"Bryan Woo Over (last {len(hist['entries'])}): "
f"{won}/{len(hist['entries'])} ({100*won/len(hist['entries']):.0f}%)")
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']}")Filter player history to one book
ProShow only DraftKings lines for backtest fidelity.
GET /v1/sports/{sport}/players/{name}/history?bookmaker=
hist = client.get_player_history(
"baseball_mlb", "Bryan Woo",
market="pitcher_strikeouts",
bookmaker="draftkings",
limit=20,
)Webhooks (push)
Streaming tier subscribers get line-movement and resolution events pushed to their endpoint as soon as PropLine ingests them — HMAC-signed, with retry on failure.
Subscribe to line moves on one player
StreamingAlert me whenever Aaron Judge's home-run prop ticks at any book.
POST /v1/webhooks
client.create_webhook({
"url": "https://your-server.com/propline",
"events": ["line_movement"],
"filter_sport_key": "baseball_mlb",
"filter_player_name": "Aaron Judge",
"filter_market_key": "batter_home_runs",
"min_price_change_pct": 1.0,
})
# Returns the secret ONCE — store it for HMAC verification.Verify webhook signatures
StreamingConfirm an incoming POST is really from PropLine and not a spoof.
X-PropLine-Signature header
from propline import PropLine
# In your webhook handler, after reading the raw body bytes:
ok = PropLine.verify_signature(
secret=WEBHOOK_SECRET,
timestamp=request.headers["X-PropLine-Timestamp"],
body=request.body,
signature=request.headers["X-PropLine-Signature"],
)
if not ok:
return 401Push line moves into Discord
StreamingSkip the bot — embed the alerts natively in a Discord channel.
POST /v1/webhooks (format=discord)
client.create_webhook({
"url": "https://discord.com/api/webhooks/.../...",
"events": ["line_movement", "resolution"],
"format": "discord",
"filter_sport_key": "baseball_mlb",
"min_price_change_pct": 5.0,
})Walkthrough: /discord-webhooks.
Futures
Season-long markets — championship winner, MVP, division winner. Polled hourly.
World Series winner odds, sorted by favorite
Who's the favorite to win the World Series?
GET /v1/sports/baseball_mlb/futures
futures = client.get_futures("baseball_mlb")
for event in futures:
if "World Series" not in event["title"]: continue
for m in event["markets"]:
if m["key"] != "world_series_winner": continue
sorted_odds = sorted(m["outcomes"], key=lambda o: o["price"])
for o in sorted_odds[:10]:
print(f" {o['name']:<25} {o['price']:+5}")Bulk export & backtesting
Stream the full resolved-prop dataset as CSV — one row per (event, market, bookmaker, outcome) with line, price, resolution, and actual value. Pro tier; tier-gated lookback (90 days on Pro, 365 on Streaming, unlimited on Enterprise). The-odds-api doesn't offer this since they don't grade props.
Last 30 days of MLB pitcher strikeouts as CSV
ProPull a CSV I can backtest a model against.
GET /v1/exports/resolved-props
curl -s "https://api.prop-line.com/v1/exports/resolved-props?\
sport=baseball_mlb&market=pitcher_strikeouts&apiKey=YOUR_KEY" \
-o mlb-strikeouts.csv
# Or in Python (streams to disk):
client.export_resolved_props(
sport="baseball_mlb",
market="pitcher_strikeouts",
out_path="./mlb-strikeouts.csv",
)Quick CSV preview before committing
What does the data look like? No auth required.
GET /v1/exports/sample
curl -s https://api.prop-line.com/v1/exports/sample | head -3
# event_id,sport_key,commence_time,home_team,away_team,...,customer_token
# 5885,baseball_mlb,2026-04-19...,Seattle Mariners,...,public-sampleNote: every export carries a customer_token column tying it to your API key — see the redistribution terms.
Want one we don't cover?
Email hello@prop-line.com with the question and we'll add it (and likely answer you in code). Recipes are intentionally short and copy-paste-able; larger reference implementations live as standalone repos under github.com/proplineapi.