Shot chart of a finished game: court x/y (percent of the full court), result and shooter.
Endpoint: GET https://fibalivestats.dcd.shared.geniussports.com/data/{match_id}/data.json
x and y are percentages (0-100) of the full court length and width, NOT normalised to one basket (a team attacks one end in periods 1-2 and the other in 3-4); the origin corner is unverified. result is 1 for a made shot, 0 for a miss; action_number joins the play-by-play row.
Arguments
- match_id
Genius match id (
match_idofgenius_schedule(),genius_widget_games()orgenius_competition_games()), e.g. 2902690.- return_parsed
Parse into a tibble (default
TRUE);FALSEreturns the parsed body or page record (a list):data.jsonas served plus_request(the ids asked for), or the sdv-internal-refs capture shape of a page.
Value
A wehoop_data tibble (a zero-row tibble with these columns when Genius answers
an empty valid page), or the raw list when return_parsed = FALSE:
| col_name | type | description |
| match_id | integer | Genius match id of the game. data.json does not carry it: it is the match_id asked for, stamped by the runtime (the global match sequence shared by every Genius customer). |
| tno | integer | Team number within this game: 1 = the team listed first (home on the hosted schedule and widget), 2 = the other, 0 = no team. In-game only: not a team id. |
| pno | integer | Player number within this game, the key of the team's player map; 0 = no player. In-game only: not a person id and not the shirt number. |
| action_number | integer | Action sequence number, unique within the game (gaps where actions were deleted); a shot joins its play-by-play row on it. Null in the 2014-15 feed. |
| period | integer | Period number; restarts at 1 for the first overtime (read it with period_type). |
| period_type | character | Period type: REGULAR or OVERTIME. |
| action_type | character | Shot value: 2pt or 3pt |
| sub_type | character | Shot type (layup, drivinglayup, jumpshot, pullupjumpshot, dunk, ...) |
| result | integer | Shot result: 1 = made, 0 = missed. |
| x | double | Court x, PERCENT of the full court length (0-100, float), along the long axis. Not normalised to one basket: a team attacks one end in periods 1-2 and the other end in periods 3-4 (ESAKE 2902690: team 1 mean x 90 and 86 in P1-P2, 11 and 19 in P3-P4). Which end x = 0 is, and which end team 1 starts at, are unverified |
| y | double | Court y, PERCENT of the full court width (0-100, float), across the court (observed 1.7-97.2). Origin side unverified |
| player | character | Shooter's display name as the org enters it, in the local script when it uses one. |
| shirt_number | character | Shooter's shirt number (string) |
Details
Access. The Genius hosts publish no robots.txt and no terms. CloudFront refuses
datacenter IPs on the LiveStats and hosted hosts: from a server set
options(wehoop.proxy = ...) or the GENIUS_PROXY environment variable to an http(s)
proxy URL (never printed or logged). Requests are spaced at least one second apart
per host; keep crawls slow and incremental. Some league sites (the NZ NBL) forbid AI
crawlers in their own robots.txt, which does not cover these hosts. Unofficial use of a
public source: payloads are not redistributed as release assets.
Org and season. Every competition is an ORG code plus a competition id
(comp_id): genius_competitions() lists an org's competitions with the sdv END-year
season (a START-year org lists 2025-26 as season 2026), genius_match_competition()
maps a game to its competition, and data.json itself names no date, competition or
season. The hosted ORG can differ from the LiveStats ORG of /u/ (NZNBL vs NZN). Match
ids are one global sequence across every Genius customer. data.json carries no global
team or player id: tno (1 or 2) and pno are in-game numbers; only finished games
are read.
Errors (classed conditions, all inheriting wehoop_error): wehoop_no_data when
Genius has nothing for the request (HTTP 404, the S3 AccessDenied 403, an empty 200
from the hosted host, an empty recent-games window, a schedule or widget with no
matches); wehoop_bad_request for HTTP 400 / 422 or a URL that is not a Genius route
(e.g. a non-numeric id); wehoop_asset_fetch when the answer is unknown: the CloudFront
"Request blocked" 403, a rate limit or 5xx, the intermittent HTTP 500 "A website error
has occurred" page (transient = TRUE: retry later), a dead connection, an error
envelope, a cut-off page, an unfinished game, or a body for another match, competition
or ORG.
genius_shots(match_id = 2902690)See also
Other Genius Functions:
genius_competition_games(),
genius_competitions(),
genius_game(),
genius_game_crosswalk(),
genius_match_competition(),
genius_pbp(),
genius_player_box(),
genius_schedule(),
genius_schedule_embed(),
genius_team_box(),
genius_team_crosswalk(),
genius_widget_games(),
lfb_competition,
wbbl_competition
