Skip to content

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 as records.

Parameters:

Name Type Description Default
platform Union[Platform, str]

EA platform id. "common-gen5" (PS5 / Xbox Series / PC) is the only one verified for FC 27.

GEN5
timeout float

Seconds to wait for EA before raising :class:~fc_clubs_api.FC27ConnectionError.

10
timezone TimezoneLike

IANA name ("Europe/Istanbul") or tzinfo used for match timestamps in records/dataframe output. Default UTC.

'UTC'
output OutputLike

Default output format: "records" (default), "dataframe" or "raw", or an :class:~fc_clubs_api.OutputFormat.

RECORDS
headers Optional[Mapping[str, str]]

Extra or replacement HTTP headers, merged over the built-in browser-like headers EA requires.

None
Example
api = FC27API(output="records", timezone="Europe/London")
club_id = api.find_club_id("Example FC")
latest = api.get_club_matches(club_id)[0]
print(latest["opponentName"], latest["result"])

timezone property writable

timezone: TimezoneLike

Timezone for match timestamps (as passed in).

get_json

get_json(endpoint: str, params: Optional[Mapping[str, Any]] = None) -> Any

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
api.get_json("clubs/info", {"clubIds": 1001})
# {"1001": {"name": "Example FC", "clubId": 1001, ...}}

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" (allTimeLeaderboard/search) or "current_season" (currentSeasonLeaderboard/search).

ALL_TIME
max_result_count Optional[int]

Sent as EA's maxResultCount. It is not an exact limit: EA returned roughly half as many clubs as asked (1 -> 0, 10 -> ~4, 50 -> 11-18). Omitted, EA behaves like ~10.

None
output Optional[OutputLike]

Override the client's output format.

None

Returns:

Type Description
Any

League record, divisions, points and crestUrl per club.

find_club_id

find_club_id(club_name: str) -> int

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 ValueError).

AmbiguousClubError

Several clubs matched and none exactly; the error's candidates lists (id, name) pairs (a ValueError).

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

get_playoff_achievements(club_id: ClubId, *, output: Optional[OutputLike] = None) -> Any

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]

"leagueMatch", "friendlyMatch" or "playoffMatch" (or a :class:~fc_clubs_api.MatchType).

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, result (win/draw/loss), dnf and

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:get_club_matches.

LEAGUE
count int

As in :meth:get_club_matches.

10
both_teams bool

Include the opponents' players (tell them apart by clubId).

False
all_columns bool

Keep every field EA sends, with EA's names.

False
include_events bool

Add the decoded event stats from :data:fc_clubs_api.events.EVENT_STATS (passesCompletedForward, possessionWonAttackingThird, ...). Where EA already sends a field of the same name (goals, assists, shots), EA's value is kept.

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:get_club_matches with output="raw").

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

RAW = 'raw'

EA's JSON exactly as received (dict / list). No renaming, no type conversion. Only JSON null is replaced by [].

RECORDS class-attribute instance-attribute

RECORDS = 'records'

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

DATAFRAME = 'dataframe'

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.

ALL_TIME class-attribute instance-attribute

ALL_TIME = 'all_time'

allTimeLeaderboard/search: totals across every season.

CURRENT_SEASON class-attribute instance-attribute

CURRENT_SEASON = 'current_season'

currentSeasonLeaderboard/search: totals for the running season only.

Platform

Bases: str, Enum

The platform parameter. Only common-gen5 has been verified for FC 27.

GEN5 class-attribute instance-attribute

GEN5 = 'common-gen5'

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

FC27APIError(message: str, url: Optional[str] = None)

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

FC27HTTPError(status_code: int, url: str)

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

FC27ConnectionError(message: str, url: Optional[str] = None)

Bases: FC27APIError

EA could not be reached: no network, DNS failure or timeout.

FC27ResponseError

FC27ResponseError(message: str, url: Optional[str] = None)

Bases: FC27APIError

EA answered, but the body was not valid JSON (often an HTML block page).

ClubNotFoundError

Bases: ValueError

No club matched the name passed to :meth:FC27API.find_club_id.

AmbiguousClubError

AmbiguousClubError(message: str, candidates: List[Tuple[int, str]])

Bases: ValueError

Several clubs matched and none had exactly the requested name.

Attributes:

Name Type Description
candidates

(club_id, club_name) pairs for every match, in EA's order.

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

EventStat(name: str, description: str, add: Tuple[int, ...], subtract: Tuple[int, ...] = ())

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.

formula property

formula: str

Human-readable formula, e.g. event_215 - (event_30 + event_32).

compute

compute(counts: Mapping[int, int]) -> int

Evaluate the stat for one player's parsed event counts.

parse_event_aggregates

parse_event_aggregates(player: Mapping[str, Any]) -> Dict[int, int]

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

decode_events(player: Mapping[str, Any]) -> Dict[str, int]

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 clubs/matches (with the match_event_aggregate_* fields), or an already parsed {event_id: count} mapping from :func:parse_event_aggregates.

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

crest_url(asset_id: IdLike) -> str

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]

clubs[id].TEAM from clubs/matches (best source).

None
team_id Optional[IdLike]

teamId from the club details.

None
crest_asset_id Optional[IdLike]

customKit.crestAssetId from the club details.

None
selected_kit_type Optional[IdLike]

customKit.selectedKitType; 1 puts the custom crest before the real badge when team is unknown.

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_crest_url(division: IdLike) -> str

Division badge PNG. division is 1 (top) to 6.

reputation_tier_url

reputation_tier_url(tier: IdLike) -> str

Reputation tier icon PNG. tier is 0 to 3 (reputationtier in EA's data).

kit_color_hex

kit_color_hex(value: IdLike) -> str

Convert EA's decimal RGB kit/crest colour to hex: "16777215" -> "#ffffff".

Constants

BASE_URL module-attribute

BASE_URL = 'https://proclubs.ea.com/api/fc'

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'}