Teams, players & transfers#
Teams#
GET /api/v2/teams/
| Param | Type | Description |
|---|---|---|
country_code |
string | ISO country code, e.g. AR |
league_id / season_id |
int | Teams competing in a league/season |
in_competition |
bool | Only teams currently in an active competition |
is_women |
bool | Women's teams |
name |
string | Name search |
limit / offset |
int | Pagination |
GET /api/v2/teams/{id}/ # profile: venue, coach, colours, country
GET /api/v2/teams/{id}/squad/ # current squad, ordered by position then shirt number
GET /api/v2/teams/{id}/fixtures/ # date_from, date_to, league_id, status
GET /api/v2/teams/{id}/form/ # last, league_id, venue — see Rolling form
GET /api/v2/teams/{id}/social/ # curated tweets/videos (type filter)
curl -H "Authorization: Token YOUR_API_KEY" \
"https://sports.bzzoiro.com/api/v2/teams/755/fixtures/?status=upcoming&limit=5"
Matching a team to a country#
Every team carries country_code next to country:
{"id": 932, "name": "Türkiye", "country": "Turkey", "country_code": "TR"}
Match on the code, not the name. The name moves — "Turkey" became "Türkiye" and every name-matching integration broke the day it did. The code does not.
Two things it is not, so nobody builds on a wrong assumption:
- It is not strictly ISO 3166-1, and we are not going to call it that. It
agrees with ISO wherever ISO has a code, and uses the customary one where it
does not: England
EN, ScotlandSX, WalesWA, Northern IrelandNX— all four of which areGBin ISO. - It identifies the country, not the team. Argentina and Argentina U23
both carry
AR. Useidto identify a team.
Empty string when we have no code. That is 1,856 clubs that have never appeared in a fixture carrying country data, plus a handful of historical national sides — USSR, Yugoslavia, East Germany — which have none to give.
Rolling form#
/api/v2/teams/{id}/form/ returns a team's last N finished matches already
averaged, instead of you fetching the fixtures and then a stats call per match.
| Param | Type | Description |
|---|---|---|
last |
int | Window size, 1–40. Default 10. |
league_id |
int | Restrict the window to one competition. Without it every competition counts, which is usually not what you want for league form. |
venue |
string | all (default), home or away — narrows the window itself. |
{"team_id": 8, "team_name": "West Ham United", "requested": 10, "matches": 10,
"from_date": "2026-08-16T15:00:00+00:00", "to_date": "2026-09-19T11:30:00+00:00",
"overall": {"matches": 10, "won": 5, "drawn": 3, "lost": 2,
"goals_for": 28, "goals_against": 15,
"points_per_match": 1.8, "form": "DLWWWWDWLD",
"stats": {"ball_possession": {"average": 54.2, "total": 542.0, "matches": 10},
"dribbles": {"average": 9.0, "total": 36.0, "matches": 4}}},
"home": {"...": "same shape"}, "away": {"...": "same shape"},
"vs_stronger": {"...": "same shape"}, "vs_weaker": {"...": "same shape"}}
Read the denominators. Every statistic carries its own matches count. In
the example above possession is averaged over ten matches and dribbles over
four, because only four reported dribbles — averaging them over ten would be
inventing six zeroes. overall.matches is the window; stats.<key>.matches is
how much of it that statistic covers.
form is most recent first.
vs_stronger and vs_weaker compare the two sides' published positions in the
table of the competition that match belonged to. When either position is
missing — a cup tie between two divisions, or a table we do not carry — the
match is left out of both strength splits rather than guessed at. It still
counts in overall, so the strength splits will not always add up to it.
Only finished matches count. A match in progress has half its statistics, and it would drag a form average down without saying so.
Squad availability#
Every player in the squad response carries their current availability:
| Field | Type | Description |
|---|---|---|
availability |
string | available · injured · doubtful · suspended |
injury_type |
string | Reason, e.g. Knee Injury. Empty when available or unpublished |
injury_expected_return |
date | Expected return, or null |
For an injured flag, test availability == "injured" — a boolean would fold
doubtful and suspended into "not injured".
The status is refreshed several times a day from the team news published for each
side's next fixture. available is the default value, so it means not currently
listed as missing rather than confirmed fit, and a club with no upcoming
fixture in a covered competition is not re-checked until it has one.
Players#
GET /api/v2/players/
| Param | Type | Description |
|---|---|---|
name |
string | Name search |
team_id |
int | Current club |
national_team_id |
int | Current national-team squad |
nationality_code |
string | ISO code, e.g. AR |
position |
string | G · D · M · F |
limit / offset |
int | Pagination |
GET /api/v2/players/{id}/ # bio, market value, skills
GET /api/v2/players/{id}/stats/ # per-match log — season_id, team_id, league_id, date_from/to
GET /api/v2/players/{id}/transfers/ # transfer history with fees
GET /api/v2/players/{id}/career/ # season-by-season summary
GET /api/v2/players/{id}/national-team/ # international record
GET /api/v2/players/{id}/social/ # tweets/videos
The full workflow with examples: Get a football player profile.
Transfers feed#
GET /api/v2/transfers/
| Param | Type | Description |
|---|---|---|
league_id |
int | Either club has played in this league |
player_id |
int | One player's transfer history |
team_id |
int | In or out of this club |
from_team_id / to_team_id |
int | Direction-specific |
date_from / date_to |
date | Transfer date window |
min_fee |
int | Minimum fee (EUR) |
has_fee |
bool | Only transfers with a disclosed fee |
ordering |
string | Sort field, prefix - for descending |
league_id takes the id from /api/v2/leagues/ and matches a transfer when
either side of the move is a club from that league — so an Argentine club
selling to Europe and buying from Europe both show up:
GET /api/v2/transfers/?league_id=85&limit=10
Unknown parameters are rejected. This endpoint answers
400if you pass a filter it does not recognise, listing what it accepts. A typo like?league=85fails loudly instead of quietly returning the unfiltered global feed.Note: the dataset includes forward-dated moves (announced loans/returns that haven't happened yet). For a "latest transfers" widget, always cap
date_toat today.
Transfer ledger#
The feed answers who moved. The ledger answers what it cost.
GET /api/v2/transfers/ledger/?team_id=…
GET /api/v2/transfers/ledger/?league_id=… # every club in the league, max 40
| Param | Type | Description |
|---|---|---|
team_id |
int | One club. Mutually exclusive with league_id |
league_id |
int | Every club in that league |
date_from |
date | Window start (ISO) |
date_to |
date | Window end. Never reaches past today whatever you pass |
Each club comes back with incoming and outgoing blocks — moves, how many
were permanent, loans, free and undisclosed, the fees that were published, and
the average age — plus gross_spend_eur, gross_income_eur, net_spend_eur,
loan_dependency_pct, squad_size, turnover_pct, age_in and age_out.
Three things decide these numbers, and you should know them before quoting one:
Money only counts where a fee was published. A move with no figure lands in
undisclosed, not as a zero. Summing the blanks as zeroes reports a tenth of
what the big leagues actually spend. free is the separate, known case of a
fee of nothing.
A loan return is not a signing. A player coming back from a loan is the same player returning to where he already belonged. Counting him as an arrival inflates the arrivals of every club that lends anyone out, and the raw feed gives you no warning that it happened.
turnover_pct is measured against today's squad. We do not keep historical
squad sizes, so a long window can exceed 100%. That is not a bug — it means the
club moved more players than it currently holds.
Transfer routes#
GET /api/v2/transfers/routes/?team_id=…&min_moves=3
| Param | Type | Description |
|---|---|---|
team_id / league_id |
int | Scope, same rules as the ledger |
date_from / date_to |
date | Window |
min_moves |
int | Minimum moves for a pair to count (default 2) |
limit |
int | Default 50, max 200 |
The club pairs that repeat, ranked: moves, loans, fees_eur and up to five
of the players who walked it.
Only moves with a real club at both ends are counted. "No team" is how the dataset spells free agency, not a destination, and it is excluded — without that it is the busiest route of almost every club on earth.
A club's own academy does count as a route, so the top line for a big club is often its own under-21s. That is the same question asked of the one feeder it controls.
World Cup squads#
GET /api/v2/worldcup/squads/ # team, group, status, has_player
GET /api/v2/worldcup/squads/{team_id}/
Tournament squad lists with per-player status — built for tournament hub pages.
Images#
Badges and photos come from ids, no extra calls:
/img/team/{id}/, /img/player/{id}/?sor=true&bg=transparent — see
Working with images.