NBA Shot Chart & Shot Location Scraper
Pricing
from $0.50 / 1,000 shots
NBA Shot Chart & Shot Location Scraper
Scrape every NBA field-goal attempt with exact court coordinates: true distance, angle, zone and plot-ready x/y. Adds expected points from the league's own per-zone baseline, so you can see which shots beat the league. Filter by game, player, team or date. No API key, no login.
Pricing
from $0.50 / 1,000 shots
Rating
0.0
(0)
Developer
Elena Vance
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
8 days ago
Last modified
Categories
Share
Every NBA field-goal attempt, with where on the floor it came from — and the spatial fields the league does not publish, computed and verified against a full season.
No API key, no login, no browser. Filter by game, player, team, date or season.
What you get
One row per shot. Real output from a 2026-04-12 game:
| Date | Player | Team | Made | Zone | Our distance | NBA's | Angle | Expected pts | Pts over expected |
|---|---|---|---|---|---|---|---|---|---|
| 2026-04-12 | Luka Garza | BOS | ✅ | Above the Break 3 | 25.778 | 25 | 28.75° | 1.038 | +1.962 |
| 2026-04-12 | Franz Wagner | ORL | ✅ | Above the Break 3 | 27.777 | 27 | 27.67° | 1.038 | +1.962 |
| 2026-04-12 | Paolo Banchero | ORL | ✅ | Restricted Area | 1.985 | 1 | 40.91° | 1.342 | +0.658 |
| 2026-04-12 | Paolo Banchero | ORL | ❌ | Restricted Area | 2.766 | 2 | −77.47° | 1.342 | −1.342 |
| 2026-04-12 | Desmond Bane | ORL | ✅ | Mid-Range | 18.623 | 18 | 35.82° | 0.800 | +1.200 |
Each row also carries the raw LOC_X/LOC_Y, plot-ready coordinates for the
standard 500×470 court grid in both orientations, the game clock as elapsed
seconds, home/away and opponent, and NBA's three zone labels.
What makes this different
The distance NBA publishes is wrong by design. SHOT_DISTANCE is floored,
not rounded — verified across all 219,160 shots of the 2025-26 regular
season, where floor matched every row and round matched 54%. Every published
distance therefore runs short by up to a foot, always in the same direction. You
get the exact value and NBA's integer beside it.
Expected points, from NBA's own numbers. The endpoint ships a league FG% table per zone alongside every query. Joining it turns a made/missed flag into points over expectation — shot-making separated from shot selection, with no model to trust. The baseline is fetched once for the whole season, deliberately: scoring a single day's shots against that same day's average drives the number to zero by construction.
A season query that is actually complete. shotchartdetail truncates at
102,400 rows and reports nothing — a 2023-24 season request returns a valid 200
whose data stops on 17 January. Summed month by month the same season holds
219,527 shots, so 53% is dropped silently. This Actor splits every
league-wide query into windows before asking, and re-splits anything that still
comes back at the cap.
Home/away that is right. The obvious source for it, MATCHUP, is usually
written from the row team's point of view — in one captured slate, 17 rows of 18
were, and Orlando's own row read MEM @ ORL. Deriving the side from that string
mislabels roughly one row in twenty. This uses the shot row's own home-team
field instead.
Works in the offseason. The default run needs no input and returns the most recent day actually played, resolving back a season when the newest one has been announced but not started.
Modes
| Give it | You get |
|---|---|
| nothing | The most recent day of play, league-wide |
gameIds | Every shot in those games (~180 each) — the cheapest way to run it |
playerIds | That player's whole season (~1,300 shots) |
playerIds + gameIds | Just those players, in just those games |
teamIds | That team's season (~7,400 shots) |
dateFrom / dateTo | League-wide over that window, split automatically |
season + seasonType | Regular season, playoffs, pre-season, play-in or All-Star |
maxItems caps both the result count and the cost. IDs are NBA Stats IDs —
201939 is Stephen Curry, 1610612744 is Golden State.
Why this is cheap to run
Runs in 256 MB with no browser and no proxy on a healthy run, so the
apify-actor-start fee is charged once rather than four times. A default run
finishes in 3.5 seconds; peak memory plateaus around 160 MB no matter how
long the run is.
What this Actor does not return
- Anything that is not a field-goal attempt. Made and missed shots only — no free throws, rebounds, assists or turnovers.
- Modelled shot quality.
expectedPointsis the league's average from that zone. It is an honest baseline, not an xG-style model — defender distance and shot clock are not in this feed, so no number here pretends to include them. - Player tracking or defender data. Served by different
stats.nba.comendpoints with a different shape; not joined here. - Seasons before 1996-97. Shot locations do not exist before then, and the Actor refuses those rather than returning an empty result that looks like a quiet day.
SHOT_ATTEMPTED_FLAG/GRID_TYPE. Constant on every row, so emitting them would cost you storage and tell you nothing.
Example runs
{ "gameIds": ["0022500578"], "maxItems": 200 }
{ "playerIds": [201939], "season": "2025-26" }
{ "teamIds": [1610612744], "season": "2025-26", "seasonType": "Playoffs" }
{ "dateFrom": "2026-01-15", "dateTo": "2026-01-31", "maxItems": 20000 }
Notes
Unofficial and not affiliated with or endorsed by the NBA. Data comes from the same public endpoints nba.com's own statistics pages read.
docs/architecture.md records every measurement behind the claims above,
including the header/TLS matrix, the cap proof and the full-season geometry
verification.