How to Get Football Data in PHP: Live Scores, Standings & Odds

Fetch live football scores, standings, player stats, and odds in PHP using GOAL API. Copy-paste cURL and Guzzle code, real endpoints, Laravel-ready examples.

GOAL API

4 min read
Football data in PHP with GOAL API: PHP code calling the API beside a live Arsenal 1-0 Chelsea score card — cURL, Guzzle and Laravel

Search "football API Python" and you'll drown in tutorials. Search "football API PHP" and you'll find almost nothing — despite PHP powering a huge share of the web, from WordPress fan sites to Laravel betting dashboards. This is the tutorial that should exist.

By the end, you'll have working PHP code for live scores, standings, match events, odds, and player stats using GOAL API — with both a zero-dependency cURL version and a clean Guzzle version, plus a Laravel service class. If you've already read the Python tutorial or the JavaScript tutorial, these are the same endpoints in PHP.

Get your free API key (no credit card): goal-api.com/signup

Setup: cURL or Guzzle

Store your key in an environment variable — never hardcode it. Every GOAL API response follows the same envelope: { "success": true, "data": {...} }, so $json["data"] is always your payload.

Option A — plain cURL (zero dependencies)

PHP
<?php
$BASE = 'https://api.goal-api.com/v1';
$KEY  = getenv('GOAL_API_KEY');

function goal_get(string $path, array $query = []): array {
    global $BASE, $KEY;
    $url = $BASE . $path . ($query ? '?' . http_build_query($query) : '');
    $ch  = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $KEY],
    ]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($code >= 400) throw new RuntimeException("GOAL API error: $code");
    return json_decode($body, true)['data'];
}
PHP
<?php
// composer require guzzlehttp/guzzle
use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://api.goal-api.com/v1/',
    'headers'  => ['Authorization' => 'Bearer ' . getenv('GOAL_API_KEY')],
    'timeout'  => 10,
]);

function goal_get(Client $client, string $path, array $query = []): array {
    $res  = $client->get(ltrim($path, '/'), ['query' => $query]);
    $json = json_decode((string) $res->getBody(), true);
    return $json['data'];
}

The examples below use the goal_get() helper. Both versions return the same array, so the rest of the tutorial works with either.

1. Fetch Live Scores

The endpoint GET /fixtures/live returns every match currently in play.

PHP
$matches = goal_get('/fixtures/live');

foreach ($matches as $m) {
    printf("[%s] %s %d-%d %s (%s')\n",
        $m['league']['name'],
        $m['homeTeam']['name'], $m['homeTeamScore'],
        $m['awayTeamScore'], $m['awayTeam']['name'],
        $m['matchMinute'] ?? $m['matchStatus']
    );
}

Sample output:

Text
[Premier League] Arsenal 1-0 Chelsea (67')
[La Liga] Real Madrid 2-1 Barcelona (81')

Endpoint: GET /fixtures/live

Each match carries an id — the fixture ID you'll use for events, lineups, and odds.

2. Fixtures by Date

PHP
$fixtures = goal_get('/fixtures/date/2026-09-20');  // YYYY-MM-DD

foreach ($fixtures as $m) {
    // Always build on kickoffUtc — it's ISO-8601 in UTC.
    $kickoff = new DateTime($m['kickoffUtc']);
    $kickoff->setTimezone(new DateTimeZone('Europe/London'));
    printf("%s | [%s] %s vs %s | %s\n",
        $kickoff->format('H:i T'),
        $m['league']['name'],
        $m['homeTeam']['name'], $m['awayTeam']['name'],
        $m['matchStatus']
    );
}

Endpoint: GET /fixtures/date/:date

Build on kickoffUtc — it's a proper ISO-8601 UTC timestamp. Convert to the user's timezone with DateTimeZone. matchDate and matchTime exist for compatibility but carry no offset.

3. League Standings

PHP
// Find a league ID first (England's Premier League is the first match)
$leagues = goal_get('/leagues', ['search' => 'premier league']);
$pl = array_values(array_filter($leagues, fn($l) =>
    $l['countryName'] === 'England'))[0];

// Then pull the table
$table = goal_get("/standings/{$pl['id']}");

printf("%-4s %-26s %-4s %-4s %-4s\n", 'Pos', 'Team', 'P', 'GD', 'Pts');
foreach ($table as $row) {
    $gd = $row['overallLeagueGF'] - $row['overallLeagueGA'];
    printf("%-4d %-26s %-4d %-4d %-4d\n",
        $row['overallLeaguePosition'], $row['team']['name'],
        $row['overallLeaguePlayed'], $gd, $row['overallLeaguePTS']
    );
}

Endpoints: GET /leagues?search= | GET /standings/:leagueId

League IDs and what data each competition holds are on the coverage page. Split views are available too: /standings/:leagueId/home, /away, /form, and /zones.

4. Match Events, Goals & Cards

PHP
// Goals, cards and substitutions are three endpoints; merge them into one timeline.
$timeline = [];
foreach (goal_get("/fixtures/$fixtureId/events") as $g) {
    $timeline[] = [$g['timeNum'], "\u{26BD}", ($g['homeScorer'] ?: $g['awayScorer']) . " ({$g['score']})"];
}
foreach (goal_get("/fixtures/$fixtureId/cards") as $c) {
    $icon = $c['card'] === 'red card' ? "\u{1F7E5}" : "\u{1F7E8}";
    $timeline[] = [$c['timeNum'], $icon, $c['homeFault'] ?: $c['awayFault']];
}
foreach (goal_get("/fixtures/$fixtureId/substitutions") as $s) {
    [$off, $on] = array_map('trim', explode('|', $s['substitution']));
    $timeline[] = [$s['timeNum'], "\u{1F504}", "$on on for $off ({$s['team']})"];
}

usort($timeline, fn($a, $b) => $a[0] <=> $b[0]);
foreach ($timeline as [$minute, $icon, $text]) {
    printf("%s %d' %s\n", $icon, $minute, $text);
}

Endpoints: GET /fixtures/:id/events (goals) | GET /fixtures/:id/cards | GET /fixtures/:id/substitutions

5. Pre-Match & Live Odds

This is where PHP betting and preview products live. Pre-match odds cover 1X2, both-teams-to-score, and over/under lines. Odds are part of the paid plans; on a free key these two endpoints return 403.

PHP
$odds = goal_get("/fixtures/$fixtureId/odds");

foreach ($odds as $book) {
    printf("[%s] Home %s | Draw %s | Away %s | BTTS %s | Over 2.5 %s\n",
        $book['bookmaker'], $book['odd1'], $book['oddX'], $book['odd2'],
        $book['btsYes'], $book['overUnder']['o+2.5'] ?? '-');
}

// In-play odds that update during the match:
$live = goal_get("/fixtures/$fixtureId/live-odds");

Endpoints: GET /fixtures/:id/odds | GET /fixtures/:id/live-odds

The in-play odds endpoint updates during the match — useful for live dashboards. Which leagues are priced is listed on the coverage page.

6. Player Statistics

PHP
// Search, then pull stats
$results = goal_get('/players/search', ['q' => 'Erling Haaland']);
$id      = $results[0]['id'];
$stats   = goal_get("/players/$id/statistics")['performance'];

printf("Goals: %d | Assists: %d | Apps: %d\n",
    $stats['goals'], $stats['assists'], $stats['matchPlayed']);

// League top scorers:
$topScorers = goal_get("/leagues/{$pl['id']}/top-scorers");
usort($topScorers, fn($a, $b) => (int) $a['playerPlace'] <=> (int) $b['playerPlace']);
foreach (array_slice($topScorers, 0, 5) as $p) {
    printf("%s. %s (%s) %s\n", $p['playerPlace'], $p['playerName'], $p['teamName'], $p['goals']);
}

Endpoints: GET /players/search?q= | GET /players/:id/statistics | GET /leagues/:id/top-scorers

7. Handling Rate Limits

Every response carries rate-limit headers. With Guzzle you can read them off the response object:

PHP
$res = $client->get('fixtures/live');
$remaining = $res->getHeaderLine('X-RateLimit-Remaining');
$limit     = $res->getHeaderLine('X-RateLimit-Limit');
echo "Quota: $remaining / $limit\n";

// On a 429, check the error code in the body:
// QUOTA_EXCEEDED     -> daily allowance gone, wait for reset
// BURST_LIMIT_EXCEEDED -> too many req/sec, back off ~1s

The rate limits documentation explains the two 429 types. Read X-RateLimit-Remaining and back off before you hit zero.

Using It in Laravel

Most PHP football projects are Laravel apps. Wrap the API in a service class so your controllers stay clean and you have one place to change if you ever swap providers.

A service class + facade

PHP
<?php
// app/Services/FootballService.php
namespace App\Services;

use Illuminate\Support\Facades\Http;

class FootballService
{
    private string $base = 'https://api.goal-api.com/v1';

    private function get(string $path, array $query = []): array
    {
        return Http::withToken(config('services.goalapi.key'))
            ->baseUrl($this->base)
            ->get($path, $query)
            ->throw()
            ->json('data');
    }

    public function liveScores(): array
    {
        return $this->get('/fixtures/live');
    }

    public function standings(string $leagueId): array
    {
        return $this->get("/standings/$leagueId");
    }
}

Add the key to config/services.php and your .env, then inject FootballService into any controller. Laravel's built-in Http client means you don't even need Guzzle directly — it's already there.

What to Build Next

Live scoreboard — poll /fixtures/live on a cron or queue job, or move to the WebSocket connection WebSocket connection for instant updates. The polling vs WebSocket vs webhooks post shows the request math.

Goal notification bot — register a webhook endpoint so goal.scored events POST to a Laravel route. No polling loop to maintain.

Match preview page — combine /h2h/:t1/:t2/stats, /fixtures/:id/lineups, and /fixtures/:id/odds.

Endpoint Quick Reference

Laravel architecture: a MatchController calls a FootballService, which calls GOAL API endpoints such as GET /fixtures/live and GET /standings/:id — controllers stay clean and swapping providers means editing one file

Endpoint Method Returns
/fixtures/live GET All live matches
/fixtures/date/:date GET Fixtures on a date
/fixtures/:id/events GET Goals
/fixtures/:id/cards GET Yellow and red cards
/fixtures/:id/substitutions GET Substitutions
/fixtures/:id/odds GET Pre-match odds
/fixtures/:id/live-odds GET In-play odds
/fixtures/:id/lineups GET Lineups & formations
/standings/:leagueId GET League table
/leagues?search= GET Find a league + its ID
/players/search?q= GET Search players
/players/:id/statistics GET Player season stats
/leagues/:id/top-scorers GET League top scorers
/h2h/:team1Id/:team2Id GET Head-to-head

Full reference at goal-api.com/documentation.

Start building free → goal-api.com/signup | No credit card | Every endpoint except odds and predictions

Keep reading

How to Get Football Data in PHP: Live Scores, Standings & Odds | GOAL API