---
title: "Player stats for a season + league"
method: GET
path: "/v1/players/{id}/stats"
tags: ["players"]
---

# Player stats for a season + league

`GET /v1/players/{id}/stats`

## Request

- Base URL: `https://api.bigballsdata.com`
- URL: `https://api.bigballsdata.com/v1/players/{id}/stats`
- Auth: one of:
  - API key in header `x-api-key`
  - HTTP bearer

**Baseball responses carry a `level` field. Read it before summing anything.**

- `level: "match"`: `batting`/`pitching` are totals folded from real
  per-game box scores. 2026 onward.
- `level: "season"`: no per-game box scores exist for the seasons
  asked for. The numbers are full-season aggregates and are returned in
  `seasons[]`, each entry additionally marked `level: "season"`.
  `batting`/`pitching` are `null`.
- `level: "mixed"`: both are present. `batting`/`pitching` (per-game
  derived) and `seasons[]` (season aggregates) cover **disjoint**
  periods; adding them together double-counts nothing but conflates two
  different grains. Treat them separately.

A season aggregate has no home/away dimension, so `?split=home|away`
returns no `seasons[]` entries rather than full-season totals that
ignore the filter. Fields a source cannot supply (e.g. pitch counts,
which season-level history does not carry) are **omitted**, never
returned as `0`.

## Path parameters

- `id` string, uuid, required

## Query parameters

- `sport` 'football' | 'basketball' | 'baseball' | 'ice_hockey' | 'cricket' | 'mma' | 'boxing' | 'formula1' | 'american_football', required
- `season` string
- `league` string
- `split` 'home' | 'away' | 'all'

## Response `200`

ok. `{ data, meta }`. `data` is POLYMORPHIC by sport and by what we hold; see the branches below. Declared no `content` at all until 2026-09-25.

- object: The standard response wrapper. Only `data` is guaranteed. `error` is absent (not null) on 16 of 40 live 200 responses measured 2026-09-18, so test for the KEY before reading it; this block declared it required until then. On a non-2xx, `error` is present and `data` is absent.
  - `data` object, required: Three branches were observed live on 2026-09-25, one request per sport. NO property is required, because the branches share none: (a) `{ stats: null }`: nothing held for this player in this sport. Seen on american_football and football. (b) per-game derived totals: `player_id, sport, season, split` plus the sport's own per-game keys (basketball: `gp, gs, ppg, rpg, apg, spg, bpg, fg_pct, fg3_pct, ft_pct`). Seen on basketball. (c) the `level`-bearing baseball shape documented above: `player_id, sport, season, split, level, batting, pitching, seasons`. Seen on baseball. basketball and baseball were the only sports returning numbers on the day, so the per-sport key SETS are UNMEASURED for the rest and are deliberately left open rather than guessed.
    - `player_id` string, uuid
    - `sport` 'football' | 'basketball' | 'baseball' | 'ice_hockey' | 'cricket' | 'mma' | 'boxing' | 'formula1' | 'american_football'
    - `season` string, nullable
    - `split` string, nullable
    - `level` 'match' | 'season' | 'mixed': Baseball only. Read it before summing anything; see the route description.
    - `batting` object, nullable
    - `pitching` object, nullable
    - `seasons` object[]
    - `stats` unknown
  - `meta` ResponseMeta: Response metadata. NOTHING here is guaranteed: measured over 38 live envelopes on 2026-09-18, `cached` and `request_id` appeared in 29, `source` in 28, `cache_age_ms` in 9 and `confidence` in 6. This block previously declared all five REQUIRED, which no response has ever satisfied. Treat every field as optional and branch on its absence. Routes add their own keys here (`count`, `limit`, `offset`, `note`, `total`, and per-endpoint ones), so do not assume a closed set.
    - `date_window` object: Present when ?date= was applied. The exact window the filter used, so an omission at a day boundary is visible. start_utc is inclusive, end_utc exclusive. timezone is UTC, or the zone named by ?tz=, in which case the window is that local day. Boxing card dates are published event dates, so timezone and bounds are null there.
      - `date` string
      - `timezone` string, nullable
      - `start_utc` string, date-time, nullable
      - `end_utc` string, date-time, nullable
      - `note` string
    - `source` 'official-league' | 'aggregator-paid' | 'aggregator-free' | 'community-scraper' | 'forecast' | 'mixed' | 'cache' | 'none' | 'unknown-source' | 'stored' | 'catalogue' | 'gateway' | 'coverage-snapshot': Opaque source-tier label. Customers never see the vendor identity. `forecast` marks a weather forecast (GET /v1/matches/{id}/weather).
    - `confidence` number, float
    - `cached` boolean
    - `cache_age_ms` integer
    - `request_id` string
    - `fields_missing` string[]
    - `note` string: Short factual disclosure about something this response deliberately withheld, present only when there is something to disclose. On live scores it appears when `match_id` could not be resolved to a stored match and was therefore omitted: it gives the counts and points at `/v1/stored/matches`, whose ids you can call back into. Absent when every served id resolves. Distinct from `message`, which explains `coverage: false` or a stale response. This one is attached to a response that DID serve data and names the part of it that is deliberately missing.
  - `error` ApiError
    - `code` string, required
    - `message` string, required
    - `details` unknown

## Other responses

- `400`: invalid player id or missing/invalid sport

---

[API](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api.md) · [All operations](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api/llms.txt) · [OpenAPI document](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api/revisions/31fb54ac414e?raw)
