Building a prop-analysis bot
paidCall /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
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"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 returns400 UNKNOWN_STATlisting the valid keys.competition: slug (nba) or mintedcomp_…id. When you passcompetitionand omitseason/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. Passstage=allto include every stage, playoffs and preseason included.line: omit it if you only need the game log and averages;hit_ratesis only built when a line is supplied.
The response
The data object is a PlayerPropCard:
{
"person": {
"id": "per_3f9kq0v2",
"bio": {
"full_name": "Jayson Tatum",
"display_name": "Jayson Tatum",
"position": "SF"
}
},
"stat": "pts",
"game_log_detail": [
{
"value": 31,
"contest_id": "cst_8m2vq4x1",
"kickoff": "2026-01-14T00:30:00Z",
"opponent": { "team_id": "team_9x1mq2v4", "name": "Miami Heat" },
"home": true
},
{
"value": 24,
"contest_id": "cst_7k1nq0v2",
"kickoff": "2026-01-16T02:00:00Z",
"opponent": { "team_id": "team_2b8kq0v7", "name": "Denver Nuggets" },
"home": false
}
],
"games": 72,
"averages": {
"season": 28.1,
"last_5": 31.2,
"last_10": 29.4,
"home": 29.0,
"away": 27.2,
"counts": { "season": 72, "last_5": 5, "last_10": 10, "home": 36, "away": 36 }
},
"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 },
"home": { "line": 27.5, "hits": 24, "games": 36, "over_rate": 0.667 },
"away": { "line": 27.5, "hits": 20, "games": 36, "over_rate": 0.556 }
},
"context": {
"sport": "basketball",
"rest": {
"back_to_back": { "games": 12, "average": 26.1 },
"rested": { "games": 60, "average": 28.5 }
}
}
}Key fields:
| Field | What it tells you |
|---|---|
hit_rates.season.over_rate | Season hit rate over the supplied line (hits / games; a hit is a game that strictly cleared the line) |
hit_rates.last_5 / .last_10 | The same hit rate over the trailing 5 / 10 games: where the recency signal lives |
hit_rates.home / .away | The venue split of the same hit rate, each with its own games denominator |
averages | Windowed means: season, last_5, last_10, and the home/away split |
averages.counts | The achieved game count behind each windowed average, e.g. early in a season last_5 may be backed by fewer than 5 games, so check the count before trusting the average |
games | Number of games included in the denominator |
game_log_detail | The chronological per-game log (last_5/last_10 are its tail): each entry carries the value plus the contest it came from: contest_id, kickoff, the named opponent, and the home side |
person.bio | The canonical person profile resolved from the supplied person_id |
context | Sport-tagged analysis context, when a registered provider produced it (see below); absent from the JSON otherwise, never null |
hit_rates is null when no line was passed.
Sport context
When the analyzed stat's sport and phase have a registered context provider, the card
carries a context object tagged with a sport discriminator:
- Baseball, batting stats:
platoon, the batter's per-game splits by the opposing starter's throwing hand (vs_left/vs_right, each{ games, average }), assembled from the matchup data behind the card's games. Served only when both sides have at least one game. - Basketball and hockey stats:
rest, back-to-back vs rested splits (back_to_back/rested, each{ games, average }). A back-to-back is a game whose kickoff is more than 12 and at most 36 elapsed hours after the previous one; longer gaps are rested, and gaps of 12 hours or less (plus the first in-scope game and games with no known kickoff) carry no rest signal and land in neither bucket. Served only when both buckets have at least one game.
A card whose sport/phase has no provider (football, soccer, baseball pitching, …) simply
omits the field: branch on its presence and the sport tag.
A 20-line Python script
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_rates"]["season"]
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:
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.