# MLB Ticket Availability Scraper (`usestring/mlb-tickets`) Actor

Read MLB box-office inventory off mlb.tickets.com: one row per section and price level still on sale, with availableSeats, maxContiguousSeats, face value in price, the checkout allInPrice and the serviceCharge between them. Name a club and it finds its own home games. 18 of 30 clubs.

- **URL**: https://apify.com/usestring/mlb-tickets.md
- **Developed by:** [String](https://apify.com/usestring) (community)
- **Categories:** Travel, Business, E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 results

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

### What does the MLB Ticket Availability Scraper do?

Every other ticket scraper on the Apify Store reads a resale marketplace, where the number on the
screen is a reseller's ask. This one reads the primary market — the seat map an MLB club publishes
on `mlb.tickets.com`, which is the box office's own inventory at the club's own face value.

Name a club, say `red-sox`, and the Actor reads that club's current home schedule itself, then
fetches the seat map for each upcoming game. You get one row per section and price level that still
has seats: how many are left, the longest run of adjacent seats in that block, the face value, the
all-in checkout price, and the per-ticket fee that is the gap between the two.

Event ids on this platform roll over every season, which is why the club is the durable input. A
saved run keyed on `red-sox` still works after the fixtures change; one keyed on an event id stops
working within months. Explicit ids are still accepted for anyone who already holds them.

- No account, session cookie or API key, at any point in the run
- One request per game, plus one per club to read its schedule
- Face value and checkout price on the same row, with the fee named rather than implied
- Values arrive typed from the venue's own feed — `174`, not `"$174.00"`

### What data does it extract?

One row per **event x section x price level** that has seats on sale. In a measured batch across
three clubs, ten games produced 574 rows, roughly 57 per game.

| Field | Type | Description |
| --- | --- | --- |
| `eventId` | string | The club's own event id, stable within a season. Use it to de-duplicate or re-poll one game |
| `eventName` | string | The event as the club names it, e.g. `Giants at Red Sox` |
| `eventDateTime` | string | Start time, ISO 8601, carrying the ballpark's own UTC offset |
| `eventCode` | string | The club's internal code for the event |
| `team` | string | The club slug you asked for. `null` when the event came in as a bare id |
| `orgId` | number | The club's tenant id on the platform. `null` on a bare id, for the same reason |
| `venueName` | string | Ballpark name, e.g. `Fenway Park` |
| `venueCity` | string | Venue city |
| `venueState` | string | Two-letter state code |
| `sectionId` | string | The venue's section id |
| `sectionName` | string | Section as printed, e.g. `LOGE BOX-LEFT FIELD` |
| `sectionCode` | string | The short public code for the section |
| `priceLevelId` | string | The price-scale id the seats sit on |
| `priceLevelName` | string | Price scale as the club names it, e.g. `Loge Box` |
| `priceLevelCode` | string | The club's short code for that price scale |
| `availableSeats` | number | Seats on sale in that section at that price level |
| `maxContiguousSeats` | number | Largest block sold together — what a group of four needs |
| `sectionCapacity` | number | The section's total size |
| `sectionAvailableSeats` | number | The section's total on sale, across all its price levels |
| `price` | number | Face value, as a number |
| `allInPrice` | number | What the checkout charges once fees are added |
| `serviceCharge` | number | `allInPrice` minus `price`, computed and stated |
| `currency` | string | Currency of both price fields |
| `inventoryAsOf` | string | The venue's own timestamp on the seat count |
| `sourceUrl` | string | The seat-map URL the row was read from |
| `collectedAt` | string | ISO timestamp of the run |

The unit is a section at a price level, not an individual seat. Seat numbers live behind a separate
and much heavier request that this Actor does not make.

### Why scrape MLB ticket availability?

Resale data tells you what a reseller hopes to get. Box-office data tells you what the club is still
selling and at what price, and the two together are the actual arbitrage picture. Only one of them
is hard to obtain, and it is this one.

Re-running a club on a schedule and diffing `availableSeats` per section gives a demand curve for a
game as it approaches: which parts of the ballpark drain first, how fast, and whether a promotion
moved anything. `maxContiguousSeats` answers a question the totals cannot — a section with 40 seats
left in ones and twos is useless to a group of four, and the field separates those cases.

`price` next to `allInPrice` puts a number on the fee load per price scale, which is otherwise
something a buyer only discovers at checkout. And because `priceLevelName` comes straight from the
club's own scale, you can compare how clubs tier an identical ballpark geometry across a homestand
or across the league on the same weekend.

### How to use it

1. Start from this page: **Try for free**, or save the Actor to your Apify account.
2. Put club slugs in the **Clubs** field, one per line — `red-sox`, `cubs`, `dodgers`. A club's
   numeric tenant id works too.
3. Optionally set **From date** and **To date** as `YYYY-MM-DD` to bound the games by their own
   local date.
4. Set **Events per club** for how deep into each schedule to read, and **Maximum results** for a
   hard ceiling on rows.
5. Click **Start**, then export from the **Dataset** tab as JSON, CSV or Excel.
6. Check the run's `SUMMARY` record: anything that could not be fetched is listed under `failures`
   with the reason.

Supply `teams`, `eventIds`, or both. An id that arrives twice — once directly, once because it is
also on a club's schedule — is fetched once, and keeps the club and date the schedule gave it.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `teams` | array of strings | — | Club slugs whose home schedule to read, up to 40. A numeric tenant id also works |
| `eventIds` | array of strings | — | Specific event ids, or event URLs carrying a `pid`. Up to 200, fetched without reading a schedule |
| `startDate` | string | — | Skip games before this local date, `YYYY-MM-DD` |
| `endDate` | string | — | Skip games after this local date, `YYYY-MM-DD` |
| `maxEventsPerTeam` | integer | `10` | Events to read per club, 1 to 200. One event is one request |
| `maxItems` | integer | `1000` | Ceiling on dataset rows, 1 to 50000 |
| `concurrency` | integer | `3` | Targets fetched in parallel, 1 to 5 |

At least one of `teams` or `eventIds` must be non-empty; a run with neither is rejected before it
fetches anything.

```json
{
  "teams": ["red-sox", "cubs"],
  "startDate": "2026-09-01",
  "endDate": "2026-09-30",
  "maxEventsPerTeam": 10,
  "maxItems": 1000,
  "concurrency": 3
}
```

### Output

One row per section and price level with seats on sale. The shape below is illustrative — it shows
the fields and their types, not a captured run.

```json
{
  "eventId": "9681705",
  "eventName": "Giants at Red Sox",
  "eventDateTime": "2026-09-12T19:10:00-04:00",
  "eventCode": "26RS0912",
  "team": "red-sox",
  "orgId": 23,
  "venueName": "Fenway Park",
  "venueCity": "Boston",
  "venueState": "MA",
  "sectionId": "1420",
  "sectionName": "LOGE BOX-LEFT FIELD",
  "sectionCode": "LB157",
  "priceLevelId": "12",
  "priceLevelName": "Loge Box",
  "priceLevelCode": "LB",
  "availableSeats": 18,
  "maxContiguousSeats": 4,
  "sectionCapacity": 132,
  "sectionAvailableSeats": 26,
  "price": 174,
  "allInPrice": 201.35,
  "serviceCharge": 27.35,
  "currency": "USD",
  "inventoryAsOf": "2026-09-05T14:31:00-04:00",
  "sourceUrl": "https://mlb.tickets.com/api/pvodc/v1/events/navmap/availability/?pid=9681705&agency=MLB_MPV&supportsVoucherRedemption=true&accessible=false",
  "collectedAt": "2026-09-05T14:36:22.114Z"
}
```

A sold-out game returns no rows rather than a wall of zeroes. The venue's response describes every
section of the ballpark whatever its state, so emitting all of them would bill several hundred empty
rows for a game with nothing to sell.

### What it covers, and what it does not

Eighteen of the thirty MLB clubs sell through this platform: `angels`, `astros`, `brewers`,
`cardinals`, `cubs`, `dodgers`, `guardians`, `marlins`, `nationals`, `phillies`, `pirates`,
`rangers`, `rays`, `red-sox`, `reds`, `royals`, `tigers`, `twins`. The other twelve are on
Ticketmaster and have no box office on this host at all. A club that is not on the list is reported
under `failures` immediately rather than fetched, so it costs nothing.

**Home games only.** A club's schedule here is what that club sells, so an away game belongs to the
host club's feed, not this one.

**Some clubs list non-baseball events on the same schedule.** Parking passes, stadium tours and
museum entry appear alongside the games — the Cardinals' schedule runs to roughly 110 entries, most
of them exactly that. They return as ordinary rows and are billed as ordinary rows. `eventName` is
what tells them apart, so filter on it (or set a tight date window) if you only want ballgames.

**`inventoryAsOf` runs a few minutes behind live.** That is the venue's own snapshot timestamp, not
our latency. It is fine for trend work and comparison against resale; it is the wrong feed to sit
behind a real-time booking flow.

Seat-by-seat detail, resale listings, holds, season-ticket inventory and anything requiring a login
are all out of scope.

### How much does it cost?

Pricing is per event: one charge for each result row written to the dataset. A club or event that
fails produces no rows and costs nothing. The current rate is in the pricing panel at the top of
this page.

What drives the total is rows per game, and that number is large — roughly 57 in our measured batch,
because a ballpark has many sections and most of them have something on sale. Bound it with
`maxEventsPerTeam` and `maxItems` before running a wide sweep, and remember that the non-baseball
entries described above are charged the same as games.

Runs started from an Apify free plan stop at 250 requests and 250 results, reported in the run's
status message; any paid plan runs the full input and whatever `maxItems` you set. The cap is there
because this Actor fetches through String's own infrastructure, which Apify does not reimburse on
free-plan runs, and it binds on requests as well as rows so a long club list cannot spend those
fetches on rows the run will not return.

### Using it with the Apify API

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

const client = new ApifyClient({ token: "<YOUR_APIFY_TOKEN>" });

const run = await client.actor("usestring/mlb-tickets").call({
  teams: ["red-sox", "cubs"],
  startDate: "2026-09-01",
  endDate: "2026-09-30",
  maxEventsPerTeam: 5
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();

// Sections that can still seat a group of four together, cheapest first.
const groupsOfFour = items
  .filter((row) => row.maxContiguousSeats >= 4)
  .sort((a, b) => a.allInPrice - b.allInPrice);
console.log(groupsOfFour.slice(0, 10));

const summary = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord("SUMMARY");
console.log(summary?.value);
```

### Is it legal to scrape MLB ticket availability?

The Actor reads the same public inventory a club's own ticketing page loads for a logged-out
visitor. There is no account, no session cookie, no paywall and no access control being stepped
around. Collecting information from a page published to the open web is lawful in the US and the EU
under current case law; what invites trouble is circumventing a login, and that does not happen
here.

Nothing collected is personal data. Every row describes a section of a ballpark and a price — no
buyers, no ticket holders, no names of any kind. Prices and seat counts are facts rather than
creative work, though a club's event descriptions and section naming are its own, so treat the text
as source material for analysis rather than something to republish. Ticketing terms of service and
your own reselling obligations are yours to check against what you intend to do with the data. This
is not legal advice.

### Related actors

No other String Actor reads ticketing, so these are neighbours rather than substitutes:

- [Spotify Artist Scraper - Listeners & Followers](https://apify.com/usestring/spotify-artists) —
  monthly listeners and follower counts, for anyone modelling live-event demand from audience size.
- [Yahoo Finance Quote Scraper](https://apify.com/usestring/yahoo-finance-quotes) — quotes and
  fundamentals, useful alongside inventory data on publicly listed sports and media groups.
- [TikTok Profile Scraper](https://apify.com/usestring/tiktok-profiles) — follower and engagement
  counts, another attention signal to line up against a demand curve.
- [Etsy Listings Scraper - Price, Shop & Badges](https://apify.com/usestring/etsy-listings) — a
  second pass-through Actor built the same way, if you want to see the pattern on a marketplace.

### FAQ

**Is this resale prices or box-office prices?**
Box office. `price` is the club's face value and `allInPrice` is what the checkout charges once fees
are added. Neither is a reseller's ask.

**Do I need an account, API key or cookies?**
No. It reads the public inventory a logged-out visitor's browser loads, and never signs in.

**Do I have to know the event id?**
No, and you should not have to — ids roll over every season. Give it a club slug and, if you like, a
date window. Ids remain available as an input for anyone who already has them.

**Which clubs work?**
The eighteen listed above. The other twelve sell through Ticketmaster and are rejected up front,
without a fetch, rather than returning an empty schedule.

**Is it seat by seat?**
No. A row is a section at a price level: how many seats are on sale there and the largest adjacent
block. Individual seat numbers need a different and far heavier request.

**How fresh are the counts?**
`inventoryAsOf` carries the venue's own timestamp on the snapshot, which in our sampling ran a few
minutes behind live.

**Why is `team` sometimes null?**
Because that event came in as a bare id, so there was no club to attribute it to. `orgId` is null
for the same reason. Every other field on the row is unaffected.

**Why did a run return parking or a stadium tour?**
Because the club lists those on the same schedule the games are on. They come back as normal rows;
filter on `eventName` to drop them.

**Do I get anything for a sold-out game?**
No rows, deliberately. An event id that does not resolve at all is different again — it lands in
`SUMMARY.failures` with the site's own message rather than becoming a row of nulls.

**How fast is it?**
A seat map took a median of 7.6 seconds at concurrency 3 in our measurements, and 2.2 seconds run
sequentially; the difference is solver work happening in parallel, not the origin slowing down.

### Feedback

If a club fails, a game returns nothing you expected, or a price looks wrong, open an issue from the
Issues tab on this Actor's Store page and include the club slug and the `eventId`.

# Actor input Schema

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

Club slugs whose home schedule to read, e.g. red-sox, cubs, dodgers. A numeric tenant id works too.

## `eventIds` (type: `array`):

Optional. Specific mlb.tickets.com event ids, or event URLs carrying a pid, fetched without reading a schedule.

## `startDate` (type: `string`):

Optional. Skip games before this local date, as YYYY-MM-DD.

## `endDate` (type: `string`):

Optional. Skip games after this local date, as YYYY-MM-DD.

## `maxEventsPerTeam` (type: `integer`):

How many of each club's upcoming events to read. One event costs one request.

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

Global cap on dataset items. Runs started from an Apify free plan stop at 250 requests and 250 results; any paid plan runs the full amount.

## `concurrency` (type: `integer`):

Targets fetched in parallel.

## Actor input object example

```json
{
  "teams": [
    "red-sox"
  ],
  "maxEventsPerTeam": 10,
  "maxItems": 1000,
  "concurrency": 3
}
```

# Actor output Schema

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

Read MLB box-office inventory off mlb.tickets.com: one row per section and price level still on sale, with availableSeats, maxContiguousSeats, face value in price, the checkout allInPrice and the serviceCharge between them. Name a club and it finds its own home games. 18 of 30 clubs.

## `summary` (type: `string`):

Item count, failure count and every target that failed, with its error.

# 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 = {
    "teams": [
        "red-sox"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("usestring/mlb-tickets").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 = { "teams": ["red-sox"] }

# Run the Actor and wait for it to finish
run = client.actor("usestring/mlb-tickets").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 '{
  "teams": [
    "red-sox"
  ]
}' |
apify call usestring/mlb-tickets --silent --output-dataset

```

## MCP server setup

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

```

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/B3LJKrjmeLGh75fZ3/builds/9R49XUjfRRDJ9AFs9/openapi.json
