statshawk
Guides

MLB box scores with typed stat lines

any plan

Find an MLB contest and read batting/pitching lines from the current boxscore API.

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

Start with the MLB edition and date you care about:

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

boxscore.sh
curl -H "X-API-Key: $STATSHAWK_KEY" \
  "https://api.statshawk.ai/v1/contests/{contest_id}/boxscore"
Run the contest boxscore endpoint in the playground →

The response

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

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

PhaseCommon measures
battingab, h, hr, rbi, bb, k, avg, obp, slg, ops
pitchingip, 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

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

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.

On this page