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.

01

Find tonight's top +EV plays for one event

Pro

Which prices on this event are mispriced relative to the sharp consensus?

GET /v1/sports/{sport}/events/{id}/ev

python
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}%")
02

Slate-wide +EV scan across every event tonight

Pro

Show every +EV play across the whole MLB slate, sorted by EV%.

GET /v1/sports/{sport}/events → /v1/sports/{sport}/events/{id}/ev

python
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}%")
03

Filter +EV plays to your books

Pro

I only have accounts at DraftKings and FanDuel — show only those.

GET /v1/sports/{sport}/events/{id}/ev

python
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.

04

Did my pick win, lose, or push?

Pro

Look up the resolution and the actual stat for one prop.

GET /v1/sports/{sport}/events/{id}/results

python
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)
05

Compute CLV for a placed bet

Pro

I 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/closing

python
closing = client.get_odds_closing(
    "baseball_mlb", event_id=5885, markets=["pitcher_strikeouts"]
)
# Single call returns the last pre-game snapshot per (book, market, outcome).
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
05b

Last 30 minutes of line movement

Pro

How did this line move in the half-hour before tip? I want to see sharp action only.

GET /v1/sports/{sport}/events/{id}/odds/history

python
# relative_from/relative_to are offsets from commence_time.
# changes_only=true collapses adjacent identical snapshots.
hist = client.get_odds_history(
    "baseball_mlb",
    event_id=5885,
    markets=["pitcher_strikeouts"],
    relative_from="-30m",
    relative_to="0",
    changes_only=True,
)
for book in hist["bookmakers"]:
    for m in book["markets"]:
        for o in m["outcomes"]:
            if o["description"] != "Bryan Woo" or o["name"] != "Over": continue
            for s in o["snapshots"]:
                print(f"{book['key']:<10} {s['recorded_at']}  "
                      f"{s['price']:+5}  line={s['point']}")
05c

Downsampled snapshots for a backtest

Pro

I want one snapshot per minute for the 3 hours before the game, not the raw 90s firehose.

GET /v1/sports/{sport}/events/{id}/odds/history

python
hist = client.get_odds_history(
    "baseball_mlb",
    event_id=5885,
    markets=["pitcher_strikeouts"],
    relative_from="-3h",
    relative_to="0",
    interval="1m",        # 30s | 1m | 5m | 15m | 30m | 1h
)
# Each bucket holds the LATEST snapshot in that minute. Stable spacing for
# moving averages / volatility windows / replay against your trade times.
06

Hit rate by market type

What % of Over plays on pitcher_strikeouts hit, league-wide last 30 days?

GET /v1/markets/hit-rates?days=30

python
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.

07

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

python
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 implementation

Reference implementation: proplineapi/propline-arb-finder.

08

Cross-book moneyline arbs

Two-way (h2h) game-line arbitrage scan.

GET /v1/sports/{sport}/odds?markets=h2h

python
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.

09

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

python
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:+}")
10

Spot reduced-juice props

Find pitcher_strikeouts O/U pairs where the combined juice is under 5%.

GET /v1/sports/{sport}/odds

python
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}%")

Period markets

Filter any odds endpoint to game-period markets (1st quarter, 1st half, 1st period, first-5-innings, etc.) with ?period=. Omit it and you get full-game only — exactly the same response shape as before. The-odds-api charges credit multiples for these as separate markets; PropLine returns them through the same endpoint with a single query param.

19

First-quarter NBA totals across every book

Which book has the highest Over on the 1Q total for this game?

GET /v1/sports/{sport}/events/{id}/odds?period=q1

python
odds = 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
)
for book in odds["bookmakers"]:
    for m in book["markets"]:
        # m["period"] == "q1"; full-game rows would be omitted entirely
        for o in m["outcomes"]:
            print(f"{book['key']:<12} {o['name']:<6} {o['point']}  {o['price']:+}")
20

MLB first-5-innings (F5) lines

What's the F5 moneyline, run-line, and total? F5 takes the bullpen out of the equation.

GET /v1/sports/{sport}/events/{id}/odds?period=f5

python
odds = client.get_odds(
    "baseball_mlb",
    event_id=5885,
    markets=["h2h", "spreads", "totals"],
    period="f5",
)
for book in odds["bookmakers"]:
    for m in book["markets"]:
        line = f"@ {m['outcomes'][0]['point']}" if m["outcomes"][0].get("point") is not None else ""
        prices = "  ".join(f"{o['name']} {o['price']:+}" for o in m["outcomes"])
        print(f"{book['key']:<12} {m['key']:<8} {line:<6} {prices}")
21

First-half vs full-game total: is the 2H priced sharp?

If 1H total < full_total/2, the market expects more scoring late — useful for live overs.

GET /v1/sports/{sport}/events/{id}/odds?period=h1 | (omitted)

python
# Two requests: full game (default) vs first half.
full = client.get_odds("basketball_nba", event_id=12345, markets=["totals"])
half = client.get_odds("basketball_nba", event_id=12345, markets=["totals"], period="h1")

def avg_total(payload):
    pts = [o["point"] for b in payload["bookmakers"]
           for m in b["markets"] for o in m["outcomes"]
           if o.get("point") is not None]
    return sum(pts) / len(pts) if pts else None

f, h = avg_total(full), avg_total(half)
if f and h:
    implied_2h = f - h
    print(f"Full {f:.1f}  |  1H {h:.1f}  |  implied 2H {implied_2h:.1f}  "
          f"({'2H over-weighted' if implied_2h > h else '1H over-weighted'})")
22

Multiple periods in one call

Pull q1 and q2 lines together for a half-by-half view.

GET /v1/sports/{sport}/events/{id}/odds?period=q1,q2

python
odds = client.get_odds(
    "basketball_nba", event_id=12345,
    markets=["totals"],
    period=["q1", "q2"],   # SDK accepts list or "q1,q2"
)
# Each market row carries a "period" field so you can bucket client-side.
for book in odds["bookmakers"]:
    for m in book["markets"]:
        for o in m["outcomes"]:
            print(f"{book['key']:<12} {m['period']:<3} {o['name']} {o['point']} {o['price']:+}")

Player prop history & hit rates

Backtest strategies player-by-player using past resolved props with full snapshot history.

11

Hit rate for one player on one market

Pro

Did Bryan Woo cover his strikeout Over in his last 10 starts?

GET /v1/sports/{sport}/players/{name}/history

python
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']}")
12

Filter player history to one book

Pro

Show only DraftKings lines for backtest fidelity.

GET /v1/sports/{sport}/players/{name}/history?bookmaker=

python
hist = client.get_player_history(
    "baseball_mlb", "Bryan Woo",
    market="pitcher_strikeouts",
    bookmaker="draftkings",
    limit=20,
)
12b

Hit-rate trends across every market for a player

Pro

What are Aaron Judge's L5 / L10 / L20 over rates, and is he on a streak?

GET /v1/sports/{sport}/players/{name}/trends

python
# One call returns aggregated splits for every market the
# player has graded history in — no per-game math on your end.
trends = client.get_player_trends("baseball_mlb", "Aaron Judge")
for m in trends["markets"]:
    l10 = m["last_10"]
    streak = m["current_streak"]
    if not l10:
        continue
    print(f"{m['market']:24} L10 {l10['over']}/{l10['over']+l10['under']} "
          f"({l10['over_pct']}% over)  avg {m['avg_actual']}  "
          f"line {m['recent_line']}  "
          f"streak {streak['result']}×{streak['length']}" if streak else "")

# Narrow to a single market with ?market=
hr = client.get_player_trends(
    "baseball_mlb", "Aaron Judge", market="batter_home_runs"
)

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.

13

Subscribe to line moves on one player

Streaming

Alert me whenever Aaron Judge's home-run prop ticks at any book.

POST /v1/webhooks

python
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.
14

Verify webhook signatures

Streaming

Confirm an incoming POST is really from PropLine and not a spoof.

X-PropLine-Signature header

python
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 401
15

Push line moves into Discord

Streaming

Skip the bot — embed the alerts natively in a Discord channel.

POST /v1/webhooks (format=discord)

python
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.

16

World Series winner odds, sorted by favorite

Who's the favorite to win the World Series?

GET /v1/sports/baseball_mlb/futures

python
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.

17

Historical backfill: resolved props + closing lines as CSV

Pro

Pull a CSV I can backtest a model against — with the pre-game closing line per row.

GET /v1/exports/resolved-props

bash
curl -s "https://api.prop-line.com/v1/exports/resolved-props?\
sport=baseball_mlb&market=pitcher_strikeouts&apiKey=YOUR_KEY" \
  -o mlb-strikeouts.csv

# Every row carries closing_price + closing_at (last line at/before
# first pitch) alongside the graded result — a complete CLV/backtest
# dataset, no second call to /odds/closing needed.

# Or in Python (streams to disk):
client.export_resolved_props(
    sport="baseball_mlb",
    market="pitcher_strikeouts",
    out_path="./mlb-strikeouts.csv",
)
18

Full line-movement history (open-to-close) as CSV

Pro

Give me every recorded line, every book, across the whole archive — not just the close.

GET /v1/exports/odds-history

bash
# Backfill-pass / Enterprise only. Page month-by-month — a full
# archive runs to gigabytes per sport.
curl -s "https://api.prop-line.com/v1/exports/odds-history?\
sport=baseball_mlb&since=2026-04-01T00:00:00Z&until=2026-05-01T00:00:00Z&apiKey=YOUR_KEY" \
  -o mlb-line-history-apr.csv

# One row per (outcome, snapshot): every price + line we recorded,
# per book, including period markets. No subscription tier can pull
# this in bulk — Pro/Streaming get per-event /odds/history only.

# Or in Python (streams to disk):
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",
)
19

Quick CSV preview before committing

What does the data look like? No auth required.

GET /v1/exports/sample

bash
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-sample

Note: 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.