API reference¶
Everything listed here is importable from fc_clubs_api and follows semantic
versioning. Modules starting with _ are internal.
Client¶
FC27API ¶
FC27API(platform: Union[Platform, str] = Platform.GEN5, timeout: float = 10, timezone: TimezoneLike = 'UTC', output: OutputLike = OutputFormat.RECORDS, headers: Optional[Mapping[str, str]] = None)
Client for EA Sports FC 27's Pro Clubs API.
Every data method can return three formats, chosen with output on the
client (the default for all calls) or on any single call:
"raw": EA's JSON exactly as received."records": a list of clean, typed, flat dicts (no pandas needed)."dataframe": a pandas DataFrame with the same columns asrecords.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
platform
|
Union[Platform, str]
|
EA platform id. |
GEN5
|
timeout
|
float
|
Seconds to wait for EA before raising
:class: |
10
|
timezone
|
TimezoneLike
|
IANA name ( |
'UTC'
|
output
|
OutputLike
|
Default output format: |
RECORDS
|
headers
|
Optional[Mapping[str, str]]
|
Extra or replacement HTTP headers, merged over the built-in browser-like headers EA requires. |
None
|
Example
get_json ¶
GET any endpoint and return EA's JSON untouched.
The escape hatch for endpoints or parameters this client doesn't wrap.
platform is added automatically.
Example
Raises:
| Type | Description |
|---|---|
FC27APIError
|
EA could not be reached, refused, or sent non-JSON. |
search_club_by_name ¶
search_club_by_name(club_name: str, all_columns: bool = False, *, scope: Union[LeaderboardScope, str] = LeaderboardScope.ALL_TIME, max_result_count: Optional[int] = None, output: Optional[OutputLike] = None) -> Any
Clubs whose name contains club_name, one row per club.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
club_name
|
str
|
Text to search for (case-insensitive, partial match). |
required |
all_columns
|
bool
|
Keep every field EA sends, with EA's names. |
False
|
scope
|
Union[LeaderboardScope, str]
|
|
ALL_TIME
|
max_result_count
|
Optional[int]
|
Sent as EA's |
None
|
output
|
Optional[OutputLike]
|
Override the client's output format. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
League record, divisions, points and |
find_club_id ¶
Return the id of the club called club_name.
Picks the club whose name matches exactly (ignoring case), or the only search result.
Raises:
| Type | Description |
|---|---|
ClubNotFoundError
|
No club matched (a |
AmbiguousClubError
|
Several clubs matched and none exactly; the
error's |
get_club_details ¶
get_club_details(club_id: ClubId, all_columns: bool = False, *, output: Optional[OutputLike] = None) -> Any
One row: club name, ids, stadium and crestUrl.
all_columns=True adds every kit field (customKit.*). Raw output
is EA's {"<clubId>": {...}} dict.
get_club_overall_stats ¶
get_club_overall_stats(club_id: ClubId, all_columns: bool = False, *, output: Optional[OutputLike] = None) -> Any
One row: record, goals, streaks, skill rating, promotions.
all_columns=True adds lastMatch0-9 / lastOpponent0-9 and
division finishes; see the endpoint reference for their meaning.
get_playoff_achievements ¶
Playoff achievements. EA has only returned an empty list so far.
get_member_stats ¶
get_member_stats(club_id: ClubId, all_columns: bool = False, *, output: Optional[OutputLike] = None) -> Any
One row per club member with their current-season stats.
Raw output is EA's {"members": [...], "positionCount": {...}}.
get_member_career_stats ¶
get_member_career_stats(club_id: ClubId, all_columns: bool = False, *, output: Optional[OutputLike] = None) -> Any
One row per club member with their career totals at this club.
get_club_matches ¶
get_club_matches(club_id: ClubId, match_type: Union[MatchType, str] = MatchType.LEAGUE, count: int = 10, *, output: Optional[OutputLike] = None) -> Any
One row per match seen from club_id's side, newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
club_id
|
ClubId
|
The club whose matches to fetch. |
required |
match_type
|
Union[MatchType, str]
|
|
LEAGUE
|
count
|
int
|
How many recent matches to ask for. EA never sends more than 10. |
10
|
output
|
Optional[OutputLike]
|
Override the client's output format. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
Opponent, score, |
Any
|
both crests. Raw output is EA's list of match objects. |
get_match_players ¶
get_match_players(club_id: ClubId, match_type: Union[MatchType, str] = MatchType.LEAGUE, count: int = 10, both_teams: bool = False, all_columns: bool = False, *, include_events: bool = False, output: Optional[OutputLike] = None) -> Any
One row per player per match: rating, goals, passes, tackles, ...
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
club_id
|
ClubId
|
The club whose matches to fetch. |
required |
match_type
|
Union[MatchType, str]
|
As in :meth: |
LEAGUE
|
count
|
int
|
As in :meth: |
10
|
both_teams
|
bool
|
Include the opponents' players (tell them apart by |
False
|
all_columns
|
bool
|
Keep every field EA sends, with EA's names. |
False
|
include_events
|
bool
|
Add the decoded event stats from
:data: |
False
|
output
|
Optional[OutputLike]
|
Override the client's output format. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
Player rows. Raw output is EA's list of match objects (the same as |
Any
|
meth: |
Output formats and enums¶
OutputFormat ¶
Bases: str, Enum
What a :class:~fc_clubs_api.FC27API method returns.
Members compare equal to their string values, so output="records" and
output=OutputFormat.RECORDS are interchangeable.
RAW
class-attribute
instance-attribute
¶
EA's JSON exactly as received (dict / list). No renaming, no type
conversion. Only JSON null is replaced by [].
RECORDS
class-attribute
instance-attribute
¶
A list of flat dict rows: nested fields flattened, numeric text
converted to int/float, timestamps as timezone-aware datetime,
curated camelCase columns. Needs no third-party packages.
DATAFRAME
class-attribute
instance-attribute
¶
A pandas.DataFrame with exactly the columns of RECORDS.
Requires pip install "fc-clubs-api[pandas]".
MatchType ¶
Bases: str, Enum
The matchType parameter of clubs/matches.
LeaderboardScope ¶
Bases: str, Enum
Which leaderboard :meth:FC27API.search_club_by_name searches.
Platform ¶
Bases: str, Enum
The platform parameter. Only common-gen5 has been verified for FC 27.
GEN5
class-attribute
instance-attribute
¶
PS5, Xbox Series X|S and PC (cross-play).
Exceptions¶
exceptions ¶
Exceptions raised by fc_clubs_api.
Everything that goes wrong while talking to EA is an :class:FC27APIError, so a
single except FC27APIError catches all network and response problems. Bad
input from the caller (an unknown match type, a club name that doesn't match
exactly one club) raises a :class:ValueError subclass instead.
FC27APIError ¶
Bases: Exception
Base class: EA could not be reached, refused the request, or sent bad data.
Attributes:
| Name | Type | Description |
|---|---|---|
url |
The full request URL, when known. |
FC27HTTPError ¶
Bases: FC27APIError
EA answered with an HTTP error status (403 from Akamai is the common one).
Attributes:
| Name | Type | Description |
|---|---|---|
status_code |
The HTTP status code EA returned. |
FC27ConnectionError ¶
FC27ResponseError ¶
ClubNotFoundError ¶
Bases: ValueError
No club matched the name passed to :meth:FC27API.find_club_id.
AmbiguousClubError ¶
Bases: ValueError
Several clubs matched and none had exactly the requested name.
Attributes:
| Name | Type | Description |
|---|---|---|
candidates |
|
Match events¶
events ¶
Decode the per-player match event counters into named stats.
Every player in a clubs/matches response carries match_event_aggregate_0
to match_event_aggregate_3: comma-separated event_id:count pairs such as
"215:21,216:5,30:8". EA does not document the event ids.
The mapping below comes from the community research at
https://github.com/Interactive-63/eafc-pro-clubs-api-research (FC 27, validated
against ~51,000 player-match rows). Only mappings that research rates as
confirmed / high confidence are decoded here. Partial and exploratory
mappings are listed in docs/match-events.md but deliberately not decoded.
Example
from fc_clubs_api.events import parse_event_aggregates, decode_events player = {"match_event_aggregate_0": "215:21,216:5,30:8", "match_event_aggregate_1": ""} parse_event_aggregates(player) {215: 21, 216: 5, 30: 8} decode_events(player)["passesCompletedForward"] 8
EVENT_STATS
module-attribute
¶
EVENT_STATS: Tuple[EventStat, ...] = (_stat('passesCompleted', 'Passes completed (excludes offside passes)', 215), _stat('passesFailed', 'Passes failed', 216), _stat('passesCompletedForward', 'Completed passes played forward', 30), _stat('passesCompletedBackward', 'Completed passes played backward', 32), _stat('passesCompletedSideways', 'Completed passes played sideways', 34), _stat('passesCompletedDirectionUnknown', 'Completed passes with no direction event', 215, minus=(30, 32, 34)), _stat('passesCompletedShort', 'Completed short passes', 24), _stat('passesCompletedMedium', 'Completed medium passes', 26), _stat('passesCompletedLong', 'Completed long passes', 28), _stat('passesCompletedCross', 'Completed crosses (includes set pieces)', 36), _stat('passesCompletedLengthUnknown', 'Completed passes with no length event', 215, minus=(24, 26, 28, 36)), _stat('passesFailedForward', 'Failed passes played forward', 31), _stat('passesFailedBackward', 'Failed passes played backward', 33), _stat('passesFailedSideways', 'Failed passes played sideways', 35), _stat('passesFailedDirectionUnknown', 'Failed passes with no direction event', 216, minus=(31, 33, 35)), _stat('passesFailedShort', 'Failed short passes', 25), _stat('passesFailedMedium', 'Failed medium passes', 27), _stat('passesFailedLong', 'Failed long passes', 29), _stat('passesFailedCross', 'Failed crosses (open play only)', 37), _stat('passesFailedLengthUnknown', 'Failed passes with no length event', 216, minus=(25, 27, 29, 37)), _stat('firstTimePasses', 'First-time passes', 143), _stat('switchesOfPlay', 'Switch-of-play passes', 144), _stat('flairPasses', 'Fancy / flair passes', 147), _stat('throughPasses', 'Successful through balls', 152), _stat('offsidePasses', 'Passes caught offside', 153), _stat('shots', 'Shots (on + off target)', 217, 218), _stat('shotsOnTarget', 'Shots on target (blocked shots can fold in)', 217), _stat('shotsOnTargetInsideBox', 'Shots on target from inside the box', 13), _stat('shotsOnTargetOutsideBox', 'Shots on target from outside the box', 18), _stat('shotsOffTarget', 'Shots off target', 218), _stat('shotsOffTargetInsideBox', 'Shots off target from inside the box', 14), _stat('shotsOffTargetOutsideBox', 'Shots off target from outside the box', 19), _stat('shotsSaved', 'Shots saved', 202), _stat('goals', 'Goals', 214), _stat('goalsFirstTime', 'Goals from first-time shots (a tag, not a partition)', 128), _stat('goalsWeakFoot', 'Goals with the weak foot (a tag, not a partition)', 131), _stat('goalsOffPost', 'Goals in off the post (a tag, not a partition)', 136), _stat('assists', 'Assists', 11), _stat('secondAssists', 'Second assists', 115), _stat('throughBallAssists', 'Assists from through balls (a tag, not a partition)', 118), _stat('dribblesCompleted', 'Dribbles completed', 174), _stat('dribblesCarried', 'Dribbles carried', 97), _stat('dribblesBeat', 'Players beaten without a skill move', 112, minus=(38,)), _stat('dribblesSkillMoveBeat', 'Players beaten with a skill move', 38), _stat('tacklesWon', 'Tackles won', 0), _stat('tacklesLost', 'Tackles lost', 1), _stat('standingTacklesWon', 'Standing tackles won', 229), _stat('slidingTacklesWon', 'Sliding tackles won', 230), _stat('cleanTackles', 'Successful tackles on the ball', 164), _stat('dangerousTackles', 'Dangerous tackles', 163), _stat('interceptions', 'Interceptions', 6), _stat('opponentsDispossessed', 'Opponents dispossessed', 158), _stat('crossesBlocked', 'Crosses blocked', 156), _stat('aerialDuelsWonAttacking', 'Aerial duels won when attacking', 265), _stat('aerialDuelsWonDefending', 'Aerial duels won when defending', 266), _stat('goodDirectionSaves', 'Saves diving the right way', 267), _stat('possessionLostDefensiveThird', 'Possession lost in the defensive third', 105), _stat('possessionLostMiddleThird', 'Possession lost in the middle third', 106), _stat('possessionLostAttackingThird', 'Possession lost in the attacking third', 107), _stat('possessionWonDefensiveThird', 'Possession won in the defensive third', 108), _stat('possessionWonMiddleThird', 'Possession won in the middle third', 109), _stat('possessionWonAttackingThird', 'Possession won in the attacking third', 110), _stat('foulsConceded', 'Fouls conceded, all areas', 2, 3), _stat('foulsConcededDefensiveThird', 'Fouls conceded around the defensive third', 3), _stat('foulsWon', 'Times this player was fouled', 4), _stat('yellowCards', 'Yellow cards (immediate + delayed)', 95, 213), _stat('yellowCardsImmediate', 'Yellow cards shown as play stopped', 95), _stat('yellowCardsDelayed', 'Yellow cards shown after advantage was played', 213), _stat('penaltiesConceded', 'Penalties conceded', 94), _stat('cornersConceded', 'Corners conceded', 10), _stat('cornersTaken', 'Corners taken', 145), _stat('inPosition', "Times the feedback praised this player's positioning", 111), _stat('outOfPosition', 'Times out of position, any severity', 219), _stat('outOfPositionSeverity1', 'Out of position, severity 1 of 5', 99), _stat('outOfPositionSeverity2', 'Out of position, severity 2 of 5', 100), _stat('outOfPositionSeverity3', 'Out of position, severity 3 of 5', 101), _stat('outOfPositionSeverity4', 'Out of position, severity 4 of 5', 102), _stat('outOfPositionSeverity5', 'Out of position, severity 5 of 5', 103), _stat('feedbackUseTheBall', "Feedback: 'Use the ball'", 212), _stat('feedbackPickYourPass', "Feedback: 'Pick your pass'", 207), _stat('feedbackNoGoodOption', "Feedback: 'No good option'", 175), _stat('feedbackGoodOptionTaken', "Feedback: 'Good option taken'", 176), _stat('feedbackBestOptionTaken', "Feedback: 'Best option taken'", 177), _stat('feedbackShouldHavePassedElsewhere', "Feedback: 'Should have passed elsewhere'", 182), _stat('feedbackChoseToPass', "Feedback: 'Chose to pass'", 183))
EventStat
dataclass
¶
A named stat computed from event counters.
The value is sum(add) - sum(subtract), floored at 0 when subtract is
used (derived "remainder" stats can't go negative).
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Output column name (camelCase). |
description |
str
|
What the stat counts, with any caveat from the research. |
add |
Tuple[int, ...]
|
Event ids whose counts are added. |
subtract |
Tuple[int, ...]
|
Event ids whose counts are subtracted. |
parse_event_aggregates ¶
Merge a player's match_event_aggregate_* strings into {event_id: count}.
Works on a raw player dict from clubs/matches (or any dict that has the
aggregate fields). Empty buckets are skipped, ids appearing in several
buckets are summed, and malformed pairs are ignored. An id that is absent
means a count of 0.
decode_events ¶
Return every stat in :data:EVENT_STATS for one player, by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
player
|
Mapping[str, Any]
|
A raw player dict from |
required |
Images and crests¶
assets ¶
URLs for club crests, division badges and reputation tier icons.
These images are served by EA's content CDNs, not by the Pro Clubs API, and need no special headers. All URLs below were checked to return a PNG on 2026-10-02.
Crest rule (from https://github.com/Interactive-63/eafc-pro-clubs-api-research):
a club in clubs/matches has a TEAM field that is normally the crest id to
use. It equals teamId when the club uses a real-world badge and
customKit.crestAssetId when it uses a custom one. In rare cases neither
matches and the TEAM URL 404s; fall back to crestAssetId or the
not-found crest. Outside clubs/matches (e.g. clubs/info) there is no
TEAM field; there customKit.selectedKitType == "1" was observed to mean
a custom crest and "0" a real badge.
crest_url ¶
256x256 crest PNG for a TEAM, teamId or crestAssetId value.
crest_url_candidates ¶
crest_url_candidates(*, team: Optional[IdLike] = None, team_id: Optional[IdLike] = None, crest_asset_id: Optional[IdLike] = None, selected_kit_type: Optional[IdLike] = None) -> List[str]
Crest URLs to try in order, ending with the not-found crest.
Useful for front ends: show the first URL and fall back to the next one when an image fails to load.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
team
|
Optional[IdLike]
|
|
None
|
team_id
|
Optional[IdLike]
|
|
None
|
crest_asset_id
|
Optional[IdLike]
|
|
None
|
selected_kit_type
|
Optional[IdLike]
|
|
None
|
club_crest_url ¶
club_crest_url(*, team: Optional[IdLike] = None, team_id: Optional[IdLike] = None, crest_asset_id: Optional[IdLike] = None, selected_kit_type: Optional[IdLike] = None) -> str
The single best crest URL for a club (the first of :func:crest_url_candidates).
division_crest_url ¶
Division badge PNG. division is 1 (top) to 6.
reputation_tier_url ¶
Reputation tier icon PNG. tier is 0 to 3 (reputationtier in EA's data).
kit_color_hex ¶
Convert EA's decimal RGB kit/crest colour to hex: "16777215" -> "#ffffff".
Constants¶
HEADERS
module-attribute
¶
HEADERS = {'accept': 'application/json', 'accept-language': 'en-US,en;q=0.9', 'sec-ch-ua': '"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"', 'sec-fetch-site': 'same-origin', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'}