Zintegruj dane kursowe ArbiScan: klucz API, przykłady Python i cURL, filtrowanie wydarzeń, paginacja, świeżość danych, webhooki i obsługa błędów. Pełna lista endpointów.
Kontrakt sprawdzony w kodzie backendu: 04.10.2026. Przykłady pochodzą z testów offline i zawierają fikcyjne wydarzenia. Nie potwierdzają bieżącego pokrycia ani stanu wdrożenia.
Dostęp i klucz
Wybierz osobny plan API. Po potwierdzeniu płatności utwórz klucz w panelu „API i klucze” i zapisz sekret wyświetlany tylko raz. Abonament skanera nie jest wymagany i sam nie daje dostępu do API.
Ustaw ARBISCAN_API_KEY wyłącznie w środowisku serwera. Wysyłaj nagłówek X-API-Key przez HTTPS. Nie umieszczaj klucza w URL, kodzie przeglądarki, localStorage, logach ani analityce. Po ujawnieniu przeprowadź rotację i unieważnij stary sekret w panelu.
Pierwsze żądanie
Adres bazowy: https://api.arbiscan.pl/v1. Zacznij od małej strony. Playground działa lokalnie, bez klucza i bez zużycia limitu; skopiowany kod uruchamiasz na swoim serwerze.
Klucz zapisujesz na swoim serwerze. Tego przykładu nie wysyłamy do API.
OdpowiedźPodgląd struktury
{
events[]
Wydarzenia
bookmakers[]
Źródła kursów
markets[]
Rynki i warianty zakładu
last_update
Czas obserwacji
}
Uruchom przykład, żeby zobaczyć JSON.
Źródła, rynki i częstotliwość
Odczytaj /sports, /leagues, /bookmakers i /markets. Nazwy w filtrach bierz z odpowiedzi katalogu: np. Betclic lub Superbet dla polskiej oferty tych marek. Nie zakładaj dostępności wszystkich rynków ani wszystkich wydarzeń. h2h oznacza rynek wyniku/zwycięzcy; dla linii używaj pełnego klucza, np. Total|2.5. Normalizacja liczby usuwa zbędne zera, ale nie znak handicapu.
API udostępnia ofertę przedmeczową. Aktualizacje zależą od źródła i publikacji; kontrakt nie gwarantuje stałego interwału ani SLA. Próg wieku 60–900 s jest filtrem, nie częstotliwością aktualizacji. Oceniaj czasy obserwacji i /health. Historia kursów i strumień zmian cen są obecnie niedostępne.
Pola odpowiedzi /odds
counthas_morenext_cursor
Liczba wydarzeń na tej stronie; flaga i kursor kontynuacji. Ostatnia strona: false i null.
Identyfikator pełnego snapshotu, generacja publikacji i aktywnego zestawu. Porównuj cały snapshot, nie tylko id: częściowa publikacja może zmienić generację bez zmiany id.
Czasy utworzenia, przyjęcia, zmiany publikacji i odczytu pierwszej strony. read_at pozostaje stały podczas paginacji. Przed pierwszą pełną publikacją część metadanych może być null.
snapshot.event_countsnapshot.outcome_count
Liczebność pełnej publikacji. Nie jest liczbą wyników Twoich filtrów ani liczbą pozostałych stron.
Wariant, kurs dziesiętny i jego oryginalny czas obserwacji. To ten timestamp służy do oceny wieku konkretnego kursu. Nazwy mogą zachowywać warianty pisowni źródła.
Przyjęte próby w minucie UTC i pozostały limit tempa. Błędy nie zwracają miejsca w limicie minutowym.
Opcjonalne pola bookmakers[].bookmaker_event_path i bookmakers[].bookmaker_event_slug opisują nawigację przy tym samym natywnym ID. Nie są pełnymi URL. Brak pola oznacza brak informacji; nie zgaduj ścieżki z nazw drużyn. Ścieżka Betsport pochodzi z hierarchii źródła. Slug, jeśli jest zwracany, dotyczy wydarzenia nadrzędnego. Budując link, sprawdzaj origin HTTPS i koduj segmenty URL. /odds nie zwraca pola event_url.
Paginacja: jeden zachowany zestaw
Wyślij pierwsze żądanie z filtrami i limit od 1 do 500 (domyślnie 200). Backend zachowuje cały wynik, jeśli potrzeba więcej stron.
Gdy has_more=true, podaj next_cursor jako cursor. Najprościej wysłać tylko cursor i limit. Jeśli powtarzasz filtry, powtórz cały zestaw; nie obliczaj na nowo względnych dat między stronami. Możesz zmienić limit.
Kursor v4 jest związany z pulą konta i filtrami. Zestaw jest zachowany przez 240 s od przygotowania; kolejne strony nie wydłużają tego czasu. Nowe publikacje i restart procesu nie zmieniają przyjętego zestawu.
Zakończ przy has_more=false i next_cursor=null. Publikuj wynik dopiero po odebraniu wszystkich stron. Przy 409 snapshot_changed lub cursor_version_expired odrzuć bufor i zacznij od nowa, z ograniczoną liczbą prób.
Ograniczenie tej wersji: parser odrzuca kursory z przesunięciem powyżej 10 000 wydarzeń (400 invalid_request), choć magazyn może przyjąć większy zestaw. Dziel szerokie odczyty na mniejsze zakresy commence_from/commence_to i deduplikuj id na wspólnej granicy dat (obie granice są włączne). Nie edytuj kursora. Każdy zakres jest osobnym snapshotem; nie traktuj kilku zakresów jako jednego atomowego odczytu.
Poniższe programy pobierają jeden kompletny zestaw. Wymagają zmiennej ARBISCAN_API_KEY. Używają trzech prób rozpoczęcia od nowa, ograniczonych ponowień 429/503 oraz marginesu 230 s. Błąd sieci kończy odczyt bez częściowego wyniku. Dla dużych zbiorów zastąp bufor pamięci transakcją lub plikiem tymczasowym, zatwierdzanym dopiero na końcu.
# Python 3, standard library only. Set ARBISCAN_API_KEY on your server.
import json
import os
import random
import time
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
def read_odds():
key = os.environ['ARBISCAN_API_KEY']
first = dict(sport='Football', bookmakers='Betclic', markets='h2h',
max_age_seconds=900, limit=500)
for restart in range(3):
events, seen = [], set()
cursor, snapshot, retries = None, None, 0
started = time.monotonic()
while True:
query = dict(cursor=cursor, limit=500) if cursor else first
request = Request('https://api.arbiscan.pl/v1/odds?' + urlencode(query),
headers={'X-API-Key': key})
try:
response = urlopen(request, timeout=30)
except HTTPError as error:
response = error
with response:
status, headers = response.status, response.headers
try:
body = json.load(response)
except (ValueError, UnicodeError):
body = {} # A proxy may return HTML.
code = body.get('code')
if status == 409 and code in ('snapshot_changed', 'cursor_version_expired'):
break # Discard this buffer and restart from page one.
if status in (429, 503) and code != 'total_quota_exceeded' and retries < 3:
retry_after = headers.get('Retry-After')
try:
delay = float(retry_after)
except (TypeError, ValueError):
try:
delay = parsedate_to_datetime(retry_after).timestamp() - time.time()
except (TypeError, ValueError, AttributeError):
delay = 2 ** retries
delay = max(0, delay)
if time.monotonic() - started + delay >= 230:
raise RuntimeError('Retry later; discard partial traversal')
retries += 1
time.sleep(delay + random.random() * 0.25)
continue
if not 200 <= status < 300:
raise RuntimeError(f'{status} {code or "non_json_error"}')
retries = 0
if snapshot is not None and snapshot != body['snapshot']:
raise RuntimeError('Snapshot mismatch')
snapshot = body['snapshot']
events.extend(body['events'])
if not body['has_more']:
return events
cursor = body['next_cursor']
if not cursor or cursor in seen:
raise RuntimeError('Invalid continuation')
seen.add(cursor)
if time.monotonic() - started >= 230:
raise RuntimeError('Traversal too slow; discard partial results')
raise RuntimeError('Restart budget exhausted')
if __name__ == '__main__':
print(json.dumps(read_odds()))
// Node.js 20+; run on your server with ARBISCAN_API_KEY set.
// Print only a complete traversal. Never mix pages from different traversals.
export async function readOdds() {
const key = process.env.ARBISCAN_API_KEY;
if (!key) throw new Error('Set ARBISCAN_API_KEY');
const first = { sport: 'Football', bookmakers: 'Betclic', markets: 'h2h',
max_age_seconds: '900', limit: '500' };
for (let restart = 0; restart < 3; restart++) {
const events = [], seen = new Set();
let cursor = null, snapshot = null, retry = 0;
const started = Date.now();
while (true) {
const query = new URLSearchParams(cursor ? { cursor, limit: '500' } : first);
const response = await fetch('https://api.arbiscan.pl/v1/odds?' + query, {
headers: { 'X-API-Key': key }, signal: AbortSignal.timeout(30_000),
});
// Proxy failures may not contain JSON.
const body = await response.json().catch(() => ({}));
if (response.status === 409 &&
['snapshot_changed', 'cursor_version_expired'].includes(body.code)) break;
if ([429, 503].includes(response.status) && body.code !== 'total_quota_exceeded' && retry < 3) {
const header = response.headers.get('Retry-After');
const seconds = header === null ? NaN : Number(header);
const delay = Number.isFinite(seconds) ? seconds * 1000
: header && Number.isFinite(Date.parse(header)) ? Math.max(0, Date.parse(header) - Date.now()) : 1000 * 2 ** retry;
// Stop instead of sleeping past the retained traversal's lifetime.
if (Date.now() - started + delay >= 230_000) throw new Error('Retry later; discard partial traversal');
retry++;
await new Promise(resolve => setTimeout(resolve, Math.max(0, delay) + Math.random() * 250));
continue;
}
if (!response.ok) throw new Error(`${response.status} ${body.code || 'non_json_error'}`);
retry = 0;
const identity = JSON.stringify(body.snapshot);
if (snapshot !== null && snapshot !== identity) throw new Error('Snapshot mismatch');
snapshot = identity;
events.push(...body.events);
if (!body.has_more) return events;
if (!body.next_cursor || seen.has(body.next_cursor)) throw new Error('Invalid continuation');
seen.add(body.next_cursor);
cursor = body.next_cursor;
if (Date.now() - started >= 230_000) throw new Error('Traversal too slow; discard partial results');
}
// A 409 restarts with the original filters and a new empty buffer.
}
throw new Error('Restart budget exhausted');
}
// Import readOdds() into your app, or run: node odds-pagination.mjs
if (process.argv[1] && import.meta.url === new URL('file://' + process.argv[1]).href) {
console.log(JSON.stringify(await readOdds()));
}
Świeżość i brak danych
freshness.mode zawsze wynosi fresh_prematch. include_stale jest niedozwolony, także z false (400 invalid_request). Filtry wieku kursów i początku wydarzeń są oceniane przy snapshot.read_at = freshness.as_of, czyli podczas pierwszego odczytu. Kolejna strona zachowuje kursy i ich timestamps, nawet jeśli w międzyczasie zdarzenie się rozpoczęło. Przed użyciem danych ponownie sprawdź wiek każdego kursu i commence_time względem bieżącego czasu; w razie potrzeby wykonaj nowy odczyt.
Puste events z 200 jest prawidłowe, gdy nic nie spełnia filtrów. /events/{event_id} zwraca availability=stale lub no_visible_markets i pustą listę bookmakers, gdy brakuje aktualnych ofert, a 410 event_expired po rozpoczęciu. /health: ok=true oznacza dostępność procesu i bazy, nie świeże dane; sprawdź data_fresh i data_status. Nie zamieniaj braku kursu na zero ani nie odświeżaj timestampu w cache.
Błędy i sposób reakcji
Błędy aplikacji zawierają error (tekst) i code (kod do obsługi programowej), czasem dodatkowe pola. Nie parsuj tekstu error jako kodu.
Respektuj Retry-After. Limit dzienny odnawia się o 00:00 UTC. Limit całkowity wymaga zmiany dostępu; 86400 s nie obiecuje odnowienia jutro.
503
pagination_capacity
Brak miejsca na zachowanie zestawu. Retry-After: 2. Ogranicz daty, źródła lub rynki, jeśli zapytanie jest zbyt duże; mniejszy limit strony nie zmniejsza całego zestawu.
5xx
internal_error
Zachowaj X-Request-ID. Ogranicz liczbę ponowień i zastosuj opóźnienie. Proxy może zwrócić odpowiedź inną niż JSON.
Nagłówki do diagnostyki
X-Request-ID
Identyfikator do zgłoszenia błędu; zapisuj go razem ze statusem, bez sekretu.
Limit minutowy, pozostałe próby i reset w ISO-8601 UTC. Przy daily_quota_exceeded Reset wskazuje północ UTC, nie następną minutę.
X-DailyLimit-Limit / X-DailyLimit-Remaining
Limit i dostępna pula dzienna, z uwzględnieniem rezerwacji.
X-TotalLimit-Limit / X-TotalLimit-Remaining
Występują, gdy klucz ma limit całkowity.
Cache-Control: private, no-store
Odpowiedzi nie nadają się do współdzielonego cache HTTP.
Limity, rozliczanie i budżet
Każda odpowiedź 2xx z mierzonego endpointu danych kosztuje jedną jednostkę puli dziennej i całkowitej — także pusta strona. Odpowiedzi 4xx/5xx zwracają rezerwację, ale przyjęta próba pozostaje w limicie minutowym. /health i /usage są bezpłatne; /usage ma oddzielny limit 30/min i działa także po wyczerpaniu puli. Zarządzane klucze jednego konta dzielą pulę, dodatkowy klucz jej nie mnoży.
Żądania w toku tymczasowo rezerwują jednostkę. Osierocona rezerwacja po awarii procesu jest zwracana po 600 s przy następnym żądaniu lub odczycie /usage. Limit dzienny odnawia się o 00:00 UTC; limit całkowity nie musi się odnawiać. today.used i total.used w /usage to zużycie konta, a history to historia liczby żądań, nie historia kursów.
Plan
2xx / dzień
Próby / min
Klucze
Starter
1 000
15
1
Data
10 000
30
1
Data Plus
100 000
180
3
Limity katalogowe z 04.10.2026; uprawnienia Twojego konta i /usage są rozstrzygające. Jeden udany odczyt na minutę to 1 440 żądań/dobę, a trzy strony na odczyt — 4 320. Dolicz katalogi i udane ponowienia. Nie dobieraj interwału wyłącznie do limitu minutowego.
Webhooks · Data Plus
Do 3 aktywnych adresów. Wybierz snapshot.updated i po powiadomieniu pobierz nowy zestaw przez REST. Dostęp wymaga klucza przypisanego do konta z odpowiednim uprawnieniem; moduł musi być włączony w usłudze. Lista: 30/min; utworzenie i usunięcie: 10/min; test: 5/min. Te operacje zarządzania nie korzystają z licznika odczytów danych.
POST /webhooks zwraca webhook oraz signing_secret tylko raz. GET zwraca webhooks i limits (destinations, delivery_attempts, timeout_seconds). DELETE zwraca 204 bez body, test — 202 z queued i event_id. Test faktycznie kolejkuje dostawę; playground jej nie wysyła.
Sprawdzaj X-ArbiScan-Signature: t=<timestamp>,v1=<hex>. Podpis to HMAC-SHA256(secret, timestamp + '.' + surowe bajty body). Porównuj stałoczasowo, sprawdzaj wiek timestampu i deduplikuj X-ArbiScan-Event-ID. Pozostałe nagłówki: X-ArbiScan-Event i X-ArbiScan-Timestamp. Maksymalnie 4 próby dostawy, timeout 5 s; odstępy ponowień 60, 300, 1800 s. Potwierdzaj odbiór odpowiedzią 2xx.
Strumień WebSocket — ceny obecnie niedostępne
Zachowany adres: wss://api.arbiscan.pl/v1/stream, nagłówek X-API-Key z serwera i dostęp Data Plus. Parametry sports i bookmakers: listy rozdzielone przecinkami, do 20 różnych nazw, każda do 100 znaków; dopasowanie bez rozróżniania wielkości liter. cursor: liczba całkowita ≥ 0, domyślnie bieżący koniec historii; nie jest to kursor /odds. Jedno połączenie na konto. Gdy historia jest wyłączona, nie oczekuj zmian cen odds.changed. Komunikaty ready i heartbeat nie potwierdzają aktualizacji kursów. Błąd WebSocket ma oddzielny format type=error, code, message; nie jest odpowiedzią HTTP {error, code}.
Endpointy i parametry
Wszystkie ścieżki poniżej mają prefiks /v1. Parametry są opcjonalne, o ile nie oznaczono inaczej. Daty wysyłaj ze strefą UTC (Z); brak strefy jest interpretowany jako UTC. Parametry zapytania koduj jako URL.
Wydarzenia i kursy prematch z paginacją zachowanego zestawu.
Kursor v4 zachowuje pełny uporządkowany wynik przez 240 s od przygotowania zestawu. Kontynuacja przyjmuje sam cursor i opcjonalnie limit. Jeśli podasz filtry, cały znormalizowany zestaw musi być identyczny. Kursor jest przypisany do puli konta. Nowa publikacja ani restart procesu nie unieważnia przyjętego kursora. Niezgodność filtrów lub konta: 400 cursor_filter_mismatch. include_stale jest odrzucany także z wartością false.
Parametr
Miejsce
Znaczenie i granice
sport
query
Dokładna nazwa zwracana przez odpowiedni katalog; wielkość liter nie ma znaczenia. Do 128 znaków.
league
query
Dokładna nazwa zwracana przez odpowiedni katalog; wielkość liter nie ma znaczenia. Do 256 znaków.
bookmakers
query
Wartości oddzielone przecinkami: do 100 różnych wartości, każda do 256 znaków. Dokładne dopasowanie bez rozróżniania wielkości liter; bez wyszukiwania przybliżonego.
markets
query
Wartości oddzielone przecinkami: do 100 różnych wartości, każda do 256 znaków. Dokładne dopasowanie bez rozróżniania wielkości liter; bez wyszukiwania przybliżonego.
commence_from
query
Dolna granica początku wydarzenia, włącznie (ISO-8601). Musi być ≤ commence_to. Zdarzenia rozpoczęte pozostają wykluczone.
commence_to
query
Górna granica początku wydarzenia, włącznie (ISO-8601).
Historia jest niedostępna: 403 bez uprawnienia, 410 po sprawdzeniu uprawnienia. Poniższe parametry dotyczą kontraktu po ponownym włączeniu.
Parametr
Miejsce
Znaczenie i granice
event_idwymagany
path
id lub canonical_id z /odds; obsługiwane są też zachowane aliasy. Zakoduj jako segment URL.
bookmaker
query
Nazwa z /bookmakers. W /markets bez rozróżniania wielkości liter, w historii dokładne dopasowanie. Do 256 znaków.
market
query
Dokładny klucz rynku z /markets. Do 512 znaków. Domyślnie: h2h.
since
query
Początek okresu historii, włącznie, ISO-8601; musi być ≤ until.
until
query
Koniec okresu historii, włącznie, ISO-8601.
limit
query
Liczba elementów na stronie, liczba całkowita. 1–5000. Domyślnie: 1000.
cursor
query
Nieprzezroczysta wartość next_cursor z tego samego endpointu. Nie twórz ani nie modyfikuj kursora. Do 2048 znaków.
HTTP: 200 · 403 · 410
Kontrakt historii po włączeniu — obecnie niedostępny
Przykład uzyskany wyłącznie w teście z włączoną historią. history[] zawiera bookmaker, outcome, price, timestamp; observed_from i observed_until opisują zaobserwowany zakres, nie gwarantują pełności. Retencja: 48 h. Zachowuj event_id, market, bookmaker, since i until na każdej stronie; kursor historii jest odrębny od kursora /odds.
Publiczny adres HTTPS, port 443, bez loginu, hasła i fragmentu. Adresy prywatne są odrzucane. Do 2048 znaków.
events
Unikalne nazwy zdarzeń. Wybierz snapshot.updated; odds.changed nie dostarcza cen, gdy historia jest wyłączona. Pominięcie wybiera oba typy. odds.changed, snapshot.updated.
Drabinki linii piłkarskich: rożne, kartki i punkty. Bez potwierdzenia zasad rozliczenia i kompletności źródła.
Parametr
Miejsce
Znaczenie i granice
event_idwymagany
path
id lub canonical_id z /odds; obsługiwane są też zachowane aliasy. Zakoduj jako segment URL.
bookmakers
query
Nazwy oddzielone przecinkami: do 20 różnych wartości, każda do 64 znaków. Inne parametry są odrzucane. Domyślnie: Sts, Betclic, LvBet, Fuksiarz. Dopasowanie bez rozróżniania wielkości liter. Do 1000 znaków.
HTTP: 200 · 400 · 401 · 404 · 410 · 422 · 429
Pełny przykład żądania i odpowiedzi · 200
Syntetyczne dane z testu backendu. Czas, limity i kursor służą wyłącznie ilustracji; kursor nie działa w usłudze.
2026-10-04 — opcjonalne ścieżki/slugi powiązane z natywnym ID źródła; uzupełniona referencja drabinek i pól odpowiedzi.
2026-10-03 — zachowane zestawy 240 s i kursor v4; snapshot.read_at / freshness.as_of; nagłówki generacji; jednolite error i code; rozliczanie tylko 2xx i rezerwacje limitu.
Daty opisują kontrakt w kodzie, nie potwierdzenie wdrożenia. Funkcje zależne od konfiguracji pozostają zależne od stanu usługi.