EA Sports FC has no public API for Pro Clubs, the mode where each player controls one footballer in an 11-a-side club. Yet stats sites, Discord bots and league tools for it are everywhere. They all read the same data: the public JSON endpoints behind EA's Clubs website, under https://proclubs.ea.com/api/fc/.
I wrapped those endpoints in an open-source TypeScript SDK, proclubs-sdk. This post covers what the endpoints look like and what breaks when you call them yourself.
Ten endpoints, two naming traps
Every endpoint is a GET with a query string. No API key, no EA account, no cookie.
| Endpoint | Query parameters |
|---|---|
allTimeLeaderboard/search | platform, clubName |
clubs/info | platform, clubIds |
clubs/overallStats | platform, clubIds |
club/playoffAchievements | platform, clubId |
members/stats | platform, clubId |
members/career/stats | platform, clubId |
clubs/matches | platform, clubIds, matchType, maxResultCount |
allTimeLeaderboard | platform |
currentSeasonLeaderboard | platform |
currentSeasonLeaderboard/search | platform, clubName |
Two details cost me time:
- The playoff route is
club/playoffAchievements, singular. Every other club route starts withclubs/. -
clubIdsis plural but takes a single ID. Send two IDs separated by a comma and EA answersInvalid query parameter: clubIds.
And one limit: clubs/matches returns at most 10 matches per type, even with maxResultCount=25. If you want a full season, store matches as they come in.
The full endpoint reference has the response shapes and platform values.
curl gets a reset, fetch gets JSON
From a home connection, a browser and Node's built-in fetch get the JSON, while curl gets an HTTP/2 stream reset and hangs over HTTP/1.1. From servers, developers report intermittent 403s and timeouts.
The SDK's default transport uses Impit with a Chrome network profile, so its requests look like the ones the website sends. The transport is injectable: if plain fetch works from where you deploy, pass that instead.
The payloads drift every September
EA ships a new game every year, and the same /api/fc/ routes start serving it. Nothing announces what changed. With FC 27, two things changed for API consumers:
-
common-gen4, the PS4 and Xbox One pool, now returns HTTP 400 on every route. - Club IDs did not carry over. An ID that pointed at one club last season can point at another one now, so search by name again after each release.
Every response in the SDK goes through a Zod schema. The schemas use z.looseObject, so fields EA adds are kept instead of being stripped, and a separate compatibility probe compares live responses with the known contract and reports drift per endpoint. That probe is how I found out about common-gen4.
I did not remove common-gen4 from the types. The SDK still accepts it and emits a one-time DeprecationWarning, so existing code keeps compiling and the HTTP 400 comes with an explanation.
Codes that decode
A lot of the data is codes. Numeric fields arrive as strings or numbers depending on the endpoint, and some values only make sense with a lookup table: proNationality: "115" is Gabon.
Regions were the fun one. regionId: 5457237 looked like a database ID until I printed it in hex: 0x534555. Those three bytes are ASCII for SEU, Southern Europe. 4543827 is EUS, the US East Coast. All eleven region IDs the SDK knows follow the same packing.
const decodeRegion = (id: number): string => { let code = '' for (let n = id; n > 0; n = Math.floor(n / 256)) { code = String.fromCharCode(n % 256) + code } return code } decodeRegion(5457237) // 'SEU' decodeRegion(4543827) // 'EUS' Player platforms are hidden too. In league and playoff matches, each player has a namespace: 1 for PlayStation, 2 for Xbox, 3 for PC, 4 for Nintendo. Friendly matches always send "0", so they never tell you who plays on what.
The SDK keeps every raw value and adds a readable one next to it: nationality: { id: "115", label: "Gabon", isoCode: "GA" }, regionLabel: "Southern Europe", derivedLabels.exactPlatform: "ps5".
Using it
import { ProClubsClient } from 'proclubs-sdk' const proclubs = new ProClubsClient({ cache: true }) const [club] = await proclubs.clubs.search({ name: 'HEMLE FC' }) if (club) { const [squad, matches] = await Promise.all([ proclubs.members.stats({ clubId: club.clubId }), proclubs.matches.list({ clubId: club.clubId, type: 'leagueMatch', limit: 5 }), ]) console.log(squad.members.map((member) => member.name)) console.log(matches.length) } It needs Node.js 22.18 or newer and ESM, and it runs server-side only. Errors are typed classes, retries are bounded, requests can be cancelled with an AbortSignal, and the memory cache is opt-in. MIT licensed.
Be gentle with it
These endpoints belong to EA and come with no support promise. The SDK sends no credentials, and it is built for low request volumes, not for scraping. Cache what you can.
- Docs: https://proclubs-sdk.mintlify.app/docs
- Endpoint reference: https://proclubs-sdk.mintlify.app/docs/reference/ea-endpoints
- GitHub: https://github.com/alex-jordan547/proclubs-sdk
- npm: https://www.npmjs.com/package/proclubs-sdk
If you have built something on Pro Clubs data, tell me about it in the show and tell discussion.

