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

2 min read

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}/events for the timeline
  • /fixtures/{id}/lineups for the teamsheet
  • /fixtures/{id}/odds for 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:

  • lineupPlayer and lineupNumber for the shirt
  • lineupPosition as an ordinal, where 1 is the goalkeeper
  • playerId and playerImage when we recognise the player
  • playerPosition, 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.
  • lineupPosition is 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}/commentary for minute by minute text
  • /fixtures/{id}/live-odds for in play prices
  • /h2h/{team1}/{team2} for previous meetings and recent form
  • /standings/{leagueId} plus the /home, /away and /form variants

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.

Keep reading

Build a match page: lineups, formations and odds | GOAL API