# MLB Stats Scraper & Stat Change Monitor - Hitting, Pitching (`neverempty/mlb-stats-scraper`) Actor

For fantasy baseball, betting models and analytics: season hitting, pitching and fielding from the official MLB Stats API - 4,223 stat lines for 2026 (732 hitting, 848 pitching, 2,643 fielding). Innings pitched are handled as thirds, not decimals. Monitoring returns only lines that changed.

- **URL**: https://apify.com/neverempty/mlb-stats-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Sports, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.65 / 1,000 stat line returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## MLB Stats Scraper & Stat Change Monitor — Hitting, Pitching, Fielding

**Built for fantasy baseball, betting models and season-long analytics**: every MLB season stat line from the **official** MLB Stats API as one flat row — the player, team, league, position, the rank the API assigns, and every stat it publishes for that group.

**A whole season, not a sample.** For 2026 the API held **732** hitting, **848** pitching and **2,643** fielding lines — **4,223 lines in all** — and this Actor pages through the entire group, 100 at a time, until the API's own total is reached. The player pool is yours to choose: 2026 hitting holds **732** lines under `ALL` but the 2025 group holds **145** under `QUALIFIED`, so leaving a default in place quietly shows you a fraction of the league.

**The two things that silently ruin baseball data are fixed here.** Innings are thirds, not decimals — `20.1` is 20 innings *and one out*, so every innings figure comes back three ways (`"20.1"`, `61` outs, `20.3333`). And a rate the API could not compute (`.---`) comes back as `null`, never `0`: on the 2026 pitching fixture **39 of 60** lines had at least one.

Turn monitoring on and every later run returns only the stat lines that have actually changed, so you are never charged for the same line twice. Run it daily through the season and pay only for the players who played. Export as JSON, CSV or Excel.

> **Unofficial Actor.** Not affiliated with or endorsed by MLB. It reads MLB's public Stats API — no key, no login, no HTML parsing.

### What it does

- ⚾ **Three stat groups.** Hitting, pitching and fielding. For the 2026 season the API held **732** hitting, **848** pitching and **2,643** fielding lines.
- 🔁 **Change monitoring.** The remembered key is the player *plus the group plus every number in their line*, so a player comes back only when a stat actually moved. Run it daily during the season and pay only for the players who played.
- 👥 **The player pool matters, and you choose it.** The 2026 hitting group holds **732** lines under `ALL`; the 2025 group holds **145** under `QUALIFIED` — the pool changes the answer by roughly a factor of five. An Actor that leaves the default in place quietly shows you a fraction of the league.
- 📄 **Full pagination.** Reads through the whole group, 100 at a time, and stops when the API's own total is reached.

### 🔴 Two things this Actor gets right that are easy to get wrong

**1. Innings are thirds, not decimals.** In baseball `20.1` means 20 innings **and one out** — 20⅓ — not 20.1. Summing or averaging the raw number is silently wrong. Every innings figure is returned three ways:

| Column | Value for `20.1` |
|---|---|
| `inningsPitchedRaw` | `"20.1"` (exactly as the API prints it) |
| `inningsPitchedOuts` | `61` |
| `inningsPitchedDecimal` | `20.3333` |

The same applies to fielding `innings`. If the API ever prints a first decimal above 2, that is not a thirds notation, and the Actor returns `null` rather than guessing.

**2. Unavailable rates are not zero.** When a rate cannot be computed, the API prints `.---` or `-.--`. Turning that into `0` invents a fact — a pitcher with no stolen-base attempts against him is not a pitcher with a 0% rate. Those come back as `null`, with the original text kept in a `…Raw` column. On the 2026 pitching fixture, **39 of 60** lines had at least one.

### What this Actor refuses to guess

**The MLB API answers HTTP 200 with an empty `stats` array for a stat group it does not know** — which looks exactly like "this player has no stats". Only the three verified groups are accepted, and an unknown one is refused with an explanation rather than reported as an empty result.

A season that has not been played returns a genuine zero (2030 gives `totalSplits: 0`), and that is reported as the API's own answer. Empty seasons, unknown groups, filters that matched nothing and unreadable responses each come back as **their own row with the reason**, and none are charged.

### What you get in every row

| Field | What it is |
|---|---|
| `season`, `statGroup`, `playerPool` | What was asked for, on every row |
| `playerId`, `playerName`, `firstName`, `lastName` | The player and their MLB id |
| `teamId`, `teamName`, `leagueId`, `leagueName` | Where they played |
| `position`, `positionAbbreviation`, `positionType` | Their position |
| `statRank` | The rank the API assigns within the group |
| `teamsPlayedFor` | More than 1 means they were traded mid-season |
| *(the stats)* | Every stat the API publishes, flattened: `homeRuns`, `avg`, `ops`, `era`, `whip`, `assists`, `putOuts` … |
| `…Raw` | Kept whenever a value could not be turned into a number |
| `statKey` | Changes when any stat changes — this is what monitoring compares |

### Configure the run

```json
{ "statGroups": ["hitting"], "season": 2026, "playerPool": "ALL", "maxPlayers": 200 }
```

Follow one team through the season, paying only for changes:

```json
{ "statGroups": ["hitting", "pitching"], "teams": ["Dodgers"], "monitoringMode": true }
```

Qualified hitters only, top 50 by the API's rank:

```json
{ "statGroups": ["hitting"], "playerPool": "QUALIFIED", "maxRank": 50 }
```

### Input reference

Every field is optional; the defaults read 2026 hitting for all players.

| Field | Type | Default | What it does |
|---|---|---|---|
| `statGroups` | array | `["hitting"]` | Which stat groups to read. The MLB API answers HTTP 200 with an empty stats array for a group it does not know, which looks exactly like 'this player has no stats',… |
| `season` | integer | `2026` | Which season to read. Seasons that have not been played return zero rows, which is the API's own honest answer and is reported as such rather than as a failure. |
| `playerPool` | string | `ALL` | Which players are included. This changes the result a lot: for 2026 hitting the API returned 732 players for ALL but only 140 for QUALIFIED (players who reached the… |
| `maxPlayers` | integer | `200` | How many rows to return. One row is one player in one stat group. You are charged for the rows you actually receive. For reference, 2026 held 732 hitting, 848… |
| `monitoringMode` | boolean | `false` | Off = return every line for the groups you picked. On = remember the exact numbers already returned and, on later runs, return a player's line again only when one of… |
| `resetMonitoringState` | boolean | `false` | Clear the remembered lines before this run, so everything counts as new again. |
| `teams` | array | `[]` | Keep a line only if the team name contains one of these (case-insensitive, partial). Example: Yankees, Dodgers. |
| `players` | array | `[]` | Keep a line only if the player's full name contains one of these (case-insensitive, partial). Example: Ohtani, Judge. |
| `positions` | array | `[]` | Keep a line only if the position or its abbreviation contains one of these (case-insensitive, partial). Example: Pitcher, P, Catcher, C. |
| `leagues` | array | `[]` | Keep a line only if the league name contains one of these. Example: American League, National League. |
| `minGamesPlayed` | integer | `0` | Keep a line only if the player appeared in at least this many games. 0 = no limit. Lines with no games-played figure are kept, because unknown is not the same as zero. |
| `maxRank` | integer | `0` | The API assigns each line a rank within the group. 0 = no limit. Lines with no rank are kept. |
| `keywords` | array | `[]` | Keep a line only if one (Any) or all (All) of these appear in the player, team, position, league or group name. |
| `keywordMatch` | string | `any` | Any = at least one keyword. All = every keyword. |
| `excludeKeywords` | array | `[]` | Drop a line if any of these appear in the same fields. |
| `useProxy` | boolean | `false` | Off by default: the official MLB Stats API answered 200 from a plain Apify datacentre address when this Actor was built (2026-09-06). If it starts answering 403, 429… |

### Pricing

Pay per result: you are charged for the stat lines you actually receive. Rows that only explain why there is nothing (`no-results`, `no-such-group`, `unreadable`, `no-filter-match`, `no-stat-change`) are never charged.

### Every number in this README is checked

The fixtures in `test/fixtures/` are the real responses this Actor was built from (2026-09-06). The test suite re-counts every figure quoted above — 60, 732, 848, 2643, 145, 39, 20.1, 61, 20.3333 — so if the API changes, the tests fail rather than the data going quietly wrong.

# Actor input Schema

## `statGroups` (type: `array`):

Which stat groups to read. The MLB API answers HTTP 200 with an empty stats array for a group it does not know, which looks exactly like 'this player has no stats', so only these three are accepted.

## `season` (type: `integer`):

Which season to read. Seasons that have not been played return zero rows, which is the API's own honest answer and is reported as such rather than as a failure.

## `playerPool` (type: `string`):

Which players are included. This changes the result a lot: for 2026 hitting the API returned 732 players for ALL but only 140 for QUALIFIED (players who reached the required plate appearances).

## `maxPlayers` (type: `integer`):

How many rows to return. One row is one player in one stat group. You are charged for the rows you actually receive. For reference, 2026 held 732 hitting, 848 pitching and 2,643 fielding lines.

## `monitoringMode` (type: `boolean`):

Off = return every line for the groups you picked. On = remember the exact numbers already returned and, on later runs, return a player's line again only when one of their stats has actually moved. Run it daily during the season and pay only for the players who played.

## `resetMonitoringState` (type: `boolean`):

Clear the remembered lines before this run, so everything counts as new again.

## `teams` (type: `array`):

Keep a line only if the team name contains one of these (case-insensitive, partial). Example: Yankees, Dodgers.

## `players` (type: `array`):

Keep a line only if the player's full name contains one of these (case-insensitive, partial). Example: Ohtani, Judge.

## `positions` (type: `array`):

Keep a line only if the position or its abbreviation contains one of these (case-insensitive, partial). Example: Pitcher, P, Catcher, C.

## `leagues` (type: `array`):

Keep a line only if the league name contains one of these. Example: American League, National League.

## `minGamesPlayed` (type: `integer`):

Keep a line only if the player appeared in at least this many games. 0 = no limit. Lines with no games-played figure are kept, because unknown is not the same as zero.

## `maxRank` (type: `integer`):

The API assigns each line a rank within the group. 0 = no limit. Lines with no rank are kept.

## `keywords` (type: `array`):

Keep a line only if one (Any) or all (All) of these appear in the player, team, position, league or group name.

## `keywordMatch` (type: `string`):

Any = at least one keyword. All = every keyword.

## `excludeKeywords` (type: `array`):

Drop a line if any of these appear in the same fields.

## `useProxy` (type: `boolean`):

Off by default: the official MLB Stats API answered 200 from a plain Apify datacentre address when this Actor was built (2026-09-06). If it starts answering 403, 429 or 503, the Actor switches to a proxy session on its own and tries again.

## Actor input object example

```json
{
  "statGroups": [
    "hitting"
  ],
  "season": 2026,
  "playerPool": "ALL",
  "maxPlayers": 200,
  "monitoringMode": false,
  "resetMonitoringState": false,
  "teams": [],
  "players": [],
  "positions": [],
  "leagues": [],
  "minGamesPlayed": 0,
  "maxRank": 0,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "useProxy": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per player per stat group: the season, group and player pool; the player with their MLB id and name; the team, league and position; the rank the API assigns and how many teams the player appeared for; and every stat the API publishes for that group, flattened into columns. Rates that the API prints as unavailable (a row of dashes) come back as null with the original text kept alongside, never as zero. Innings pitched and fielding innings are baseball thirds, not decimals, so each is returned three ways: the API's own text, the count of outs, and a true decimal. Requests for a stat group the API does not know, seasons with no stats, filters that matched nothing and responses that could not be read come back as their own rows and are not charged.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "statGroups": [
        "hitting"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/mlb-stats-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "statGroups": ["hitting"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/mlb-stats-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "statGroups": [
    "hitting"
  ]
}' |
apify call neverempty/mlb-stats-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/mlb-stats-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/JBmSn4fx5EHMdjomX/builds/LvikVdtwmZVUSNmKl/openapi.json
