# Sports Reference Stats Scraper - NBA & NHL (`recordsdata/sports-reference-stats-scraper`) Actor

Scrape NBA player and team stats from Basketball-Reference and NHL stats from Hockey-Reference: per-season stats, advanced metrics (PER, win shares, BPM), team rosters and salaries. One row per season, 23 seasons for LeBron James in one page. Export CSV, Excel, JSON. No login or API key.

- **URL**: https://apify.com/recordsdata/sports-reference-stats-scraper.md
- **Developed by:** [RecordsData](https://apify.com/recordsdata) (community)
- **Categories:** Sports, Developer tools
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $21.00 / 1,000 player season records

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?

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

<p align="center">
  <img src="https://api.apify.com/v2/key-value-stores/AAm3a1h3Z9nYfrvh9/records/banner?v=3" alt="RecordsData" width="100%" />
</p>

> **Sports Reference Stats Scraper** scrapes Basketball-Reference (NBA) and Hockey-Reference (NHL). Paste player, team or league-season URLs, or pick a whole season or a last-name letter with no URL at all. Get one row per season with every stat column the page publishes, plus playoffs, career totals, bio and draft, advanced metrics, hockey goalies, Corsi and Fenwick, rosters and salaries. Export CSV, Excel, JSON or API. No login, no API key. Last updated 2026-10-11. From $25 per 1,000 season rows.

Sports Reference Stats Scraper turns Basketball-Reference and Hockey-Reference pages into structured data. A single LeBron James page with every option on returned 115 rows in a cloud run. It is built for sports analysts, fantasy players, journalists and data scientists who need historical NBA and NHL numbers.

### 🏀 What does Sports Reference Stats Scraper do?

It has three modes, set with the **Mode** input:

- **Start URLs** (`urls`): player pages, team-season pages, league-season tables or letter indexes from either site.
- **Whole league season** (`season`): every player of one NBA or NHL season, no URL needed. Pick `sport`, `season` and `statType`; hockey adds `playerType`.
- **Player index** (`index`): the players-by-last-name-letter list of one sport, with player id, first and last season, position and active flag.

On player, team and league pages you can switch on extra rows:

- **Playoff stats**, **Career totals** and **Advanced stats** (PER, win shares, BPM and VORP for basketball, rate stats for hockey).
- **Corsi and Fenwick**: hockey possession rows with PDO and zone starts.
- **Team roster** and **Salaries** (basketball), including tables hidden in HTML comments.
- **Bio and draft details**: full name, height, weight, birth date and place, college, draft round, pick and year, and Hall of Fame flag on every row of the player.
- **Numbers as numbers** (`numericValues`): stat values as numbers instead of text.

Hockey goalie pages work, with goalie columns such as `goalie_wins` and `save_pct_goalie`. Every row carries `sport`, `pageType` and `recordType`, and the column names are the ones the source site uses, so numbers match the page.

### ⚖️ Basketball-Reference scraper vs other Apify actors

Read on 2026-10-10 from the public Store pages of each actor. A cell says "not shown" when the page does not mention the feature.

| | Us | lulzasaur (basketball and hockey) | parseforge (basketball and hockey) |
|---|---|---|---|
| Sports | NBA and NHL in one actor | One actor per sport | One actor per sport |
| Start without a URL | Season mode and letter index | Letter index or detailed mode | Season and stat type |
| Playoffs, career, bio, draft | All four as switches | Playoffs, career, bio, draft | Not shown |
| Hockey goalies, Corsi | Goalies, Corsi, Fenwick, PDO | Goalies yes, Corsi not shown | Goalies, Corsi "where available" |
| Roster, salaries | Both, as switches | Not shown | Not shown |
| Price per 1,000 | $25 Free plan, $21 top tier | From $10 | $19 header, $21 README |

lulzasaur is the cheapest, about 2.5 times below our season row, and also returns playoffs, bio and draft; it gives 11 fields in its basketball views while we publish 220 data columns, plus rosters and salaries. parseforge is level on price and starts from a season, but its page shows neither playoffs nor bio. If you only need a cheap flat list of one sport, lulzasaur may fit better.

### 📊 What data can you extract from Basketball-Reference and Hockey-Reference?

| Field | Description |
|---|---|
| `sport`, `pageType`, `recordType` | Basketball or hockey; player, team, league or index; season, playoffs, career, career-playoffs, advanced, playoffs-advanced, possession, playoffs-possession, roster, salary or index |
| `name`, `team`, `url`, `playerId`, `playerUrl` | Player or team name, source page, site player id (for example `jamesle01`) and player page |
| `year_id`, `age`, `team_name_abbr`, `comp_name_abbr`, `pos` | Season (`Career` on career rows), age, team, league and position |
| `pts_per_g`, `trb_per_g`, `ast_per_g`, `per`, `ws`, `bpm`, `vorp` | Basketball per-game and advanced metrics |
| `goals`, `assists`, `corsi_pct`, `pdo`, `goalie_wins`, `save_pct_goalie` | Hockey skater, possession and goalie stats |
| `salary`, `salaryUsd` | Salary rows as printed and as a number in US dollars |
| `fullName`, `birthPlace`, `draftYear`, `draftPick`, `hallOfFame` | Bio and draft fields (Bio on) |

The dataset schema declares 223 fields (220 data columns plus `error`, `errorType` and `hint`) and five output views. Cells the source leaves empty are left out of the row.

### 🧾 Sample output of the Sports Reference Stats Scraper

Trimmed row from a cloud run on a LeBron James player page (season 2012-13). Bio and draft columns appear only when their switch is on:

```json
{
  "sport": "basketball",
  "pageType": "player",
  "recordType": "season",
  "name": "LeBron James",
  "url": "https://www.basketball-reference.com/players/j/jamesle01.html",
  "year_id": "2012-13",
  "age": "28",
  "team_name_abbr": "MIA",
  "comp_name_abbr": "NBA",
  "pos": "PF",
  "games": "76",
  "fg_pct": ".565",
  "trb_per_g": "8.0",
  "ast_per_g": "7.3",
  "pts_per_g": "26.8",
  "awards": "MVP-1,DPOY-2,AS,NBA1,DEF1",
  "scrapedAt": "2026-10-04T04:50:40.409Z"
}
```

Values are strings exactly as the site prints them (for example `.565`) unless **Numbers as numbers** is on.

### 💰 How much does it cost to scrape Basketball-Reference and Hockey-Reference?

The Actor uses pay-per-event pricing. You pay only for rows saved to the dataset. Prices are the live Free-plan prices; paid Apify plans pay less (down to $21 per 1,000 season rows).

| Event | What you are charged for | Price per 1,000 |
|---|---|---|
| `player-season-record` | One season row, including league-season table rows | $25.00 |
| `player-advanced-record` | One advanced row, including Corsi and Fenwick possession rows | $12.00 |
| `team-roster-record` | One roster row on a team page | $10.00 |
| `salary-record` | One salary row, basketball only | $12.00 |
| `player-playoff-record` | One playoff season row of a player or a whole league season | $12.00 |
| `player-career-record` | One career total row of a player page | $10.00 |
| `player-index-record` | One line of the players-by-letter list | $10.00 |

Playoff, career and player-index events are the same on every plan. Bio and draft details cost nothing extra. Error rows, 404 pages and empty tables are never charged.

Worked examples at the Free-plan price: 1,000 season rows cost 1,000 x $0.025 = $25.00, and adding 1,000 advanced rows brings it to $25.00 + 1,000 x $0.012 = $37.00.

- One team-season page with Advanced stats and Team roster on: 3 + 19 + 19 events, about $0.49.
- One player page, salaries only (1 season row + 23 salary rows): about $0.30.
- One letter of the player index, 500 index rows: 500 x $0.010 = about $5.00.

Set a maximum cost per run in the Apify run options and the Actor stops cleanly when it is reached. Free Apify users are limited to 10 rows per run.

### 🚀 How to scrape Basketball-Reference and Hockey-Reference (3 steps)

1. **Open the Actor** in Apify Console. Keep Mode on Start URLs and paste player or team URLs, or switch Mode to Whole league season and pick sport and season.
2. **Set Max Items** and switch on only the extras you need: playoffs, career, advanced, Corsi and Fenwick, roster, salaries, bio.
3. **Click Start**, then download the dataset as JSON, CSV, Excel or XML from the Output tab.

### ⚙️ Input of the Sports Reference Stats Scraper

| Field | Type | Description |
|---|---|---|
| `mode` | string | `urls` (default), `season` or `index`. |
| `startUrls` | array | Player, team, league-season or letter pages. Used in urls mode. Default: LeBron James. |
| `sport` | string | `basketball` or `hockey`. Used in season and index modes. |
| `season` | integer | Ending year, 2025 means 2024-25. Season mode. Default 2025. |
| `statType` | string | `standard`, `totals` (basketball only) or `advanced`. Season mode. |
| `playerType` | string | `skaters` or `goalies`. Hockey season mode and team pages. |
| `letter` | string | One letter a to z. Index mode. |
| `maxItems` | integer | Maximum main rows. Free users: 10. Paid users: up to 1,000,000. Extra rows are not counted. |
| `includePlayoffs`, `includeCareer`, `includeAdvanced`, `includeCorsiFenwick`, `includeRoster`, `includeSalaries` | boolean | Extra row types, all off by default. |
| `includeBio`, `numericValues` | boolean | Bio and draft details on every player row; numeric stat values. Off by default. |

```json
{
  "mode": "season",
  "sport": "hockey",
  "season": 2024,
  "statType": "standard",
  "playerType": "goalies",
  "maxItems": 50,
  "includePlayoffs": true
}
```

### 📦 Output of the Sports Reference Stats Scraper

Results go to the default dataset with five views. Error rows (for example an unsupported URL) carry `error`, `errorType` and `hint` and are never billed. If a page does not exist (404), the run ends as succeeded with zero rows and zero charge. A run that is rate limited by the site (HTTP 429) ends as succeeded, keeps the rows already delivered and writes one free error row.

### 💡 Use cases for Basketball-Reference and Hockey-Reference data

- **Model building and fantasy or betting research:** pull every season of a career, with playoffs, PER, win shares, goalie save percentage and Corsi.
- **Payroll and draft analysis:** combine rosters, salary history and draft fields.
- **Media and content:** get verified season-by-season numbers for articles.

### 🔌 Run Sports Reference Stats Scraper via API, schedule and integrations

Call it from your own code with the Apify API client:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('RecordsData/sports-reference-stats-scraper').call({
  mode: 'season',
  sport: 'basketball',
  season: 2024,
  statType: 'advanced',
  maxItems: 100,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length);
```

Schedule runs and send results to Google Sheets, Zapier, Make or n8n.

### 🤖 Sports Reference Stats Scraper MCP server for AI agents

AI agents can call this Actor through the Apify MCP server at `https://mcp.apify.com?tools=RecordsData/sports-reference-stats-scraper`. Add this to your MCP client configuration (Claude Desktop, Cursor, VS Code or any remote MCP client):

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=RecordsData/sports-reference-stats-scraper"
    }
  }
}
```

Example agent prompt:

> Use the Sports Reference Stats Scraper on https://www.basketball-reference.com/players/j/jamesle01.html with advanced stats and playoffs on and 10 rows max, then tell me which season had the highest PER.

An agent should choose this Actor when it needs season-by-season NBA or NHL stats, playoffs, career totals, advanced metrics, hockey goalies or Corsi, team rosters, NBA salaries, or a whole league season.

### ⚖️ Is it legal to scrape Basketball-Reference and Hockey-Reference?

The Actor reads publicly available statistics pages and does not log in or bypass any protection. It requests pages one at a time with a 3.3 second delay and stops when the site answers HTTP 429. You are responsible for using the data in line with the target site's terms of use and the laws that apply to you. This is an independent tool and is not affiliated with Sports Reference LLC.

### ❓ Frequently asked questions about the Sports Reference Stats Scraper

#### Which Sports Reference sites does Sports Reference Stats Scraper support?

Sports Reference Stats Scraper supports Basketball-Reference.com for the NBA and Hockey-Reference.com for the NHL. Baseball-Reference, Pro-Football-Reference and FBref sat behind a Cloudflare challenge when we checked on 2026-09-15, so they are not supported. You can mix NBA and NHL URLs in a single Start URLs list.

#### Which URLs can I use with Sports Reference Stats Scraper?

Sports Reference Stats Scraper accepts player pages such as /players/j/jamesle01.html, team-season pages such as /teams/BOS/2024.html, league-season tables such as /leagues/NBA_2025_per_game.html, and letter indexes such as /players/a/. Any other URL type returns an error row with a hint, and that error row is never billed, so a wrong link costs nothing.

#### Why did my Sports Reference Stats Scraper run return 0 results?

If the page does not exist (404), Sports Reference Stats Scraper ends as succeeded with zero rows and you are not charged. Check the error rows too, since the URL may be unsupported and a wrong link returns a free error row with a hint. A run that delivers nothing also stops after 10 pages instead of walking a long URL list.

#### Why do basketball and hockey rows have different fields in Sports Reference Stats Scraper?

Each sport tracks different statistics, so Sports Reference Stats Scraper keeps each site's own column names instead of forcing a shared schema. Basketball rows have fields such as pts_per_g, while hockey rows have goals and goalie rows have save_pct_goalie. Every row is tagged with sport, pageType and recordType. Columns that do not apply to a row type are absent: league-wide season rows carry ranker and name_display, player-page rows do not, and hockey index rows for non-NHL players have no year_min or year_max because the source lists none.

#### Why are some salary or advanced rows missing for older seasons?

The source site does not publish every number for every season, especially salaries from before the 1990s. Sports Reference Stats Scraper returns what is on the page and does not invent values. Cells the source leaves empty are left out of the row instead of being filled with a placeholder, so a missing column simply means the page has no number there.

#### Can I scrape a whole team roster with Sports Reference Stats Scraper?

Yes. Paste a team-season URL such as /teams/BOS/2024.html, switch on Team roster, and optionally Advanced stats or Playoff stats. You get one roster row per player with number, position, height, weight, birth date and college. Roster rows cost $0.010 each on the Free plan and the option works for team URLs only.

#### Do I need a proxy to run Sports Reference Stats Scraper?

No. Both Basketball-Reference and Hockey-Reference responded normally in our tests without a proxy, so Sports Reference Stats Scraper has no proxy setting to configure. It requests pages one at a time with a delay, uses no login, and needs no API key, only an Apify account.

#### How do I limit my spend on Sports Reference Stats Scraper?

Set the Max Items input and the maximum cost per run in the Apify run options. Sports Reference Stats Scraper stops cleanly when either limit is reached, with the status message "Stopped at your max charge limit". Note that Max Items counts main rows only, while advanced, roster, salary and other extra rows are not counted there, and the billed ones are extra events.

#### What happens if I send an empty or invalid input to Sports Reference Stats Scraper?

Start URLs is no longer required, and its default is the LeBron James page; season and index modes ignore it. Mistakes such as a URL that is not a Basketball-Reference or Hockey-Reference page end the run as succeeded with one free error row that has errorType invalid-input, an error and a hint.

### 🔗 Want more sports data? Other RecordsData scrapers

- [Basketball Match Stats Scraper](https://apify.com/RecordsData/basketball-match-stats-scraper)
- [Sofascore Match Stats Scraper](https://apify.com/RecordsData/sofascore-match-stats-scraper)
- [Tennis Match Stats Scraper](https://apify.com/RecordsData/tennis-match-stats-scraper)

### 🆘 Support

Found a bug or need another field? Open an issue in the Issues tab of this Actor or write to contact.punkrecordsdata@gmail.com.

Last updated: 2026-11-02

# Actor input Schema

## `mode` (type: `string`):

Start URLs: stats from player, team, season or letter pages you paste. Whole league season: every player of one NBA or NHL season. Player index: the players-by-letter list of one sport (name, id, years active).

## `startUrls` (type: `array`):

Examples: a player /players/j/jamesle01.html, a team season /teams/WSH/2024.html, a league table /leagues/NBA_2025_per_game.html or /leagues/NHL_2025_skaters.html, a letter index /players/a/. Prefilled with LeBron James.

## `sport` (type: `string`):

Basketball = NBA (basketball-reference.com), hockey = NHL (hockey-reference.com). Default: basketball.

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

The year the season ends: 2025 means season 2024-25. Basketball from 1947, hockey from 1918. Default 2025.

## `statType` (type: `string`):

Standard: per-game stats (basketball) or season stats (hockey). Totals: season totals, basketball only. Advanced: PER, win shares and BPM for basketball; Corsi, Fenwick, PDO and zone starts for hockey skaters.

## `playerType` (type: `string`):

Hockey only. In Whole league season mode it picks the league table; on team pages it picks which stat table is read. Default: skaters.

## `letter` (type: `string`):

One letter, a to z: players whose last name starts with it, for example a. Default a.

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

Maximum number of main rows (player-season rows, league-table rows or index rows). Free plan: capped at 10 rows per run. Paid plans: up to 1,000,000. Prefilled with 10. Extra rows from the options below are not counted here.

## `includeBio` (type: `boolean`):

Adds the bio block of a player page to every row of that player: full name, positions, height and weight in cm and kg, birth date and place, college, draft round, pick and year, debut. No extra charge and no extra request.

## `numericValues` (type: `boolean`):

Return stat values as numbers (0.512, 27.1) instead of text ('.512', '27.1'). Ids, names, positions, seasons and dates stay text. Off by default so existing integrations keep working.

## `includePlayoffs` (type: `boolean`):

Adds the playoff table of each page as extra rows (and playoff advanced rows if Advanced stats is on). Works on player, team and league-season pages. Billed per extra row.

## `includeCareer` (type: `boolean`):

Adds the career total rows of a player page: all-seasons, per-team and per-league rows, with seasons played. Billed per extra row.

## `includeAdvanced` (type: `boolean`):

Adds advanced metrics rows per season: PER, win shares and BPM (basketball); rate and plus-minus stats (hockey). Billed per extra row.

## `includeCorsiFenwick` (type: `boolean`):

Adds hockey possession rows per season: Corsi for and against, CF%, Fenwick, PDO and zone starts. Hockey skater and team pages only. Billed like advanced stats.

## `includeRoster` (type: `boolean`):

Adds the full roster of a team-season page, one row per player. Only applies to team links. Billed per extra row.

## `includeSalaries` (type: `boolean`):

Adds salary rows per season where published. Basketball-reference only. Billed per extra row.

## Actor input object example

```json
{
  "mode": "urls",
  "startUrls": [
    {
      "url": "https://www.basketball-reference.com/players/j/jamesle01.html"
    }
  ],
  "sport": "basketball",
  "season": 2025,
  "statType": "standard",
  "playerType": "skaters",
  "letter": "a",
  "maxItems": 10,
  "includeBio": false,
  "numericValues": false,
  "includePlayoffs": false,
  "includeCareer": false,
  "includeAdvanced": false,
  "includeCorsiFenwick": false,
  "includeRoster": false,
  "includeSalaries": false
}
```

# Actor output Schema

## `overview` (type: `string`):

Key fields: player/team, season, sport

## `fullData` (type: `string`):

Complete dataset including playoff, career, advanced, Corsi, roster and salary rows

# 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 = {
    "mode": "urls",
    "startUrls": [
        {
            "url": "https://www.basketball-reference.com/players/j/jamesle01.html"
        }
    ],
    "season": 2025,
    "letter": "a",
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("recordsdata/sports-reference-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 = {
    "mode": "urls",
    "startUrls": [{ "url": "https://www.basketball-reference.com/players/j/jamesle01.html" }],
    "season": 2025,
    "letter": "a",
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("recordsdata/sports-reference-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 '{
  "mode": "urls",
  "startUrls": [
    {
      "url": "https://www.basketball-reference.com/players/j/jamesle01.html"
    }
  ],
  "season": 2025,
  "letter": "a",
  "maxItems": 10
}' |
apify call recordsdata/sports-reference-stats-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,recordsdata/sports-reference-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/yPECcipvVpjgRXgk3/builds/s8D0YuTHx5QUOUaCj/openapi.json
