Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FootballSoccerAPI.com - PHP SDK

The official PHP client for footballsoccerapi.com — 680,000 matches back to 2012 across 158 countries, with kickoff prices, team statistics, lineups and live scores.

No dependencies. Curl and json, both of which PHP already has.

There are two versions in here. Start with the simple one.


The simple one

One file, three functions, no classes. Copy fsapi-simple.php next to your script.

require 'fsapi-simple.php';

$matches = fsapi_get('/v1/matches', [
    'country' => 'England',
    'status'  => 'finished',
    'limit'   => 5,
]);

foreach ($matches as $m) {
    printf("%s %d-%d %s\n",
        $m['home_team_name'], $m['home_goals'], $m['away_goals'], $m['away_team_name']);
}

That is the whole thing. Three functions:

fsapi_get($path, $params) the data
fsapi_call($path, $params) the data and the meta
fsapi_walk($path, $params) every row, paging for you

Use this for a script, a cron job, or working out whether the data is what you need. Use the full client below if you are building something that has to keep working.


Your API key

The key is a string. getenv() wants the NAME of an environment variable, not the key itself.

This is the mistake everyone makes once, so it is worth spelling out.

// WRONG — getenv is asked for a variable called "fsa_live_...", which does not exist
$api = new Client(getenv('fsa_live_af1f2c1b6cc47a646e1d57b3524ea654'));

// Right — the key, directly
$api = new Client('fsa_live_af1f2c1b6cc47a646e1d57b3524ea654');

// Better — the key stays out of the file
$api = new Client(getenv('FSAPI_KEY'));

For the last one, set the variable when you run the script:

FSAPI_KEY=fsa_live_af1f2c1b6cc47a646e1d57b3524ea654 php your-script.php

Or for the whole session:

export FSAPI_KEY=fsa_live_af1f2c1b6cc47a646e1d57b3524ea654
php your-script.php

The environment version is worth the extra step: a key in a file ends up in a repository, a screenshot or a support ticket eventually.

For the simple client you can also set it at the top of fsapi-simple.php:

const FSAPI_KEY = 'fsa_live_...';

A free key takes a minute and needs no card: footballsoccerapi.com/free-key. It reaches eleven endpoints on a one to seven day delay — enough to build something real before deciding whether to pay.


The full client

Same data, more handled for you. Clone it and require the autoloader; that is the installation.

require 'src/autoload.php';

use FootballSoccerApi\Client;

$api = new Client(getenv('FSAPI_KEY'));

$res = $api->matches(['country' => 'England', 'status' => 'finished', 'limit' => 5]);

foreach ($res['data'] as $m) {
    printf("%s %d-%d %s\n",
        $m['home_team_name'], $m['home_goals'], $m['away_goals'], $m['away_team_name']);
}

Composer works if you use it, but nothing here needs it.

Errors that explain themselves

The API never sends a bare status code, and neither does this.

use FootballSoccerApi\{PlanRequiredException, RateLimitException};

try {
    $api->live();
} catch (PlanRequiredException $e) {
    printf("Needs %s, you have %s — %s\n", $e->needsPlan(), $e->yourPlan(), $e->upgradeUrl());
} catch (RateLimitException $e) {
    printf("Wait %d seconds\n", $e->retryAfter);
}

Rate limits are retried once by default using the API's own figure, because guessing a backoff when the response tells you the answer is worse for both sides. Pass retryOnRateLimit: false to handle it yourself.

A response that is not JSON throws a TransportException saying so, rather than a confusing parse error — that means something between you and the API answered, usually a proxy or a challenge page, and knowing which layer failed saves an hour.

Walking large sets

The cursor pages by sort position rather than offset, so the ten-thousandth page is as quick as the first and a match arriving mid-walk cannot shift the boundary and make you skip a row.

foreach ($api->walkMatches(['league_id' => 'lg_24T9Z0G', 'season' => 2024]) as $match) {
    // every match in the season, one at a time, memory flat
}

Batching

Fifty ids in one request instead of fifty requests. Longer lists are chunked rather than refused.

$matches = $api->matchesByIds(['mt_0CXSZRJ', 'mt_087EP2A', /* ... */]);

Each id costs one call against your rate limit, the same as fetching them separately. What batching saves is round trips, not quota — on a slow connection or from a serverless function that is still often the difference between a page that loads and one that times out.

The meta comes back with the data

$res = $api->matches(['season' => 2024]);

$res['data'];               // the matches
$res['meta']['total'];      // how many matched
$res['meta']['data_as_of']; // when the archive was last rebuilt

A client that stripped the meta to hand you a bare array would be throwing away the part that stops a figure being quoted without its base.


Examples

export FSAPI_KEY=your_key

php examples/simple.php            # the simple client, start to finish
php examples/quickstart.php        # recent results, with prices
php examples/walk-a-season.php     # walk a season with the cursor
php examples/home-advantage.php    # does home advantage differ by country?

The last one reproduces a study from the Lab. It is here so you can check the figure rather than take our word for it.


Ids

Ids carry their type — mt_ for a match, lg_ for a competition, tm_ for a club, vn_ for a ground. A club id passed where a competition belongs fails loudly instead of quietly returning the wrong thing, and raw integers are refused.

What is in the archive

680,916 matches from July 2012, across 928 competitions and 16,621 clubs. Kickoff prices on 53.7%, full-time results on 97.1%, team statistics on 132,441 matches, lineups and formations on 116,309.

Coverage varies by competition, and every competition and season publishes its own fill rate per field. That is the figure to build against rather than an archive-wide average — a field can be near complete in one competition and absent from another.

A field we do not hold comes back as null rather than vanishing from the response, so nothing on your side changes as coverage grows.

Documentation

Licence

MIT.