---
id: example-php
title: Sport Line API — PHP Example
description: Example Sport Line API request for PHP 8.1 using the cURL extension.
version: v1
language: en
---

# Sport Line API — PHP Example

## Requirements

This example requires PHP 8.1 or later with the cURL extension installed.

Check the PHP version and extension:

```bash
php --version
php -m | grep -i curl
```

## Where to Make Requests

This example is intended for a backend application. Do not place the Sport Line API key
in HTML, JavaScript, or any other code sent to the user's browser.

If the user interface needs the data, it should call the client's backend, and the
backend should request Sport Line API.

## Environment Variables

The code uses two variables:

| Variable | Value |
|---|---|
| `SPORTAPI_BASE_URL` | Base URL received from the SportAPI manager |
| `SPORTAPI_PACKAGE_KEY` | Personal API key |

Do not write an active key directly into the source code or commit a secrets file to a
public repository.

## Minimal Request

```php
<?php

declare(strict_types=1);

$baseUrl = rtrim((string) getenv('SPORTAPI_BASE_URL'), '/');
$apiKey = (string) getenv('SPORTAPI_PACKAGE_KEY');

$curl = curl_init($baseUrl . '/v1/menu/live/en');

curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Package: ' . $apiKey,
        'Accept: application/json',
    ],
    CURLOPT_TIMEOUT => 15,
]);

$responseText = curl_exec($curl);

if ($responseText === false) {
    throw new RuntimeException(curl_error($curl));
}

$payload = json_decode($responseText, true, 512, JSON_THROW_ON_ERROR);

print_r($payload);
```

The API key is sent in the `Package` HTTP header, not in the URL.

## Shared Request Function

The following function:

- adds the `Package` header;
- limits connection and response wait times;
- parses JSON;
- checks the HTTP status;
- recognizes `error_code` and `error_message`;
- validates the standard `status`, `page`, and `body` envelope.

```php
<?php

declare(strict_types=1);

final class SportApiException extends RuntimeException
{
    public function __construct(
        string $message,
        public readonly int|string|null $apiCode = null,
        public readonly ?int $httpStatus = null,
        public readonly ?array $payload = null,
        ?Throwable $previous = null,
    ) {
        parent::__construct($message, 0, $previous);
    }
}

$rawBaseUrl = getenv('SPORTAPI_BASE_URL');
$apiKey = getenv('SPORTAPI_PACKAGE_KEY');

if ($rawBaseUrl === false || trim($rawBaseUrl) === '') {
    throw new RuntimeException('SPORTAPI_BASE_URL is not configured');
}

if ($apiKey === false || trim($apiKey) === '') {
    throw new RuntimeException('SPORTAPI_PACKAGE_KEY is not configured');
}

$baseUrl = rtrim($rawBaseUrl, '/');

function sportApiGet(string $path): array
{
    global $baseUrl, $apiKey;

    $curl = curl_init($baseUrl . $path);

    if ($curl === false) {
        throw new SportApiException('Could not initialize cURL');
    }

    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Package: ' . $apiKey,
            'Accept: application/json',
        ],
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 15,
    ]);

    $responseText = curl_exec($curl);

    if ($responseText === false) {
        $message = curl_error($curl);
        curl_close($curl);

        throw new SportApiException(
            'Could not connect to Sport Line API: ' . $message,
        );
    }

    $httpStatus = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);

    try {
        $payload = json_decode(
            $responseText,
            true,
            512,
            JSON_THROW_ON_ERROR,
        );
    } catch (JsonException $error) {
        throw new SportApiException(
            "Sport Line API returned invalid JSON. HTTP {$httpStatus}",
            httpStatus: $httpStatus,
            previous: $error,
        );
    }

    if (!is_array($payload)) {
        throw new SportApiException(
            'Unknown Sport Line API response format',
            httpStatus: $httpStatus,
        );
    }

    if (array_key_exists('error_code', $payload)) {
        $apiErrorMessage = $payload['error_message'] ?? 'Unknown error';

        throw new SportApiException(
            "Sport Line API error {$payload['error_code']}: {$apiErrorMessage}",
            apiCode: $payload['error_code'],
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    if ($httpStatus >= 400) {
        throw new SportApiException(
            "Sport Line API returned HTTP {$httpStatus}",
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    if (
        !array_key_exists('status', $payload)
        || !isset($payload['page'])
        || !is_string($payload['page'])
        || !array_key_exists('body', $payload)
    ) {
        throw new SportApiException(
            'Unknown Sport Line API response format',
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    return $payload;
}
```

Check for API errors in the JSON as well. Do not determine the request result using only
the HTTP status.

## Classifying `body`

In standard methods, `body` contains a data array or object. In `event`, it may also
contain a service message.

```php
function classifyBody(array $payload): array
{
    $body = $payload['body'];

    if (
        is_array($body)
        && isset($body['message'])
        && is_string($body['message'])
    ) {
        return [
            'type' => 'message',
            'message' => $body['message'],
        ];
    }

    if (is_array($body) && $body === []) {
        return [
            'type' => 'empty',
            'data' => [],
        ];
    }

    return [
        'type' => 'data',
        'data' => $body,
    ];
}
```

Possible results:

| `type` | Meaning |
|---|---|
| `data` | The API returned method data |
| `empty` | No data is currently available for this selection |
| `message` | `event` returned `Game not found` or `Game id finished` |

## Selecting a Current Branch from `menu`

The following function selects the first sport, country, and tournament present in the
current menu. In a real interface, the user selects the required branch.

```php
function findFirstTournament(array $menuBody): ?array
{
    foreach ($menuBody as $sport) {
        foreach ($sport['sub'] ?? [] as $country) {
            foreach ($country['sub'] ?? [] as $tournament) {
                return [
                    'sport_id' => $sport['id'],
                    'sport_name' => $sport['name'],
                    'country_id' => $country['id'],
                    'country_name' => $country['name'],
                    'tournament_id' => $tournament['id'],
                    'tournament_name' => $tournament['name'],
                ];
            }
        }
    }

    return null;
}
```

The IDs are not hard-coded. The function retrieves them from the current `menu`
response.

## Selecting a Match from an `events` Response

`events` groups matches by tournament. To retrieve the first available match, iterate
through `events_list`:

```php
function findFirstGame(array $eventsBody): ?array
{
    foreach ($eventsBody as $tournament) {
        $eventsList = $tournament['events_list'] ?? [];

        if ($eventsList !== []) {
            return $eventsList[0];
        }
    }

    return null;
}
```

## Complete `menu → events → event` Example

Save the following code as `sportapi_example.php`:

```php
<?php

declare(strict_types=1);

final class SportApiException extends RuntimeException
{
    public function __construct(
        string $message,
        public readonly int|string|null $apiCode = null,
        public readonly ?int $httpStatus = null,
        public readonly ?array $payload = null,
        ?Throwable $previous = null,
    ) {
        parent::__construct($message, 0, $previous);
    }
}

$rawBaseUrl = getenv('SPORTAPI_BASE_URL');
$apiKey = getenv('SPORTAPI_PACKAGE_KEY');

if ($rawBaseUrl === false || trim($rawBaseUrl) === '') {
    throw new RuntimeException('SPORTAPI_BASE_URL is not configured');
}

if ($apiKey === false || trim($apiKey) === '') {
    throw new RuntimeException('SPORTAPI_PACKAGE_KEY is not configured');
}

$baseUrl = rtrim($rawBaseUrl, '/');
$lineType = 'live';
$language = 'en';

function sportApiGet(string $path): array
{
    global $baseUrl, $apiKey;

    $curl = curl_init($baseUrl . $path);

    if ($curl === false) {
        throw new SportApiException('Could not initialize cURL');
    }

    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Package: ' . $apiKey,
            'Accept: application/json',
        ],
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 15,
    ]);

    $responseText = curl_exec($curl);

    if ($responseText === false) {
        $message = curl_error($curl);
        curl_close($curl);

        throw new SportApiException(
            'Could not connect to Sport Line API: ' . $message,
        );
    }

    $httpStatus = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);

    try {
        $payload = json_decode(
            $responseText,
            true,
            512,
            JSON_THROW_ON_ERROR,
        );
    } catch (JsonException $error) {
        throw new SportApiException(
            "Sport Line API returned invalid JSON. HTTP {$httpStatus}",
            httpStatus: $httpStatus,
            previous: $error,
        );
    }

    if (!is_array($payload)) {
        throw new SportApiException(
            'Unknown Sport Line API response format',
            httpStatus: $httpStatus,
        );
    }

    if (array_key_exists('error_code', $payload)) {
        $apiErrorMessage = $payload['error_message'] ?? 'Unknown error';

        throw new SportApiException(
            "Sport Line API error {$payload['error_code']}: {$apiErrorMessage}",
            apiCode: $payload['error_code'],
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    if ($httpStatus >= 400) {
        throw new SportApiException(
            "Sport Line API returned HTTP {$httpStatus}",
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    if (
        !array_key_exists('status', $payload)
        || !isset($payload['page'])
        || !is_string($payload['page'])
        || !array_key_exists('body', $payload)
    ) {
        throw new SportApiException(
            'Unknown Sport Line API response format',
            httpStatus: $httpStatus,
            payload: $payload,
        );
    }

    return $payload;
}

function classifyBody(array $payload): array
{
    $body = $payload['body'];

    if (
        is_array($body)
        && isset($body['message'])
        && is_string($body['message'])
    ) {
        return [
            'type' => 'message',
            'message' => $body['message'],
        ];
    }

    if (is_array($body) && $body === []) {
        return [
            'type' => 'empty',
            'data' => [],
        ];
    }

    return [
        'type' => 'data',
        'data' => $body,
    ];
}

function findFirstTournament(array $menuBody): ?array
{
    foreach ($menuBody as $sport) {
        foreach ($sport['sub'] ?? [] as $country) {
            foreach ($country['sub'] ?? [] as $tournament) {
                return [
                    'sport_id' => $sport['id'],
                    'sport_name' => $sport['name'],
                    'country_id' => $country['id'],
                    'country_name' => $country['name'],
                    'tournament_id' => $tournament['id'],
                    'tournament_name' => $tournament['name'],
                ];
            }
        }
    }

    return null;
}

function findFirstGame(array $eventsBody): ?array
{
    foreach ($eventsBody as $tournament) {
        $eventsList = $tournament['events_list'] ?? [];

        if ($eventsList !== []) {
            return $eventsList[0];
        }
    }

    return null;
}

function main(): void
{
    global $lineType, $language;

    $menuPayload = sportApiGet("/v1/menu/{$lineType}/{$language}");
    $menuResult = classifyBody($menuPayload);

    if ($menuResult['type'] === 'empty') {
        echo "No sections are currently available in the selected line.\n";
        return;
    }

    if (
        $menuResult['type'] !== 'data'
        || !is_array($menuResult['data'])
        || !array_is_list($menuResult['data'])
    ) {
        throw new SportApiException(
            'The menu method returned an unexpected body.',
        );
    }

    $selection = findFirstTournament($menuResult['data']);

    if ($selection === null) {
        echo "No tournament is currently available.\n";
        return;
    }

    echo "Selected current navigation branch:\n";
    echo json_encode(
        $selection,
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
    ) . "\n";

    $eventsPayload = sportApiGet(
        "/v1/events/{$selection['sport_id']}"
        . "/{$selection['tournament_id']}"
        . "/sub/50/{$lineType}/{$language}",
    );
    $eventsResult = classifyBody($eventsPayload);

    if ($eventsResult['type'] === 'empty') {
        echo "The selected tournament currently has no matches.\n";
        return;
    }

    if (
        $eventsResult['type'] !== 'data'
        || !is_array($eventsResult['data'])
        || !array_is_list($eventsResult['data'])
    ) {
        throw new SportApiException(
            'The events method returned an unexpected body.',
        );
    }

    $game = findFirstGame($eventsResult['data']);

    if ($game === null) {
        echo "No match is currently available.\n";
        return;
    }

    echo "Selected current match:\n";
    echo json_encode(
        [
            'game_id' => $game['game_id'],
            'first_opponent' => $game['opp_1_name'] ?? null,
            'second_opponent' => $game['opp_2_name'] ?? null,
        ],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
    ) . "\n";

    $eventPayload = sportApiGet(
        "/v1/event/{$game['game_id']}"
        . "/group/{$lineType}/{$language}",
    );
    $eventResult = classifyBody($eventPayload);

    if ($eventResult['type'] === 'message') {
        echo "The match is unavailable: {$eventResult['message']}\n";
        return;
    }

    if (
        $eventResult['type'] !== 'data'
        || !is_array($eventResult['data'])
        || array_is_list($eventResult['data'])
    ) {
        throw new SportApiException(
            'The event method returned an unexpected body.',
        );
    }

    $event = $eventResult['data'];

    echo "Detailed match:\n";
    echo json_encode(
        [
            'game_id' => $event['game_id'],
            'first_opponent' => $event['opp_1_name'] ?? null,
            'second_opponent' => $event['opp_2_name'] ?? null,
            'outcomes' => $event['game_oc_counter'] ?? null,
            'subgames' => count($event['sub_games'] ?? []),
        ],
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
    ) . "\n";
}

try {
    main();
} catch (SportApiException $error) {
    fwrite(STDERR, $error->getMessage() . "\n");

    if ($error->apiCode !== null) {
        fwrite(STDERR, "SportAPI error code: {$error->apiCode}\n");
    }

    if ($error->httpStatus !== null) {
        fwrite(STDERR, "HTTP status: {$error->httpStatus}\n");
    }

    exit(1);
}
```

Run it with:

```bash
SPORTAPI_BASE_URL='https://YOUR_API_DOMAIN' \
SPORTAPI_PACKAGE_KEY='YOUR_API_KEY' \
php sportapi_example.php
```

Do not use this form with an active key in shared shell history or during a screen
demonstration. In a production environment, store the key in the project's secrets
system.

## Retrieving All Tournaments for a Selected Sport

If you do not need to select a particular tournament, pass `tournamentId=0`:

```php
$eventsPayload = sportApiGet(
    "/v1/events/{$sportId}/0/sub/50/live/en",
);
```

You must still retrieve `$sportId` from the current Live menu.

## Requesting a Submatch

After retrieving the main match details, select an item from `sub_games`:

```php
$match = $eventResult['data'];
$subgames = $match['sub_games'] ?? [];

if ($subgames !== [] && isset($subgames[0]['game_id'])) {
    $subgamePayload = sportApiGet(
        "/v1/event/{$subgames[0]['game_id']}"
        . "/group/{$lineType}/{$language}",
    );
    $subgameResult = classifyBody($subgamePayload);

    if ($subgameResult['type'] === 'data') {
        echo 'Selected subgame: '
            . ($subgameResult['data']['game_dop_name'] ?? '')
            . "\n";
    }
}
```

The returned odds apply only to the selected submatch.

## Match Search

Always encode user-provided text with `rawurlencode()`:

```php
$searchText = rawurlencode('Manchester City');
$searchPayload = sportApiGet(
    "/v1/search/line/en/{$searchText}",
);
$searchResult = classifyBody($searchPayload);
```

Do not add an unprocessed user-provided string directly to the URL.

## Optional Additional Methods

The following methods are not required for the core integration. Use them only when the
interface or project logic needs the corresponding feature.

The recommended core flow remains:

```text
menu → events → event
```

Purpose of the additional requests:

- `sports`, `countries`, and `tournaments` provide a step-by-step alternative to
  `menu`;
- `topmatches` provides a ready-made top-match selection across all sports;
- `toplist` provides a Prematch selection for one chosen sport;
- `cybersport=true` provides a separate esports selection.

Do not make all these requests automatically simply because the methods exist.

```php
// Live sports
$sports = sportApiGet('/v1/sports/live/en');

// Countries for a current sport
$countries = sportApiGet(
    "/v1/countries/{$sportId}/live/en",
);

// Tournaments for a current sport and country
$tournaments = sportApiGet(
    "/v1/tournaments/{$sportId}/{$countryId}/live/en",
);

// Live top matches with extended summaries
$topmatches = sportApiGet('/v1/topmatches/live/en?full=true');

// Top matches for a selected sport, Prematch only
$toplist = sportApiGet(
    "/v1/toplist/{$prematchSportId}/en?full=true",
);

// Esports Live menu
$cybersportMenu = sportApiGet(
    '/v1/menu/live/en?cybersport=true',
);
```

Retrieve `$sportId`, `$countryId`, and `$prematchSportId` in these fragments from the
current responses for the corresponding sports line type.

## Handling `Game id finished`

```php
$payload = sportApiGet(
    "/v1/event/{$gameId}/group/live/en",
);
$result = classifyBody($payload);

if ($result['type'] === 'message') {
    if ($result['message'] === 'Game id finished') {
        echo "Stop updating this game_id and refresh the Live events list.\n";
    } else {
        echo "SportAPI event message: {$result['message']}\n";
    }
}
```

`Game id finished` does not indicate whether the match ended, was cancelled, or moved
from Prematch to Live. Stop requesting the old ID and refresh `events`.

## An Empty List Is Not an Error

```php
$payload = sportApiGet(
    '/v1/events/SPORT_ID_FROM_MENU/0/sub/50/live/en',
);
$result = classifyBody($payload);

if ($result['type'] === 'empty') {
    echo "There are currently no matches in this selection.\n";
}
```

Do not replace an empty current response with previously stored matches.

## What to Log When an Error Occurs

You may log:

- request time;
- method path without the key;
- HTTP status;
- `error_code` and `error_message`;
- `body.message`;
- IDs and sports line type used.

Do not log the value of the `Package` header.

## What the Example Does Not Provide

This code is not a ready-made SDK and does not implement:

- periodic updates;
- automatic retries;
- application caching;
- data models;
- a user interface;
- a bet placement and settlement system.

Recommended request frequencies are documented separately in
[Data Update Guidelines](../getting-started/update-guidelines.md).

## Related Documents

- [cURL Examples](curl.md)
- [JavaScript Example](javascript.md)
- [Python Example](python.md)
- [Quick Start](../getting-started/quick-start.md)
- [Response Format](../data-models/response-format.md)
- [Error Handling](../getting-started/error-handling.md)
- [Sport Line API Methods](../api-reference/README.md)
