

The current REST surface exposes MLB game results through competition editions and contest
box scores. A boxscore returns one line per appearance, and each line has typed phase blocks
such as `batting` and `pitching`; there is no raw `stats` blob to unpack.

## 1. Find the contest [#1-find-the-contest]

Start with the MLB edition and date you care about:

```bash title="games.sh"
curl -H "X-API-Key: $STATSHAWK_KEY" \
  "https://api.statshawk.ai/v1/competitions/mlb/editions/2026/games?date=2026-04-15"
```

The response is a contest list; use the `id` from one `data.items[]` row as the
`contest_id`.

## 2. Read the boxscore [#2-read-the-boxscore]

```bash title="boxscore.sh"
curl -H "X-API-Key: $STATSHAWK_KEY" \
  "https://api.statshawk.ai/v1/contests/{contest_id}/boxscore"
```

<PlaygroundLink href="/docs/api">
  Run the contest boxscore endpoint in the playground →
</PlaygroundLink>

## The response [#the-response]

The `data` object is a `ContestBoxscore` — the contest's match summary, a `finalized`
flag, and one line per appearance:

```json title="ContestBoxscore"
{
  "contest": {
    "id": "cst_8m2vq4x1",
    "competition": "comp_2r9wq7k3",
    "edition": "edt_5t1nq8y6",
    "home_team": "team_4c8xk2m9",
    "away_team": "team_7p3jw5v0",
    "kickoff": "2026-04-15T02:10:00Z",
    "status": "Final",
    "score": { "home": 5, "away": 3 }
  },
  "finalized": true,
  "lines": [
    {
      "appearance": "apr_...",
      "person": "per_...",
      "team": "team_7p3jw5v0",
      "phases": [
        {
          "phase": "batting",
          "measures": {
            "ab": 4,
            "h": 2,
            "hr": 1,
            "rbi": 3,
            "ops": 1.250
          }
        }
      ]
    },
    {
      "appearance": "apr_...",
      "person": "per_...",
      "team": "team_4c8xk2m9",
      "phases": [
        {
          "phase": "pitching",
          "measures": {
            "ip": 6.0,
            "pitching_k": 8,
            "pitching_h": 4,
            "er": 2,
            "whip": 1.00
          }
        }
      ]
    }
  ]
}
```

Ids are opaque minted ids (`cst_…`, `comp_…`, `edt_…`, `team_…`, `per_…`, `apr_…`) —
resolve names via `/v1/teams/{team_id}` and `/v1/persons/{person_id}`, or hold onto the
names from the contest list you already fetched. `finalized` is `true` once the contest
status is `Final`; `status` is one of `Scheduled`, `InProgress`, `Final`, `Postponed`,
`Cancelled`, `Suspended`.

## Phase blocks [#phase-blocks]

| Phase      | Common measures                                                      |
| ---------- | -------------------------------------------------------------------- |
| `batting`  | `ab`, `h`, `hr`, `rbi`, `bb`, `k`, `avg`, `obp`, `slg`, `ops`        |
| `pitching` | `ip`, `pitching_k`, `pitching_h`, `pitching_bb`, `er`, `era`, `whip` |

Measures that collide between phases are prefixed on the pitching side: a pitcher's
strikeouts are `pitching_k` (batter strikeouts stay `k`), hits allowed are `pitching_h`,
walks issued are `pitching_bb`. A two-way player has both a `batting` and a `pitching`
block on the same line. Derived rates (`avg`, `era`, `whip`, …) are omitted when undefined
rather than fabricated as `0` — use `.get(...)` semantics, not direct key access.

## A Python script — pitcher strikeouts [#a-python-script--pitcher-strikeouts]

```python title="pitcher_strikeouts.py"
import os, requests

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

def first_game_id(date: str) -> str:
    r = requests.get(
        f"{BASE}/competitions/mlb/editions/2026/games",
        headers={"X-API-Key": KEY},
        params={"date": date},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["data"]["items"][0]["id"]

def pitching_lines(contest_id: str):
    r = requests.get(
        f"{BASE}/contests/{contest_id}/boxscore",
        headers={"X-API-Key": KEY},
        timeout=15,
    )
    r.raise_for_status()
    for line in r.json()["data"]["lines"]:
        for phase in line["phases"]:
            if phase["phase"] == "pitching":
                yield line["person"], phase["measures"]

contest_id = first_game_id("2026-04-15")
for person_id, measures in pitching_lines(contest_id):
    print(f"{person_id}: {measures.get('pitching_k', 0)} K, {measures.get('ip', 0)} IP")
```

## Going deeper: pitch-level data [#going-deeper-pitch-level-data]

The boxscore is the game-line summary. For Statcast-grade pitch tracking — every pitch's
type, velocity, and result, grouped by plate appearance — use
`/v1/contests/{contest_id}/play-by-play` (weight 5×). It takes a `detail` parameter
(`sparse`, `standard`, or `full`, default `full`) to trade payload size for field
coverage, and `pitcher_id` / `batter_id` filters to narrow to one player or a
head-to-head intersection.

This endpoint is available on any tier and reads the same normalized contest model as the
NBA, NHL, NFL, KBO, NCAA, and soccer surfaces.
