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

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Значение
dataAPI вернул данные метода
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;
  • пользовательский интерфейс;
  • систему приёма и расчёта ставок.

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

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