Sport Line API — пример на JavaScript
Требования
Пример рассчитан на Node.js 18 или новее, где функция fetch() доступна без установки
дополнительной библиотеки.
Проверить версию:
node --version
Где выполнять запросы
Пример предназначен для backend-приложения на Node.js. Не помещайте ключ Sport Line API в JavaScript, который выполняется в браузере: пользователь сайта сможет увидеть его в исходном коде и сетевых запросах.
Если данные нужны браузерному интерфейсу, браузер обращается к backend клиента, а backend выполняет запрос к Sport Line API.
Переменные окружения
Код использует две переменные:
| Переменная | Значение |
|---|---|
SPORTAPI_BASE_URL | Базовый URL, полученный от менеджера SportAPI |
SPORTAPI_PACKAGE_KEY | Персональный API-ключ |
Не записывайте рабочий ключ непосредственно в исходный код и не добавляйте файл с секретами в публичный репозиторий.
Минимальный запрос
const baseUrl = process.env.SPORTAPI_BASE_URL;
const apiKey = process.env.SPORTAPI_PACKAGE_KEY;
const response = await fetch(`${baseUrl}/v1/menu/live/ru`, {
headers: {
Package: apiKey,
Accept: 'application/json'
}
});
const payload = await response.json();
console.log(payload);
API-ключ передаётся в HTTP-заголовке Package, а не в URL.
Общая функция запроса
Следующая функция:
- добавляет заголовок
Package; - проверяет HTTP-статус;
- разбирает JSON;
- распознаёт
error_codeиerror_message; - проверяет общую оболочку
status,page,body.
const rawBaseUrl = process.env.SPORTAPI_BASE_URL;
const apiKey = process.env.SPORTAPI_PACKAGE_KEY;
if (!rawBaseUrl) {
throw new Error('SPORTAPI_BASE_URL is not configured');
}
if (!apiKey) {
throw new Error('SPORTAPI_PACKAGE_KEY is not configured');
}
const baseUrl = rawBaseUrl.replace(/\/$/, '');
async function sportApiGet(path) {
const response = await fetch(`${baseUrl}${path}`, {
method: 'GET',
headers: {
Package: apiKey,
Accept: 'application/json'
}
});
const responseText = await response.text();
let payload;
try {
payload = JSON.parse(responseText);
} catch {
throw new Error(
`Sport Line API returned invalid JSON. HTTP ${response.status}`
);
}
if (
payload &&
typeof payload === 'object' &&
!Array.isArray(payload) &&
payload.error_code !== undefined
) {
const error = new Error(
`Sport Line API error ${payload.error_code}: ${payload.error_message}`
);
error.code = payload.error_code;
error.payload = payload;
throw error;
}
if (!response.ok) {
const error = new Error(`Sport Line API returned HTTP ${response.status}`);
error.httpStatus = response.status;
error.payload = payload;
throw error;
}
if (
!payload ||
typeof payload !== 'object' ||
Array.isArray(payload) ||
payload.status === undefined ||
typeof payload.page !== 'string' ||
!('body' in payload)
) {
const error = new Error('Unknown Sport Line API response format');
error.payload = payload;
throw error;
}
return payload;
}
fetch() сам по себе не выбрасывает исключение при HTTP-ответах 4xx и 5xx, поэтому
response.ok проверяется отдельно. При этом ошибку API нужно искать и в JSON, а не
определять только по HTTP-статусу.
Классификация body
В обычных методах body содержит массив или объект данных. В методе event там также
может находиться служебное сообщение.
function classifyBody(payload) {
const { body } = payload;
if (
body &&
typeof body === 'object' &&
!Array.isArray(body) &&
typeof body.message === 'string'
) {
return {
type: 'message',
message: body.message
};
}
if (Array.isArray(body) && body.length === 0) {
return {
type: 'empty',
data: []
};
}
return {
type: 'data',
data: body
};
}
Возможные результаты:
type | Значение |
|---|---|
data | API вернул данные метода |
empty | В текущей выборке нет данных |
message | Метод event вернул Game not found или Game id finished |
Выбор актуальной ветки из menu
Следующая функция выбирает первый вид спорта, страну и турнир, присутствующие в текущем меню. В реальном интерфейсе соответствующую ветку выбирает пользователь.
function findFirstTournament(menuBody) {
for (const sport of menuBody) {
for (const country of sport.sub ?? []) {
for (const tournament of country.sub ?? []) {
return {
sportId: sport.id,
sportName: sport.name,
countryId: country.id,
countryName: country.name,
tournamentId: tournament.id,
tournamentName: tournament.name
};
}
}
}
return null;
}
ID не записываются статически: функция получает их из актуального ответа menu.
Выбор матча из ответа events
Метод events группирует матчи по турнирам. Чтобы получить первый доступный матч,
нужно пройти через events_list:
function findFirstGame(eventsBody) {
for (const tournament of eventsBody) {
const game = tournament.events_list?.[0];
if (game) {
return game;
}
}
return null;
}
Полный пример menu → events → event
Сохраните следующий код в файле sportapi-example.mjs:
const rawBaseUrl = process.env.SPORTAPI_BASE_URL;
const apiKey = process.env.SPORTAPI_PACKAGE_KEY;
if (!rawBaseUrl) {
throw new Error('SPORTAPI_BASE_URL is not configured');
}
if (!apiKey) {
throw new Error('SPORTAPI_PACKAGE_KEY is not configured');
}
const baseUrl = rawBaseUrl.replace(/\/$/, '');
const lineType = 'live';
const language = 'ru';
async function sportApiGet(path) {
const response = await fetch(`${baseUrl}${path}`, {
method: 'GET',
headers: {
Package: apiKey,
Accept: 'application/json'
}
});
const responseText = await response.text();
let payload;
try {
payload = JSON.parse(responseText);
} catch {
throw new Error(
`Sport Line API returned invalid JSON. HTTP ${response.status}`
);
}
if (
payload &&
typeof payload === 'object' &&
!Array.isArray(payload) &&
payload.error_code !== undefined
) {
const error = new Error(
`Sport Line API error ${payload.error_code}: ${payload.error_message}`
);
error.code = payload.error_code;
error.payload = payload;
throw error;
}
if (!response.ok) {
const error = new Error(`Sport Line API returned HTTP ${response.status}`);
error.httpStatus = response.status;
error.payload = payload;
throw error;
}
if (
!payload ||
typeof payload !== 'object' ||
Array.isArray(payload) ||
payload.status === undefined ||
typeof payload.page !== 'string' ||
!('body' in payload)
) {
const error = new Error('Unknown Sport Line API response format');
error.payload = payload;
throw error;
}
return payload;
}
function classifyBody(payload) {
const { body } = payload;
if (
body &&
typeof body === 'object' &&
!Array.isArray(body) &&
typeof body.message === 'string'
) {
return { type: 'message', message: body.message };
}
if (Array.isArray(body) && body.length === 0) {
return { type: 'empty', data: [] };
}
return { type: 'data', data: body };
}
function findFirstTournament(menuBody) {
for (const sport of menuBody) {
for (const country of sport.sub ?? []) {
for (const tournament of country.sub ?? []) {
return {
sportId: sport.id,
sportName: sport.name,
countryId: country.id,
countryName: country.name,
tournamentId: tournament.id,
tournamentName: tournament.name
};
}
}
}
return null;
}
function findFirstGame(eventsBody) {
for (const tournament of eventsBody) {
const game = tournament.events_list?.[0];
if (game) {
return game;
}
}
return null;
}
async function main() {
const menuPayload = await sportApiGet(
`/v1/menu/${lineType}/${language}`
);
const menuResult = classifyBody(menuPayload);
if (menuResult.type === 'empty') {
console.log('No sections are currently available in the selected line.');
return;
}
if (menuResult.type !== 'data' || !Array.isArray(menuResult.data)) {
throw new Error('The menu method returned an unexpected body.');
}
const selection = findFirstTournament(menuResult.data);
if (!selection) {
console.log('No tournament is currently available.');
return;
}
console.log('Selected current navigation branch:', selection);
const eventsPayload = await sportApiGet(
`/v1/events/${selection.sportId}/${selection.tournamentId}` +
`/sub/50/${lineType}/${language}`
);
const eventsResult = classifyBody(eventsPayload);
if (eventsResult.type === 'empty') {
console.log('The selected tournament currently has no matches.');
return;
}
if (eventsResult.type !== 'data' || !Array.isArray(eventsResult.data)) {
throw new Error('The events method returned an unexpected body.');
}
const game = findFirstGame(eventsResult.data);
if (!game) {
console.log('No match is currently available.');
return;
}
console.log('Selected current match:', {
gameId: game.game_id,
firstOpponent: game.opp_1_name,
secondOpponent: game.opp_2_name
});
const eventPayload = await sportApiGet(
`/v1/event/${game.game_id}/group/${lineType}/${language}`
);
const eventResult = classifyBody(eventPayload);
if (eventResult.type === 'message') {
console.log(`The match is unavailable: ${eventResult.message}`);
return;
}
if (eventResult.type !== 'data' || Array.isArray(eventResult.data)) {
throw new Error('The event method returned an unexpected body.');
}
console.log('Detailed match:', {
gameId: eventResult.data.game_id,
firstOpponent: eventResult.data.opp_1_name,
secondOpponent: eventResult.data.opp_2_name,
outcomes: eventResult.data.game_oc_counter,
subgames: eventResult.data.sub_games?.length ?? 0
});
}
main().catch((error) => {
console.error(error.message);
if (error.code !== undefined) {
console.error('SportAPI error code:', error.code);
}
if (error.httpStatus !== undefined) {
console.error('HTTP status:', error.httpStatus);
}
process.exitCode = 1;
});
Запуск:
SPORTAPI_BASE_URL='https://YOUR_API_DOMAIN' \
SPORTAPI_PACKAGE_KEY='YOUR_API_KEY' \
node sportapi-example.mjs
Не используйте эту форму с рабочим ключом в общей истории команд или в демонстрации экрана. В рабочем окружении храните ключ в системе секретов проекта.
Получение всех турниров выбранного спорта
Если конкретный турнир выбирать не нужно, передайте tournamentId=0:
const eventsPayload = await sportApiGet(
`/v1/events/${sportId}/0/sub/50/live/ru`
);
Значение sportId всё равно должно быть получено из актуального Live-меню.
Запрос субматча
После получения подробного основного матча выберите элемент из sub_games:
const match = eventResult.data;
const subgame = match.sub_games?.[0];
if (subgame?.game_id) {
const subgamePayload = await sportApiGet(
`/v1/event/${subgame.game_id}/group/${lineType}/${language}`
);
const subgameResult = classifyBody(subgamePayload);
if (subgameResult.type === 'data') {
console.log('Selected subgame:', subgameResult.data.game_dop_name);
}
}
Коэффициенты ответа относятся только к выбранному субматчу.
Поиск матчей
Пользовательский текст обязательно кодируется через encodeURIComponent():
const searchText = encodeURIComponent('Манчестер Сити');
const searchPayload = await sportApiGet(
`/v1/search/line/ru/${searchText}`
);
const searchResult = classifyBody(searchPayload);
Не добавляйте необработанную пользовательскую строку непосредственно в URL.
Необязательные дополнительные методы
Методы ниже не являются обязательной частью основной интеграции. Используйте их только по необходимости, если соответствующая возможность нужна интерфейсу или логике проекта.
Основная рекомендуемая цепочка остаётся прежней:
menu → events → event
Назначение дополнительных запросов:
sports,countriesиtournaments— пошаговая альтернативаmenu;topmatches— готовая подборка топ-матчей по всем видам спорта;toplist— Prematch-подборка одного выбранного вида спорта;cybersport=true— отдельная выборка киберспортивных данных.
Не выполняйте все эти запросы автоматически только потому, что они существуют в API.
// Виды спорта Live
const sports = await sportApiGet('/v1/sports/live/ru');
// Страны актуального вида спорта
const countries = await sportApiGet(
`/v1/countries/${sportId}/live/ru`
);
// Турниры актуальных спорта и страны
const tournaments = await sportApiGet(
`/v1/tournaments/${sportId}/${countryId}/live/ru`
);
// Топ-матчи Live с расширенными карточками
const topmatches = await sportApiGet(
'/v1/topmatches/live/ru?full=true'
);
// Топ-матчи выбранного спорта, только Prematch
const toplist = await sportApiGet(
`/v1/toplist/${prematchSportId}/ru?full=true`
);
// Киберспортивное Live-меню
const cybersportMenu = await sportApiGet(
'/v1/menu/live/ru?cybersport=true'
);
Переменные sportId, countryId и prematchSportId в этих фрагментах должны быть
получены из актуальных ответов соответствующего типа линии.
Обработка Game id finished
const payload = await sportApiGet(
`/v1/event/${gameId}/group/live/ru`
);
const result = classifyBody(payload);
if (result.type === 'message') {
if (result.message === 'Game id finished') {
console.log('Stop updating this game_id and refresh the Live events list.');
} else {
console.log('SportAPI event message:', result.message);
}
}
Game id finished не позволяет определить, завершился матч, был отменён или перешёл из
Prematch в Live. После сообщения прекратите запросы по старому ID и обновите events.
Пустой список — не ошибка
const payload = await sportApiGet(
'/v1/events/SPORT_ID_FROM_MENU/0/sub/50/live/ru'
);
const result = classifyBody(payload);
if (result.type === 'empty') {
console.log('There are currently no matches in this selection.');
}
Не заменяйте пустой актуальный ответ ранее сохранёнными матчами.
Что логировать при ошибке
Можно записывать:
- время запроса;
- путь метода без ключа;
- HTTP-статус;
error_codeиerror_message;body.message;- используемые ID и тип линии.
Не записывайте значение заголовка Package в логи.
Что не делает пример
Код не является готовым SDK и не реализует:
- периодическое обновление;
- автоматические повторные запросы;
- кэш приложения;
- типы TypeScript;
- пользовательский интерфейс;
- систему приёма и расчёта ставок.
Рекомендуемая частота запросов описана отдельно: «Рекомендации по обновлению данных».