SportAPI Документация
RU
S Документация продуктаSport Line API
v1
Услуга и цены ↗ Получить доступ ↗

Sport Line API — пример на PHP

Требования

Пример рассчитан на PHP 8.1 или новее с установленным расширением cURL.

Проверить версию PHP и наличие расширения:

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

Где выполнять запросы

Пример предназначен для backend-приложения. Не помещайте ключ Sport Line API в HTML, JavaScript или другой код, который отправляется в браузер пользователя.

Если данные нужны пользовательскому интерфейсу, интерфейс обращается к backend клиента, а backend выполняет запрос к Sport Line API.

Переменные окружения

Код использует две переменные:

ПеременнаяЗначение
SPORTAPI_BASE_URLБазовый URL, полученный от менеджера SportAPI
SPORTAPI_PACKAGE_KEYПерсональный API-ключ

Не записывайте рабочий ключ непосредственно в исходный код и не добавляйте файл с секретами в публичный репозиторий.

Минимальный запрос

<?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/ru');

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);

API-ключ передаётся в HTTP-заголовке Package, а не в URL.

Общая функция запроса

Следующая функция:

  • добавляет заголовок Package;
  • ограничивает время соединения и ожидания ответа;
  • разбирает JSON;
  • проверяет HTTP-статус;
  • распознаёт error_code и error_message;
  • проверяет общую оболочку status, page, body.
<?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;
}

Ошибку API нужно проверять и в JSON. Не следует определять результат запроса только по HTTP-статусу.

Классификация body

В обычных методах body содержит массив или объект данных. В методе event там также может находиться служебное сообщение.

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,
    ];
}

Возможные результаты:

typeЗначение
dataAPI вернул данные метода
emptyВ текущей выборке нет данных
messageМетод event вернул Game not found или Game id finished

Выбор актуальной ветки из menu

Следующая функция выбирает первый вид спорта, страну и турнир, присутствующие в текущем меню. В реальном интерфейсе соответствующую ветку выбирает пользователь.

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;
}

ID не записываются статически: функция получает их из актуального ответа menu.

Выбор матча из ответа events

Метод events группирует матчи по турнирам. Чтобы получить первый доступный матч, нужно пройти через events_list:

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

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

    return null;
}

Полный пример menu → events → event

Сохраните следующий код в файле sportapi_example.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 = 'ru';

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);
}

Запуск:

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

Не используйте эту форму с рабочим ключом в общей истории команд или в демонстрации экрана. В рабочем окружении храните ключ в системе секретов проекта.

Получение всех турниров выбранного спорта

Если конкретный турнир выбирать не нужно, передайте tournamentId=0:

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

Значение $sportId всё равно должно быть получено из актуального Live-меню.

Запрос субматча

После получения подробного основного матча выберите элемент из sub_games:

$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";
    }
}

Коэффициенты ответа относятся только к выбранному субматчу.

Поиск матчей

Пользовательский текст обязательно кодируется через rawurlencode():

$searchText = rawurlencode('Манчестер Сити');
$searchPayload = sportApiGet(
    "/v1/search/line/ru/{$searchText}",
);
$searchResult = classifyBody($searchPayload);

Не добавляйте необработанную пользовательскую строку непосредственно в URL.

Необязательные дополнительные методы

Методы ниже не являются обязательной частью основной интеграции. Используйте их только по необходимости, если соответствующая возможность нужна интерфейсу или логике проекта.

Основная рекомендуемая цепочка остаётся прежней:

menu → events → event

Назначение дополнительных запросов:

  • sports, countries и tournaments — пошаговая альтернатива menu;
  • topmatches — готовая подборка топ-матчей по всем видам спорта;
  • toplist — Prematch-подборка одного выбранного вида спорта;
  • cybersport=true — отдельная выборка киберспортивных данных.

Не выполняйте все эти запросы автоматически только потому, что они существуют в API.

// Виды спорта Live
$sports = sportApiGet('/v1/sports/live/ru');

// Страны актуального вида спорта
$countries = sportApiGet(
    "/v1/countries/{$sportId}/live/ru",
);

// Турниры актуальных спорта и страны
$tournaments = sportApiGet(
    "/v1/tournaments/{$sportId}/{$countryId}/live/ru",
);

// Топ-матчи Live с расширенными карточками
$topmatches = sportApiGet('/v1/topmatches/live/ru?full=true');

// Топ-матчи выбранного спорта, только Prematch
$toplist = sportApiGet(
    "/v1/toplist/{$prematchSportId}/ru?full=true",
);

// Киберспортивное Live-меню
$cybersportMenu = sportApiGet(
    '/v1/menu/live/ru?cybersport=true',
);

Переменные $sportId, $countryId и $prematchSportId в этих фрагментах должны быть получены из актуальных ответов соответствующего типа линии.

Обработка Game id finished

$payload = sportApiGet(
    "/v1/event/{$gameId}/group/live/ru",
);
$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 не позволяет определить, завершился матч, был отменён или перешёл из Prematch в Live. После сообщения прекратите запросы по старому ID и обновите events.

Пустой список — не ошибка

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

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

Не заменяйте пустой актуальный ответ ранее сохранёнными матчами.

Что логировать при ошибке

Можно записывать:

  • время запроса;
  • путь метода без ключа;
  • HTTP-статус;
  • error_code и error_message;
  • body.message;
  • используемые ID и тип линии.

Не записывайте значение заголовка Package в логи.

Что не делает пример

Код не является готовым SDK и не реализует:

  • периодическое обновление;
  • автоматические повторные запросы;
  • кэш приложения;
  • модели данных;
  • пользовательский интерфейс;
  • систему приёма и расчёта ставок.

Рекомендуемая частота запросов описана отдельно: «Рекомендации по обновлению данных».

Связанные документы