Sportlive

Sportlive eSoccer API guide

What you get

Connect your server to pre-match and live eSoccer odds. Get fixture metadata, price changes, bet stops, results, settlements, cancellations and rollbacks in one API. Your backoffice controls your margins. The API delivers your prices.

Use the catalog to find matches. Read an odds snapshot, then keep it current with the stream. Read the accounting log to recover settlements and corrections.

Open the API reference for response schemas, or download the OpenAPI document. Measured end-to-end latency (p50/p95) will be published here after the current measurement.

Authentication

Send your API key in X-API-Key. Keep it on your server. Your server sends the data your application needs to browsers and mobile apps.

Setting Value
BASE API origin: https://feed.sportlive.hr (dev: https://dev.feed.sportlive.hr), without a trailing slash
API_KEY Your API key, stored in your server's environment
Transport HTTPS; JSON for REST, Server-Sent Events for the stream

The curl examples use these environment values. They each make one request.

1. Read the catalog

Read the sports and tournaments. Choose a fixture from a tournament's fixtures. Use its urn as FIXTURE below. Match the tournament's sportUrn to a sport's urn.

curl "$BASE/tenant-catalog-api/sports" -H "X-API-Key: $API_KEY"
curl "$BASE/tenant-catalog-api/tournaments" -H "X-API-Key: $API_KEY"
curl "$BASE/tenant-catalog-api/fixtures/$FIXTURE?locale=en" -H "X-API-Key: $API_KEY"
Resource Use
Sports and tournaments Discover the matches in your feed
Fixture Read competitors, start time and match metadata
Market descriptions Decode market ids, outcomes and specifiers using the reference

2. Get odds (snapshot + stream)

Read the current odds. Replace your book with the returned fixtures and save cut. Open the stream with Last-Event-ID: cut. Keep the same sport and fixture filters on both requests. Omit filters to receive your whole book.

curl "$BASE/tenant-odds-output/odds/fixtures?fixture=$FIXTURE" -H "X-API-Key: $API_KEY"
curl -N "$BASE/tenant-odds-output/odds/events?fixture=$FIXTURE" -H "X-API-Key: $API_KEY" -H "Last-Event-ID: $CUT"

Apply each named event before saving its SSE id. SSE data contains that event's JSON payload; the event name and resume id are separate SSE fields.

Event Action
snapshot_start Clear the cursor and begin a replacement book
fixture_snapshot Store data.fixture in the replacement book; outside a snapshot, replace that fixture
snapshot_complete Publish the replacement book atomically and save the id
odds_change Merge changed market lines and outcomes; apply matchStatus
bet_stop Suspend the fixture or the listed market lines
fixture_change Apply metadata changes; nextStartTime carries a changed start time
fixture_removed Remove the fixture
alive Refresh the keep-alive deadline
bet_settlement Remove settled price lines and wake your accounting reader
bet_cancel Remove price lines for a cancel without from, to or operation; wake accounting
rollback_bet_settlement, rollback_bet_cancel Wake accounting to recover corrections

3. Reconnect

Reconnect with the last id you successfully applied. Keep ids opaque. Both examples start from a REST snapshot and resume after a network disconnect.

Supply two application functions: replace_book(fixtures) swaps your book atomically; apply_event(name, data) applies the table above. In JavaScript these are replaceBook and applyEvent. They must finish successfully before the cursor moves. They are application hooks, not libraries supplied by this API.

Python (standard library)

import json, os, time, urllib.request, urllib.error
from your_app import replace_book, apply_event
base, key = os.environ['BASE'], os.environ['API_KEY']
headers = {'X-API-Key': key}
request = urllib.request.Request(base + '/tenant-odds-output/odds/fixtures', headers=headers)
with urllib.request.urlopen(request, timeout=30) as response:
    snapshot = json.load(response)
replace_book(snapshot['fixtures'])
cursor, staging = snapshot['cut'], None
while True:
    request = urllib.request.Request(base + '/tenant-odds-output/odds/events',
                                    headers={**headers, 'Last-Event-ID': cursor})
    fields, data = {}, []
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            for raw in response:
                line = raw.decode().rstrip('\r\n')
                if line and not line.startswith(':'):
                    name, _, value = line.partition(':')
                    value = value.removeprefix(' ')
                    if name == 'data': data.append(value)
                    else: fields[name] = value
                elif not line:
                    if data:
                        name, payload = fields['event'], json.loads('\n'.join(data))
                        if name == 'snapshot_start': cursor, staging = '', []
                        elif name == 'fixture_snapshot' and staging is not None:
                            staging.append(payload['fixture'])
                        elif name == 'snapshot_complete':
                            if staging is not None: replace_book(staging)
                            staging = None
                        else: apply_event(name, payload)
                        if 'id' in fields: cursor = fields['id']
                    fields, data = {}, []
    except urllib.error.HTTPError:
        raise  # Handle the status table before retrying.
    except (OSError, EOFError): pass
    if staging is not None: cursor, staging = '', None
    time.sleep(2)

JavaScript (Node, built-in fetch)

import { replaceBook, applyEvent } from './your-app.js';
const { BASE, API_KEY } = process.env;
const headers = { 'X-API-Key': API_KEY };
const snapshotResponse = await fetch(BASE + '/tenant-odds-output/odds/fixtures', { headers });
if (!snapshotResponse.ok) throw new Error(`HTTP ${snapshotResponse.status}`);
const snapshot = await snapshotResponse.json();
await replaceBook(snapshot.fixtures);
let cursor = snapshot.cut, staging = null;
while (true) {
  let response; try {
    response = await fetch(BASE + '/tenant-odds-output/odds/events', {
      headers: { ...headers, 'Last-Event-ID': cursor },
    });
  } catch { await new Promise(r => setTimeout(r, 2000)); continue; }
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  let buffer = '', fields = {}, data = []; try {
    for await (const chunk of response.body.pipeThrough(new TextDecoderStream())) {
      buffer += chunk; let end;
      while ((end = buffer.indexOf('\n')) >= 0) {
        const line = buffer.slice(0, end).replace(/\r$/, ''); buffer = buffer.slice(end + 1);
        if (line && !line.startsWith(':')) {
          const colon = line.indexOf(':'), name = colon < 0 ? line : line.slice(0, colon);
          const value = colon < 0 ? '' : line.slice(colon + 1).replace(/^ /, '');
          if (name === 'data') data.push(value); else fields[name] = value;
        } else if (!line) {
          if (data.length) {
            const name = fields.event, payload = JSON.parse(data.join('\n'));
            if (name === 'snapshot_start') { cursor = ''; staging = []; }
            else if (name === 'fixture_snapshot' && staging !== null) staging.push(payload.fixture);
            else if (name === 'snapshot_complete') { if (staging !== null) await replaceBook(staging); staging = null; }
            else await applyEvent(name, payload);
            if ('id' in fields) cursor = fields.id;
          }
          fields = {}; data = []; }
      }
    }
  } catch (error) { if (!(error instanceof TypeError)) throw error; }
  if (staging !== null) { cursor = ''; staging = null; }
  await new Promise(r => setTimeout(r, 2000));
}

Keep your last applied book while disconnected. Add a keep-alive watchdog to your application: alive arrives every 10 seconds; reconnect after 30 seconds without it. The examples stop on HTTP errors so you can handle access and retry decisions below.

4. Settlements and accounting

Run a separate accounting reader. Start with since in epoch milliseconds: the oldest ticket placement you still need to correct, or now if there are no tickets.

curl "$BASE/tenant-odds-output/odds/accounting?since=$SINCE&limit=500" -H "X-API-Key: $API_KEY"
curl "$BASE/tenant-odds-output/odds/accounting?after=$NEXT&limit=500" -H "X-API-Key: $API_KEY"
curl "$BASE/tenant-odds-output/odds/accounting/fixtures?fixture=$FIXTURE&since=$SINCE" -H "X-API-Key: $API_KEY"

Apply events in order. Save next in the same transaction as ticket and wallet changes. Continue with after=next, omitting since. Use accountingId to recognise the same effect in the stream and the log. The accounting cursor is independent of SSE.

Response Action
more=true Read the next page immediately
No more events Poll every five seconds; stream accounting events and snapshot_start wake the reader
gap=true Repair all tickets still subject to correction before committing the page
Fixture repair Request repeated fixture ids, at most 50 per call; include the oldest correction-window since
Missing fixture or complete=false Leave tickets unresolved and contact support

Repair settled lines with their factors, void cancelled lines and reopen open lines. Include paid, voided and hidden tickets in repair. Commit repair, page effects and next atomically. The correction window is 72 hours. Accounting changes bets and wallets; it does not edit the odds book.

Errors and limits

Check the HTTP status before applying a response. Keep the last applied state and cursor on failures. Your backoffice shows the limits assigned to each API key.

Status Meaning Next step
429 requests_per_minute or concurrent_streams limit reached Close redundant streams; wait for Retry-After seconds
402 Access paused for payment Follow payUrl; contact support if absent; resume after access is restored
403 suspended or not_entitled Contact support about access
503 API unavailable, snapshot warming or accounting recovering Wait for Retry-After seconds, then retry

Each stream consumes one concurrent-stream slot until it closes. Opening a stream also counts toward requests per minute. Catalog, snapshots, status and accounting requests count too. A 401 means the key is missing or unknown.

Support

Write to support@sportlive.hr. Include the route, HTTP status and time of the issue. Keep your API key private.

Details

Case Handling
Absent SSE id Leave the cursor unchanged
Empty SSE id Clear the cursor
Interrupted replacement snapshot Discard staging; reconnect with an empty cursor
snapshot_start reasons connect, resume_gap, overflow, partial, warmed; all begin a replacement
Partial snapshot Keep its book current; a warmed replacement follows recovery
Empty catalog or zero live fixtures Retry discovery later; use the status route in the reference to check producers
Process restart Read a fresh REST snapshot; recover accounting from its separately persisted cursor