# MLB Statcast Pitch-by-Pitch Data Scraper (`incognito_mode/mlb-statcast-pitch-scraper`) Actor

Scrape every MLB pitch with Statcast tracking: velocity, spin rate, movement, release point, plate location, plus exit velocity and launch angle on balls in play. Adds computed approach angle, perceived velocity and barrel classification MLB does not publish. No API key, no login, no proxy.

- **URL**: https://apify.com/incognito\_mode/mlb-statcast-pitch-scraper.md
- **Developed by:** [Elena Vance](https://apify.com/incognito_mode) (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 $0.50 / 1,000 tracked pitches

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## MLB Statcast Pitch-by-Pitch Data Scraper

Every pitch of every MLB game, with the full Statcast tracking record — and
the flight metrics MLB measures but never publishes.

Most MLB scrapers sell you teams, rosters, schedules and standings. This one
sells the thing underneath: **one row per pitch**, with release speed, spin
rate, movement, release point and plate location, exit velocity and launch
angle on balls in play, and the raw nine-parameter trajectory model the whole
thing is derived from.

No API key. No login. No proxy. Runs on the free plan.

***

### What one row looks like

A called strike, first pitch of the game:

| Field | Value |
| --- | --- |
| `gameDate` / `venue` | `2026-09-06` · Great American Ball Park |
| `pitcher` / `batter` | Brady Singer (R) → Sal Frelick (L) |
| `inning` · `balls`-`strikes` | 1 top · 0-0 |
| `pitchType` | `SI` — Sinker |
| `releaseSpeed` → `plateSpeed` | 89.2 → 81.6 mph |
| `perceivedVelocity` | **90.52 mph** *(computed)* |
| `spinRate` / `spinDirection` | 2464 rpm / 206° |
| `extension` | 6.79 ft |
| `releasePoint` | **x −1.30, y 53.71, z 5.15 ft** *(computed)* |
| `plateLocation` | x 0.70, z 2.49 ft · zone 6 · **inZone true** *(computed)* |
| `movement` | −7.15 in horizontal, 5.32 in vertical, **8.91 in total** *(computed)*, 9.4 in induced vertical |
| `approachAngle` | **VAA −5.37°, HAA 1.07°** *(computed)* |
| `flightTime` | **0.419 s** *(computed)* |
| `trajectory` | all nine raw parameters, kept verbatim |
| `call` | `C` — Called Strike |
| `playResult` | Groundout — *"Sal Frelick grounds out, first baseman Sal Stewart to pitcher Brady Singer."* |

On a ball in play the row also carries `battedBall`: exit velocity, launch
angle, distance, trajectory, landing coordinates, and **`isBarrel`,
`isHardHit`, `isSweetSpot`** computed from Statcast's published thresholds.

***

### The computed fields are the point

MLB publishes the nine-parameter constant-acceleration model for every pitch
and almost none of the quantities analysts actually use. This Actor solves the
model and returns both.

| Computed | Why you want it |
| --- | --- |
| **Vertical approach angle (VAA)** | The standard explanation for why a flat 93 mph fastball at the top of the zone misses bats a steeper 97 does not. MLB has never published it. |
| **Horizontal approach angle (HAA)** | The same, laterally. |
| **Release point** | The feed gives position at a fixed 50 ft measurement plane, which is a convention, not a place any pitcher stands. Arm-slot drift and pitch tunnelling are measured from the real release point. |
| **Perceived velocity** | Release speed normalised to league-average extension — what the pitch plays like to the hitter, not to the radar gun. |
| **Flight time** | Release to plate. The hitter's entire decision budget. |
| **In-zone flag** | Recomputed from the location and *this batter's* measured zone, so it survives a dropped `zone` field. |
| **Barrel / hard-hit / sweet-spot** | Statcast's published contact grades, absent from this feed. |
| **Recomputed plate location** | A per-row cross-check: it should match MLB's own `pX`/`pZ` to 0.001 ft. When it doesn't, that pitch was mistracked — and a model trained on it would learn the error. |

Sanity check on 365 pitches from one real game — four-seams flat at the top,
curveballs steep at the bottom, exactly as they should be:

```
FF four-seam  −3.76°     FC cutter    −5.59°     CH changeup  −7.07°
SI sinker     −5.32°     ST sweeper   −6.78°     SL slider    −7.67°
                                                 CU curveball −8.42°
```

***

### Input

Everything is optional. **Run it with no input at all** and it scrapes the most
recent slate that was actually played — which works in February as well as in
July, because it falls back to searching the whole previous season rather than
returning nothing in the offseason.

| Field | Meaning |
| --- | --- |
| `dateFrom` / `dateTo` | Game date range, `YYYY-MM-DD`. Max 31 days. |
| `gamePks` | Specific MLB game IDs, e.g. `[824469]`. Overrides the dates. |
| `teamIds` | Only games involving these teams, e.g. `[147]` for the Yankees. |
| `gameTypes` | Regular season and postseason by default. Spring training is off — many of those parks have no tracking rig, so their pitches come back empty. |
| `pitcherIds` / `batterIds` | Keep only these players' pitches. Combine both for one matchup. |
| `maxGames` | **The real cost control.** A game is ~300 pitches. Default 5. |
| `maxItems` | Hard cap on rows. Default 1000. |

#### Examples

Every pitch Paul Skenes threw last week:

```json
{ "dateFrom": "2026-09-01", "dateTo": "2026-09-07", "pitcherIds": [694973], "maxGames": 40 }
```

One specific game, complete:

```json
{ "gamePks": [824469], "maxItems": 500 }
```

A whole team's month:

```json
{ "dateFrom": "2026-08-01", "dateTo": "2026-08-31", "teamIds": [147], "maxGames": 30, "maxItems": 10000 }
```

Filters are applied by MLB before the schedule comes back, so a narrow request
over a wide range stays cheap rather than merely ending up smaller.

***

### Notes on cost

- **`maxGames`, not the date range, is what a run costs.** A month-wide range
  with `maxGames: 5` reads five games. The range only decides what is eligible.
- **Player filters cut your bill.** MLB has no "every pitch by this pitcher"
  endpoint, so whole games are still read — but you are only charged for rows
  you keep.
- **Games that haven't started are skipped** before any request is made. They
  contain no pitches.
- **Runs go direct.** No proxy is bought unless MLB actually refuses an IP, and
  then a free datacenter one is tried well before any metered residential one.

***

### Output

One dataset row per pitch, exportable as JSON, CSV, Excel, XML or HTML. The
full field list with descriptions is in
[`.actor/dataset_schema.json`](.actor/dataset_schema.json).

`battedBall` is `null` on the ~85% of pitches nobody hit, rather than a block
of empty columns, so a CSV of a whole game stays readable.

Game context (teams, date, venue, game type) is repeated on every row on
purpose — a pitch that cannot say which park it was thrown in is not much use
on its own, and park effects on carry are large.

***

### How it works

MLB's Stats API is genuinely open: no key, no login, no TLS fingerprinting, no
rate limit encountered. There is nothing to bypass, so this Actor is a plain
HTTP client with the physics on top. Two endpoints do all the work — one
schedule request per run, one play-by-play request per game, four at a time.

The play-by-play endpoint is used rather than the more obvious live feed, with
a field projection that cuts a game from 967 KB to 484 KB for byte-identical
pitch data.

Details, including how the plate-crossing plane was verified against MLB's own
published coordinates, are in [`docs/architecture.md`](docs/architecture.md).

***

### Not affiliated with MLB

Independent tool. Reads the same public endpoint that mlb.com's own Gameday
scoreboard reads. "MLB" and "Statcast" are trademarks of their respective
owners and are used here only to describe what the data is.

# Changelog

This Actor's version history is a separate document: https://apify.com/incognito\_mode/mlb-statcast-pitch-scraper/changelog.md

# Actor input Schema

## `dateFrom` (type: `string`):

First game date to scrape (YYYY-MM-DD). Leave both dates empty and the Actor scrapes the most recent slate that was actually played — so a run with nothing filled in returns real pitches in February as well as in July.

## `dateTo` (type: `string`):

Last game date to scrape (YYYY-MM-DD), inclusive. Defaults to "Date from". Maximum 31 days per run — use "Max games" to control what a wide range actually costs.

## `gamePks` (type: `array`):

MLB game IDs (gamePk) to scrape directly, e.g. \[824469]. Find one in any Gameday URL, or in the `gamePk` column of a previous run. Overrides the date range and the game-type filter — a game you name by ID is always fetched.

## `teamIds` (type: `array`):

Only scrape games involving these MLB team IDs, e.g. \[147] for the Yankees or \[119] for the Dodgers. Filtered by MLB before the schedule is returned, so it makes a wide date range cheap rather than just smaller.

## `gameTypes` (type: `array`):

Which kinds of game to include. Spring training and exhibition games are excluded by default: many of those parks have no tracking rig, so their pitches come back with every physics field empty.

## `pitcherIds` (type: `array`):

Keep only pitches thrown by these MLB person IDs, e.g. \[694973] for Paul Skenes. MLB has no "every pitch by this pitcher" endpoint, so whole games are still read — but you are only charged for the rows you keep.

## `batterIds` (type: `array`):

Keep only pitches thrown to these MLB person IDs. Combine with "Pitcher IDs" to pull a single matchup.

## `maxGames` (type: `integer`):

How many games to read at most. A full MLB game is around 300 pitches, so this is the real cost control for a wide date range. The most recent matching games are kept.

## `maxItems` (type: `integer`):

Hard cap on how many pitches are stored in the dataset. The run stops as soon as it is reached.

## `lookbackDays` (type: `integer`):

Only used when no dates and no game IDs are given: how far back to look for the most recent slate. If nothing is found in this window the Actor widens the search to the whole previous season, so this rarely needs changing.

## `proxyConfiguration` (type: `object`):

Leave off. The MLB Stats API needs no key, no login and no proxy, so a normal run spends nothing here. If an IP is ever refused, the Actor escalates on its own — first to a datacenter proxy (included in every Apify plan, free tier included), rotating a few IPs, and only then to a residential one if your plan has them. Set this only to pin a specific proxy configuration from the start.

## Actor input object example

```json
{
  "gameTypes": [
    "R",
    "F",
    "D",
    "L",
    "W"
  ],
  "maxGames": 5,
  "maxItems": 1000,
  "lookbackDays": 7,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset containing every scraped pitch.

# 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 = {
    "gameTypes": [
        "R",
        "F",
        "D",
        "L",
        "W"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("incognito_mode/mlb-statcast-pitch-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 = {
    "gameTypes": [
        "R",
        "F",
        "D",
        "L",
        "W",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("incognito_mode/mlb-statcast-pitch-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 '{
  "gameTypes": [
    "R",
    "F",
    "D",
    "L",
    "W"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call incognito_mode/mlb-statcast-pitch-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,incognito_mode/mlb-statcast-pitch-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/KOuecscgJHGN2zcfe/builds/K8OV97eUH704XVpUK/openapi.json
