Build a match page: lineups, formations and odds
Four endpoints cover a full match page. Draw a 4-2-3-1 from the lineup data, read odds keyed by line, and know which fields go missing.
GOAL API· Engineering

What one fixture gives you
A match page needs more than two names and a score. Four endpoints cover the whole screen.
/fixtures/{id}for the scoreboard/fixtures/{id}/eventsfor the timeline/fixtures/{id}/lineupsfor the teamsheet/fixtures/{id}/oddsfor prices
Every one accepts our fixture id or the provider match id, so an id from a head to head list resolves without a lookup.
Lineups arrive with players attached
curl https://api.goal-api.com/v1/fixtures/FIXTURE_ID/lineups \
-H "Authorization: Bearer $GOAL_API_KEY"
Each row in startingLineups carries:
lineupPlayerandlineupNumberfor the shirtlineupPositionas an ordinal, where 1 is the goalkeeperplayerIdandplayerImagewhen we recognise the playerplayerPosition, one of Goalkeepers, Defenders, Midfielders, Forwards
Rows sort by position ascending. Use playerId to link through to a full profile.
Draw the formation
homeFormation and awayFormation hold strings like 4-2-3-1.
Position 1 is the keeper. The formation digits consume the remaining ten in order: defence, midfield bands, then attack.
function bands(formation, starters) {
const gk = starters[0];
const outfield = starters.slice(1);
const digits = formation.split("-").map(Number);
// Trust the formation only when the digits match the players you hold.
const total = digits.reduce((a, b) => a + b, 0);
if (total !== outfield.length) return null;
const rows = [[gk]];
let i = 0;
for (const n of digits) {
rows.push(outfield.slice(i, i + n));
i += n;
}
return rows;
}
Two details save you from a broken pitch graphic:
- Formations exist on roughly 16 percent of fixtures. Build a fallback before you ship.
lineupPositionis stored as text, so a default sort puts 10 before 2. Sort numerically.
When no formation arrives, group players by playerPosition instead. Defenders, then midfielders, then forwards gives you a usable shape for most lineups.
Add prices
curl https://api.goal-api.com/v1/fixtures/FIXTURE_ID/odds \
-H "Authorization: Bearer $GOAL_API_KEY"
Each row is one bookmaker with odd1, oddX, odd2, double chance, and both teams to score.
Asian handicap and over/under come back as objects keyed by line:
{
"bookmaker": "10Bet",
"odd1": "2.1",
"oddX": "3.6",
"odd2": "2.88",
"asianHandicap": { "ah0_1": "1.75", "ah0_2": "1.93" },
"overUnder": { "o+2.5": "1.57", "u+2.5": "2.34" }
}
Bookmakers publish different lines for different matches, so read the keys you need and skip the rest. Fixed columns break the first time a bookmaker offers a line you never planned for.
Finish the page
/fixtures/{id}/commentaryfor minute by minute text/fixtures/{id}/live-oddsfor in play prices/h2h/{team1}/{team2}for previous meetings and recent form/standings/{leagueId}plus the/home,/awayand/formvariants
Neutral venue tournaments return no home or away split. A World Cup group has no home record, so handle the empty response as "not published" rather than an error.
Keep the page live
Open a WebSocket for the score. Refresh events and statistics every 30 seconds while the match runs.
Static data stays static. Fetch lineups once. Refetching a teamsheet every 30 seconds spends quota to receive the same eleven names.


