---
id: example-javascript
title: Sport Line API — JavaScript Example
description: Example of connecting to Sport Line API from Node.js using the built-in fetch function.
version: v1
language: en
---

# Sport Line API — JavaScript Example

## Requirements

This example requires Node.js 18 or later, where `fetch()` is available without an
additional library.

Check the version:

```bash
node --version
```

## Where to Make Requests

This example is intended for a Node.js backend application. Do not place the Sport Line
API key in JavaScript that runs in the browser: website users could see it in the source
code and network requests.

If a browser interface needs the data, the browser 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

```js
const baseUrl = process.env.SPORTAPI_BASE_URL;
const apiKey = process.env.SPORTAPI_PACKAGE_KEY;

const response = await fetch(`${baseUrl}/v1/menu/live/en`, {
  headers: {
    Package: apiKey,
    Accept: 'application/json'
  }
});

const payload = await response.json();
console.log(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;
- checks the HTTP status;
- parses JSON;
- recognizes `error_code` and `error_message`;
- validates the standard `status`, `page`, and `body` envelope.

```js
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()` does not throw an exception for `4xx` and `5xx` HTTP responses by itself, so
the code checks `response.ok` separately. It must also look for an API error in the JSON
instead of relying only on the HTTP status.

## Classifying `body`

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

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

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.

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

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`:

```js
function findFirstGame(eventsBody) {
  for (const tournament of eventsBody) {
    const game = tournament.events_list?.[0];

    if (game) {
      return game;
    }
  }

  return null;
}
```

## Complete `menu → events → event` Example

Save the following code as `sportapi-example.mjs`:

```js
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 = 'en';

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

Run it with:

```bash
SPORTAPI_BASE_URL='https://YOUR_API_DOMAIN' \
SPORTAPI_PACKAGE_KEY='YOUR_API_KEY' \
node sportapi-example.mjs
```

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`:

```js
const eventsPayload = await 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`:

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

The returned odds apply only to the selected submatch.

## Match Search

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

```js
const searchText = encodeURIComponent('Manchester City');
const searchPayload = await sportApiGet(
  `/v1/search/line/en/${searchText}`
);
const 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.

```js
// Live sports
const sports = await sportApiGet('/v1/sports/live/en');

// Countries for a current sport
const countries = await sportApiGet(
  `/v1/countries/${sportId}/live/en`
);

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

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

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

// Esports Live menu
const cybersportMenu = await 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`

```js
const payload = await sportApiGet(
  `/v1/event/${gameId}/group/live/en`
);
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` 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

```js
const payload = await sportApiGet(
  '/v1/events/SPORT_ID_FROM_MENU/0/sub/50/live/en'
);
const result = classifyBody(payload);

if (result.type === 'empty') {
  console.log('There are currently no matches in this selection.');
}
```

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;
- TypeScript types;
- 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)
- [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)
