

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.

<Callout type="tier" title="paid plan required">
  `/analysis/*` endpoints are on the paid tier (`x-requires-tier: paid`) and weigh 10× per
  call — a free-tier account gets a `403 TIER_REQUIRES_PAID`. Free-tier accounts can still
  compute hit rates manually from `/persons/{id}/game-log`. See the
  [hit-rate cookbook recipe](/docs/cookbook/hit-rate) for that path.
</Callout>

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 [#the-call]

```bash title="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"
```

<PlaygroundLink href="/docs/api">
  Run /analysis/player-prop in the playground →
</PlaygroundLink>

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-response]

The `data` object is a `PlayerPropCard`:

```json title="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:

| Field                           | What it tells you                                                                                         |
| ------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `hit_rate.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                         |
| `average`                       | Mean value across games that carried the stat (same as `averages.season`)                                 |
| `averages`                      | Windowed means: `season`, `last_5`, `last_10`, and the `home`/`away` split                                |
| `games`                         | Number of games included in the denominator                                                               |
| `game_log`                      | The raw per-game values, chronological — `last_5`/`last_10` are its tail                                  |
| `person.bio`                    | The canonical person profile resolved from the supplied `person_id`                                       |
| `context`                       | Best-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 [#a-20-line-python-script]

<CodeTabs>
  <CodeTab label="python">
    ```python title="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}")
    ```
  </CodeTab>

  <CodeTab label="node">
    ```javascript title="analyze.mjs"
    const KEY = process.env.STATSHAWK_KEY;
    const BASE = "https://api.statshawk.ai/v1";

    async function analyze(competition, player, stat, line) {
      const searchUrl = new URL(`${BASE}/persons`);
      searchUrl.searchParams.set("q", player);
      searchUrl.searchParams.set("limit", "1");
      const searchRes = await fetch(searchUrl, { headers: { "X-API-Key": KEY } });
      const person = (await searchRes.json()).data.items[0];

      const url = new URL(`${BASE}/analysis/player-prop`);
      url.searchParams.set("person_id", person.id);
      url.searchParams.set("stat", stat);
      url.searchParams.set("line", String(line));
      url.searchParams.set("competition", competition);
      const res = await fetch(url, { headers: { "X-API-Key": KEY } });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return (await res.json()).data;
    }

    const card = await analyze("nba", "Jayson Tatum", "pts", 27.5);
    const season = card.hit_rate;
    const recent = card.hit_rates.last_10;
    const verdict =
      recent.over_rate >= 0.6 && card.averages.last_5 > season.line ? "LEAN OVER" : "PASS";

    console.log(`${card.person.bio.full_name} | line ${season.line} pts`);
    console.log(
      `Season: ${(season.over_rate * 100).toFixed(0)}%  |  L10: ${(recent.over_rate * 100).toFixed(0)}%  |  L5 avg: ${card.averages.last_5.toFixed(1)}`,
    );
    console.log(`Verdict: ${verdict}`);
    ```
  </CodeTab>
</CodeTabs>

## Scanning a whole slate [#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:

```bash title="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.
