statshawk
Guides

Building a prop-analysis bot

paid

Call /analysis/player-prop with a person id, read the card, act on it.

The analysis endpoint returns a person-keyed prop card: the resolved person, the requested measure, the per-game values from their in-scope appearances, windowed averages, and hit rates against your line. You supply the person id, stat, line, and optional competition/season scope; the API does the math.

If you only have a name, resolve it first with /v1/persons?q=Jayson%20Tatum and pass the returned per_… id into the analysis call.

The call

call.sh
curl -H "X-API-Key: $STATSHAWK_KEY" \
  "https://api.statshawk.ai/v1/analysis/player-prop\
?person_id=per_3f9kq0v2&stat=pts&line=27.5&competition=nba"
Run /analysis/player-prop in the playground →

Scope and stat selection:

  • stat — the measure to analyse. The canonical form is phase-qualified (pitching.so, passing.yards) to disambiguate a measure that exists in more than one phase; a bare key (pts) works when it's unambiguous, and friendly aliases are accepted. An unknown stat returns 400 UNKNOWN_STAT listing the valid keys.
  • competition — slug (nba) or minted comp_… id. When you pass competition and omit season/edition/from/to, the window defaults to that competition's latest edition with at least one final contest — so in-season you get current-season numbers without hardcoding a year.
  • season, edition, from, to — supply any of these to opt out of the default edition window.
  • stage — by default the card only includes regular-season (plus unknown-stage) games, the same window season stats use. Pass stage=all to include every stage, playoffs and preseason included.
  • line — omit it if you only need the game log and averages; hit_rate and hit_rates are only built when a line is supplied.

The response

The data object is a PlayerPropCard:

PlayerPropCard
{
  "person": {
    "id": "per_3f9kq0v2",
    "bio": {
      "full_name": "Jayson Tatum",
      "display_name": "Jayson Tatum",
      "position": "SF"
    }
  },
  "stat": "pts",
  "game_log": [31, 24, 35, 29],
  "games": 72,
  "average": 28.1,
  "averages": {
    "season": 28.1,
    "last_5": 31.2,
    "last_10": 29.4,
    "home": 29.0,
    "away": 27.2
  },
  "hit_rate": {
    "line": 27.5,
    "hits": 44,
    "games": 72,
    "over_rate": 0.611
  },
  "hit_rates": {
    "season": { "line": 27.5, "hits": 44, "games": 72, "over_rate": 0.611 },
    "last_5": { "line": 27.5, "hits": 4, "games": 5, "over_rate": 0.8 },
    "last_10": { "line": 27.5, "hits": 7, "games": 10, "over_rate": 0.7 }
  },
  "context": null
}

Key fields:

FieldWhat it tells you
hit_rate.over_rateSeason hit rate over the supplied line (hits / games; a hit is a game that strictly cleared the line)
hit_rates.last_5 / .last_10The same hit rate over the trailing 5 / 10 games — where the recency signal lives
averageMean value across games that carried the stat (same as averages.season)
averagesWindowed means: season, last_5, last_10, and the home/away split
gamesNumber of games included in the denominator
game_logThe raw per-game values, chronological — last_5/last_10 are its tail
person.bioThe canonical person profile resolved from the supplied person_id
contextBest-effort sport-specific extras (e.g. pitcher rates) when cheaply available; often null

hit_rate and hit_rates are null when no line was passed.

A 20-line Python script

analyze.py
import os, requests

KEY = os.environ["STATSHAWK_KEY"]
BASE = "https://api.statshawk.ai/v1"

def analyze(competition: str, player: str, stat: str, line: float) -> dict:
    person = requests.get(
        f"{BASE}/persons",
        headers={"X-API-Key": KEY},
        params={"q": player, "limit": 1},
        timeout=10,
    ).json()["data"]["items"][0]

    r = requests.get(
        f"{BASE}/analysis/player-prop",
        headers={"X-API-Key": KEY},
        params={
            "person_id": person["id"],
            "stat": stat,
            "line": line,
            "competition": competition,
        },
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["data"]

card = analyze("nba", "Jayson Tatum", "pts", 27.5)

season = card["hit_rate"]
recent = card["hit_rates"]["last_10"]
verdict = "LEAN OVER" if recent["over_rate"] >= 0.6 and card["averages"]["last_5"] > season["line"] else "PASS"

print(f"{card['person']['bio']['full_name']} | line {season['line']} pts")
print(f"Season: {season['over_rate']:.0%}  |  L10: {recent['over_rate']:.0%}  |  L5 avg: {card['averages']['last_5']:.1f}")
print(f"Verdict: {verdict}")

Scanning a whole slate

If you're evaluating one line, player-prop is the tool. If you're scanning tonight's slate for the best candidates, don't loop it over every player — call /v1/analysis/stat-board once instead:

stat_board.sh
curl -H "X-API-Key: $STATSHAWK_KEY" \
  "https://api.statshawk.ai/v1/analysis/stat-board\
?competition=mlb&date=2026-07-07&stat=batting.h"

The board ranks the day's slate by over-rate over a recent-game window (window, default 20 games) and returns two lists: recommended (entries with at least min_games games in the window, default 10) and full_slate (everyone, up to limit). date is interpreted in the competition's local scoreboard timezone — US Eastern for MLB. Same 10× weight and paid tier as player-prop, but one call replaces dozens.

On this page