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 |