Przygotuj pierwsze zapytanie API.

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.

Otwórz panel 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.

Sprawdź odpowiedź APIDane demo
GET/v1/odds
curl --fail-with-body 'https://api.arbiscan.pl/v1/odds?bookmakers=Betclic&markets=h2h&max_age_seconds=900&limit=1' \
  --header "X-API-Key: $ARBISCAN_API_KEY"

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.
snapshot.idsnapshot.publication_generationsnapshot.active_generation
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.
snapshot.generated_atsnapshot.accepted_atsnapshot.changed_atsnapshot.read_at
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.
freshness.modefreshness.max_age_secondsfreshness.as_of
fresh_prematch, zastosowany próg wieku i chwila oceny: taka sama jak snapshot.read_at.
events[].idevents[].canonical_id
Kanoniczna tożsamość wydarzenia w ArbiScan. To nie jest ID bukmachera.
events[].sportevents[].disciplineevents[].event_scopeevents[].leagueevents[].source_leagues
Sport, dyscyplina, zakres wydarzenia, liga i oryginalne etykiety lig. Nie łącz wydarzeń wyłącznie po nazwach drużyn.
events[].home_teamevents[].away_teamevents[].commence_timeevents[].last_seen
Kanoniczna kolejność stron, początek wydarzenia i obserwacja metadanych. last_seen nie zastępuje czasu obserwacji kursu.
events[].portfolio.truncatedevents[].portfolio.reasonevents[].portfolio.outcome_limitevents[].portfolio.outcomes_beforeevents[].portfolio.outcomes_after
Informacja o ograniczeniu zakresu rynków przez publikację. false nie gwarantuje pełnej oferty bukmachera; wartości szczegółowe mogą być null.
events[].bookmakers[].keyevents[].bookmakers[].bookmaker_event_id
Nazwa źródła i jego natywne ID (może być null). W filtrach używaj nazw zwróconych przez /bookmakers, nie identyfikatorów z logo na stronie.
events[].bookmakers[].last_updateevents[].bookmakers[].markets[].last_update
Najnowsza obserwacja w widocznych kursach źródła lub rynku. Poszczególne warianty mogą być starsze.
events[].bookmakers[].markets[].key
Dokładny klucz z /markets: np. h2h lub Total|2.5. Zachowaj rodzinę, linię i znak; nie odwracaj stron ani handicapów.
events[].bookmakers[].markets[].outcomes[].nameevents[].bookmakers[].markets[].outcomes[].priceevents[].bookmakers[].markets[].outcomes[].last_update
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.
quota.usedquota.limitquota.total_usedquota.total_limitquota.remaining
Dzisiejsze zużycie i limit oraz zużycie, limit i pozostała pula całkowita. remaining dotyczy limitu całkowitego; null oznacza brak takiego limitu.
quota.minute_usedquota.minute_limitquota.minute_remaining
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

  1. 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.
  2. 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.
  3. 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.
  4. 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.

cURL + Bash + jq

Pobierz działający przykład

#!/usr/bin/env bash
# Requires Bash, curl and jq. Set ARBISCAN_API_KEY on your server.
set -euo pipefail
: "${ARBISCAN_API_KEY:?Set ARBISCAN_API_KEY}"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
for restart in 1 2 3; do
  : > "$work/events.ndjson"
  : > "$work/cursors"
  cursor='' snapshot='' retry=0
  started=$SECONDS
  while :; do
    args=(--data-urlencode 'limit=500')
    if [[ -n "$cursor" ]]; then
      args+=(--data-urlencode "cursor=$cursor")
    else
      args+=(--data-urlencode 'sport=Football' --data-urlencode 'bookmakers=Betclic'
        --data-urlencode 'markets=h2h' --data-urlencode 'max_age_seconds=900')
    fi
    status=$(curl --silent --show-error --max-time 30 --get \
      'https://api.arbiscan.pl/v1/odds' --header "X-API-Key: $ARBISCAN_API_KEY" \
      "${args[@]}" --dump-header "$work/headers" --output "$work/page" --write-out '%{http_code}')
    code=$(jq -r '.code // empty' "$work/page" 2>/dev/null || true)
    if [[ "$status" == 409 && ( "$code" == snapshot_changed || "$code" == cursor_version_expired ) ]]; then
      break # The next attempt starts with an empty buffer.
    fi
    if [[ ( "$status" == 429 || "$status" == 503 ) && "$code" != total_quota_exceeded && $retry -lt 3 ]]; then
      delay=$(awk 'tolower($1)=="retry-after:" {gsub("\r", "", $2); print $2}' "$work/headers" | tail -1)
      # The API sends seconds. Stop on a date-valued proxy header; never retry early.
      [[ "$delay" =~ ^[0-9]+$ ]] || { echo 'Retry later; inspect Retry-After' >&2; exit 1; }
      (( SECONDS - started + delay < 230 )) || { echo 'Retry later; discard partial results' >&2; exit 1; }
      retry=$((retry + 1))
      sleep "$delay"
      continue
    fi
    [[ "$status" == 2?? ]] || { echo "HTTP $status $code; no partial output" >&2; exit 1; }
    retry=0
    current=$(jq -cS '.snapshot' "$work/page")
    [[ -z "$snapshot" || "$snapshot" == "$current" ]] || { echo 'Snapshot mismatch' >&2; exit 1; }
    snapshot=$current
    jq -c '.events[]' "$work/page" >> "$work/events.ndjson"
    if [[ $(jq -r '.has_more' "$work/page") == false ]]; then
      jq -s '.' "$work/events.ndjson"
      exit 0
    fi
    cursor=$(jq -er '.next_cursor | select(type == "string" and length > 0)' "$work/page")
    if grep -Fxq "$cursor" "$work/cursors"; then echo 'Repeated cursor' >&2; exit 1; fi
    printf '%s\n' "$cursor" >> "$work/cursors"
    (( SECONDS - started < 230 )) || { echo 'Traversal too slow' >&2; exit 1; }
  done
done
echo 'Restart budget exhausted; no partial output' >&2
exit 1
Python 3

Pobierz działający przykład

# 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()))
JavaScript · Node.js 20+

Pobierz działający przykład

// 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.

{
  "code": "snapshot_changed",
  "error": "Odds traversal expired; restart pagination"
}
HTTPcodeReakcja
400invalid_requestPopraw typ, długość, zakres lub daty. Usuń include_stale (także false).
400cursor_filter_mismatchWróć do zgodnych filtrów i konta lub zacznij nowy odczyt. Nie ponawiaj zmienionych filtrów z tym samym kursorem.
400invalid_filtersW drabinkach dozwolony jest tylko filtr bookmakers w podanych granicach.
401invalid_api_key / unauthorizedSprawdź X-API-Key, ważność klucza i konta. Bez automatycznej pętli ponowień.
403plan_upgrade_required / forbiddenSprawdź uprawnienia. Historia i webhooki wymagają odpowiedniego dostępu Data Plus.
404event_not_found / not_foundOdśwież katalog. Brak wydarzenia różni się od pustej listy kursów.
405method_not_allowedSprawdź metodę HTTP; odpowiedź może zawierać Allow.
409snapshot_changed / cursor_version_expiredOdrzuć częściowy wynik i rozpocznij od pierwszej strony. Pierwszy kod oznacza wygasły/brakujący zestaw, drugi stary kursor v1–v3.
409conflictPrzy webhookach sprawdź limit aktywnych adresów (3).
410event_expiredWydarzenie rozpoczęło się lub wygasło. Nie używaj go jako aktualnego prematch.
410goneHistoria jest wyłączona; disabled=true. Ponowienia nie włączą tej funkcji.
422unsupported_sportDrabinki obsługują Football.
429rate_limit_exceeded / minute_quota_exceeded / daily_quota_exceeded / total_quota_exceededRespektuj Retry-After. Limit dzienny odnawia się o 00:00 UTC. Limit całkowity wymaga zmiany dostępu; 86400 s nie obiecuje odnowienia jutro.
503pagination_capacityBrak 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.
5xxinternal_errorZachowaj 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.
X-Odds-Snapshot-ID / X-Odds-Publication-Generation / X-Odds-Active-Generation
Metadane odczytu na odpowiedziach danych. Kontynuacje podają zachowaną generację; ID bootstrap oznacza brak pełnego snapshotu (id=null w JSON).
Retry-After
Czas oczekiwania na 429; także 2 s przy pagination_capacity. Obsłuż datę HTTP, jeśli doda ją proxy. Nie ponawiaj wcześniej.
X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset
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.

Plan2xx / dzieńPróby / minKlucze
Starter1 000151
Data10 000301
Data Plus100 0001803

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.

Pobierz specyfikację OpenAPI z 04.10.2026

GET /health

Stan usługi i świeżość danych; bez klucza, bez opłaty za odczyt.

HTTP: 200 · 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.

curl --get 'https://api.arbiscan.pl/v1/health'
{
  "active_max_age_seconds": 900,
  "data_fresh": true,
  "data_status": "fresh",
  "events_known": 2,
  "fresh_events_known": 2,
  "last_ingest": "2026-10-04T12:00:00Z",
  "last_ingest_age_seconds": 0,
  "ok": true,
  "stale_events_known": 0
}

GET /sports

Sporty z aktualnymi ofertami prematch.

HTTP: 200 · 401 · 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.

curl --get 'https://api.arbiscan.pl/v1/sports' \
  --header "X-API-Key: $ARBISCAN_API_KEY"
{
  "sports": [
    "Football"
  ]
}

GET /leagues

Ligi z aktualnymi ofertami; opcjonalnie jeden sport.

ParametrMiejsceZnaczenie i granice
sportqueryDokładna nazwa z /sports; wielkość liter nie ma znaczenia.

HTTP: 200 · 401 · 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.

curl --get 'https://api.arbiscan.pl/v1/leagues' \
  --header "X-API-Key: $ARBISCAN_API_KEY" \
  --data-urlencode 'sport=Football'
{
  "leagues": [
    "League"
  ]
}

GET /bookmakers

Nazwy źródeł z aktualnymi ofertami. Używaj zwróconych nazw w filtrach.

HTTP: 200 · 401 · 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.

curl --get 'https://api.arbiscan.pl/v1/bookmakers' \
  --header "X-API-Key: $ARBISCAN_API_KEY"
{
  "bookmakers": [
    "Betclic",
    "Excluded"
  ]
}

GET /markets

Dostępne klucze rynków. Osobny kursor katalogu, bez gwarancji zachowania zestawu przez 240 s.

ParametrMiejsceZnaczenie i granice
sportqueryDokładna nazwa z /sports; wielkość liter nie ma znaczenia. Do 256 znaków.
leaguequeryDokładna nazwa zwracana przez odpowiedni katalog; wielkość liter nie ma znaczenia. Do 256 znaków.
bookmakerqueryNazwa z /bookmakers. W /markets bez rozróżniania wielkości liter, w historii dokładne dopasowanie. Do 256 znaków.
limitqueryLiczba elementów na stronie, liczba całkowita. 1–2000. Domyślnie: 500.
cursorqueryNieprzezroczysta wartość next_cursor z tego samego endpointu. Nie twórz ani nie modyfikuj kursora. Do 2048 znaków.

HTTP: 200 · 400 · 401 · 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.

curl --get 'https://api.arbiscan.pl/v1/markets' \
  --header "X-API-Key: $ARBISCAN_API_KEY" \
  --data-urlencode 'bookmaker=Betclic' \
  --data-urlencode 'limit=1'
{
  "count": 1,
  "has_more": true,
  "markets": [
    {
      "bookmaker_count": 1,
      "event_count": 2,
      "family": "h2h",
      "key": "h2h",
      "specifier": null
    }
  ],
  "next_cursor": "eyJtYXJrZXQiOiJoMmgiLCJ2IjoxfQ"
}

GET /odds

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.

ParametrMiejsceZnaczenie i granice
sportqueryDokładna nazwa zwracana przez odpowiedni katalog; wielkość liter nie ma znaczenia. Do 128 znaków.
leaguequeryDokładna nazwa zwracana przez odpowiedni katalog; wielkość liter nie ma znaczenia. Do 256 znaków.
bookmakersqueryWartoś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.
marketsqueryWartoś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_fromqueryDolna granica początku wydarzenia, włącznie (ISO-8601). Musi być ≤ commence_to. Zdarzenia rozpoczęte pozostają wykluczone.
commence_toqueryGórna granica początku wydarzenia, włącznie (ISO-8601).
max_age_secondsquery60–900 sekund. Domyślnie próg usługi (standardowo 900); sprawdź freshness.max_age_seconds w odpowiedzi. 60–900.
limitqueryLiczba elementów na stronie, liczba całkowita. 1–500. Domyślnie: 200.
cursorqueryNieprzezroczysta wartość next_cursor z tego samego endpointu. Nie twórz ani nie modyfikuj kursora. Do 2048 znaków.

HTTP: 200 · 400 · 401 · 409 · 429 · 503

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.

curl --get 'https://api.arbiscan.pl/v1/odds' \
  --header "X-API-Key: $ARBISCAN_API_KEY" \
  --data-urlencode 'sport=Football' \
  --data-urlencode 'bookmakers=Betclic' \
  --data-urlencode 'markets=h2h' \
  --data-urlencode 'max_age_seconds=900' \
  --data-urlencode 'limit=1'
{
  "count": 1,
  "next_cursor": "eyJ2Ijo0LCJ0cmF2ZXJzYWwiOiJjZjc1ZWY2MTk4NWI0YWFiOWE0Mjc5MTU4Zjc0NTA5YSIsIm9mZnNldCI6MX0",
  "has_more": true,
  "snapshot": {
    "id": null,
    "generated_at": null,
    "accepted_at": null,
    "event_count": 0,
    "outcome_count": 0,
    "publication_generation": 0,
    "active_generation": 0,
    "changed_at": "2026-10-04T12:00:00Z",
    "read_at": "2026-10-04T12:00:00Z"
  },
  "freshness": {
    "mode": "fresh_prematch",
    "max_age_seconds": 900,
    "as_of": "2026-10-04T12:00:00Z"
  },
  "quota": {
    "used": 1,
    "limit": 10000,
    "total_used": 1,
    "total_limit": null,
    "remaining": null,
    "minute_used": 1,
    "minute_limit": 10000,
    "minute_remaining": 9999
  },
  "events": [
    {
      "id": "event-00000",
      "canonical_id": "event-00000",
      "sport": "Football",
      "discipline": "unknown",
      "event_scope": "fixture",
      "league": "League",
      "source_leagues": [],
      "home_team": "Home",
      "away_team": "Away",
      "commence_time": "2026-10-04T13:00:00Z",
      "last_seen": "2026-10-04T12:00:00Z",
      "portfolio": {
        "truncated": false,
        "reason": null,
        "outcome_limit": null,
        "outcomes_before": null,
        "outcomes_after": null
      },
      "bookmakers": [
        {
          "key": "Betclic",
          "bookmaker_event_id": "fixture-native-001",
          "last_update": "2026-10-04T12:00:00Z",
          "markets": [
            {
              "key": "h2h",
              "last_update": "2026-10-04T12:00:00Z",
              "outcomes": [
                {
                  "name": "Away",
                  "price": 2,
                  "last_update": "2026-10-04T12:00:00Z"
                },
                {
                  "name": "Draw",
                  "price": 2,
                  "last_update": "2026-10-04T12:00:00Z"
                },
                {
                  "name": "Home",
                  "price": 2,
                  "last_update": "2026-10-04T12:00:00Z"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

GET /events/{event_id}

Bieżący odczyt jednego wydarzenia lub jego aliasu; 410 po rozpoczęciu.

ParametrMiejsceZnaczenie i granice
event_idwymaganypathid lub canonical_id z /odds; obsługiwane są też zachowane aliasy. Zakoduj jako segment URL.

HTTP: 200 · 401 · 404 · 410 · 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.

curl --get 'https://api.arbiscan.pl/v1/events/event-00000' \
  --header "X-API-Key: $ARBISCAN_API_KEY"
{
  "availability": "available",
  "away_team": "Away",
  "bookmakers": [
    {
      "bookmaker_event_id": "fixture-native-001",
      "key": "Betclic",
      "last_update": "2026-10-04T12:00:00Z",
      "markets": [
        {
          "key": "Total Corners|9.5",
          "last_update": "2026-10-04T12:00:00Z",
          "outcomes": [
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Over",
              "price": 2
            },
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Under",
              "price": 2
            }
          ]
        },
        {
          "key": "Total|2.5",
          "last_update": "2026-10-04T12:00:00Z",
          "outcomes": [
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Over",
              "price": 2
            },
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Under",
              "price": 2
            }
          ]
        },
        {
          "key": "h2h",
          "last_update": "2026-10-04T12:00:00Z",
          "outcomes": [
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Away",
              "price": 2
            },
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Draw",
              "price": 2
            },
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Home",
              "price": 2
            }
          ]
        }
      ]
    },
    {
      "bookmaker_event_id": null,
      "key": "Excluded",
      "last_update": "2026-10-04T12:00:00Z",
      "markets": [
        {
          "key": "Total Corners|9.5",
          "last_update": "2026-10-04T12:00:00Z",
          "outcomes": [
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Over",
              "price": 2
            },
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Under",
              "price": 2
            }
          ]
        },
        {
          "key": "Total|2.5",
          "last_update": "2026-10-04T12:00:00Z",
          "outcomes": [
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Over",
              "price": 2
            },
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Under",
              "price": 2
            }
          ]
        },
        {
          "key": "h2h",
          "last_update": "2026-10-04T12:00:00Z",
          "outcomes": [
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Away",
              "price": 2
            },
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Draw",
              "price": 2
            },
            {
              "last_update": "2026-10-04T12:00:00Z",
              "name": "Home",
              "price": 2
            }
          ]
        }
      ]
    }
  ],
  "canonical_id": "event-00000",
  "commence_time": "2026-10-04T13:00:00Z",
  "discipline": "unknown",
  "event_scope": "fixture",
  "featured": false,
  "featured_rank": null,
  "home_team": "Home",
  "id": "event-00000",
  "last_seen": "2026-10-04T12:00:00Z",
  "league": "League",
  "portfolio": {
    "outcome_limit": null,
    "outcomes_after": null,
    "outcomes_before": null,
    "reason": null,
    "truncated": false
  },
  "requested_id": "event-00000",
  "source_leagues": [],
  "sport": "Football"
}

GET /events/{event_id}/history

Historia jest niedostępna: 403 bez uprawnienia, 410 po sprawdzeniu uprawnienia. Poniższe parametry dotyczą kontraktu po ponownym włączeniu.

ParametrMiejsceZnaczenie i granice
event_idwymaganypathid lub canonical_id z /odds; obsługiwane są też zachowane aliasy. Zakoduj jako segment URL.
bookmakerqueryNazwa z /bookmakers. W /markets bez rozróżniania wielkości liter, w historii dokładne dopasowanie. Do 256 znaków.
marketqueryDokładny klucz rynku z /markets. Do 512 znaków. Domyślnie: h2h.
sincequeryPoczątek okresu historii, włącznie, ISO-8601; musi być ≤ until.
untilqueryKoniec okresu historii, włącznie, ISO-8601.
limitqueryLiczba elementów na stronie, liczba całkowita. 1–5000. Domyślnie: 1000.
cursorqueryNieprzezroczysta 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.

{
  "bookmaker": "Betclic",
  "count": 1,
  "event_id": "event-00000",
  "has_more": false,
  "history": [
    {
      "bookmaker": "Betclic",
      "outcome": "Home",
      "price": 2,
      "timestamp": "2026-10-04T12:00:00Z"
    }
  ],
  "market": "h2h",
  "next_cursor": null,
  "observed_from": "2026-10-04T12:00:00Z",
  "observed_until": "2026-10-04T12:00:00Z",
  "retention_hours": 48
}
Pełny przykład żądania i odpowiedzi · 410

Syntetyczne dane z testu backendu. Czas, limity i kursor służą wyłącznie ilustracji; kursor nie działa w usłudze.

curl --get 'https://api.arbiscan.pl/v1/events/event-00000/history' \
  --header "X-API-Key: $ARBISCAN_API_KEY"
{
  "disabled": true,
  "error": "Historia kursów jest tymczasowo wyłączona.",
  "code": "gone"
}

GET /webhooks

Lista webhooków; API Data Plus.

HTTP: 200 · 401 · 403 · 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.

curl --get 'https://api.arbiscan.pl/v1/webhooks' \
  --header "X-API-Key: $ARBISCAN_API_KEY"
{
  "limits": {
    "delivery_attempts": 4,
    "destinations": 3,
    "timeout_seconds": 5
  },
  "webhooks": [
    {
      "active": true,
      "created_at": "2026-10-04T12:00:00Z",
      "events": [
        "snapshot.updated"
      ],
      "failure_count": 0,
      "id": "wh_5fee49743a954d1ab87afe643ac83bc6",
      "updated_at": "2026-10-04T12:00:00Z",
      "url": "https://example.com/arbiscan"
    }
  ]
}

POST /webhooks

Utworzenie webhooka; API Data Plus.

Body: application/json. Tylko pola url i events.

url · wymagany
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.
{
  "url": "https://example.com/arbiscan-webhook",
  "events": [
    "snapshot.updated"
  ]
}

HTTP: 201 · 400 · 401 · 403 · 409 · 429

Pełny przykład żądania i odpowiedzi · 201

Syntetyczne dane z testu backendu. Czas, limity i kursor służą wyłącznie ilustracji; kursor nie działa w usłudze.

curl --request POST 'https://api.arbiscan.pl/v1/webhooks' \
  --header "X-API-Key: $ARBISCAN_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{"url":"https://example.com/arbiscan","events":["snapshot.updated"]}'
{
  "signing_secret": "e57497994e01ed5c3c50071393e68416110c4223dc18e57b8220663db7aac1a7",
  "signing_secret_notice": "Shown once. Store it securely.",
  "webhook": {
    "active": true,
    "created_at": "2026-10-04T12:00:00Z",
    "events": [
      "snapshot.updated"
    ],
    "id": "wh_5fee49743a954d1ab87afe643ac83bc6",
    "url": "https://example.com/arbiscan"
  }
}

DELETE /webhooks/{webhook_id}

Wyłącza webhook; API Data Plus.

ParametrMiejsceZnaczenie i granice
webhook_idwymaganypathIdentyfikator zwrócony przy utworzeniu webhooka.

HTTP: 204 · 401 · 403 · 404 · 429

Pełny przykład żądania i odpowiedzi · 204

Syntetyczne dane z testu backendu. Czas, limity i kursor służą wyłącznie ilustracji; kursor nie działa w usłudze.

curl --request DELETE 'https://api.arbiscan.pl/v1/webhooks/wh_5fee49743a954d1ab87afe643ac83bc6' \
  --header "X-API-Key: $ARBISCAN_API_KEY"
204 No Content (bez body)

POST /webhooks/{webhook_id}/test

Kolejkuje testową dostawę; API Data Plus.

ParametrMiejsceZnaczenie i granice
webhook_idwymaganypathIdentyfikator zwrócony przy utworzeniu webhooka.

HTTP: 202 · 401 · 403 · 404 · 429

Pełny przykład żądania i odpowiedzi · 202

Syntetyczne dane z testu backendu. Czas, limity i kursor służą wyłącznie ilustracji; kursor nie działa w usłudze.

curl --request POST 'https://api.arbiscan.pl/v1/webhooks/wh_5fee49743a954d1ab87afe643ac83bc6/test' \
  --header "X-API-Key: $ARBISCAN_API_KEY"
{
  "event_id": "test:5df6f446b70c472d91ef5503ee7db388",
  "queued": true
}

GET /usage

Wspólne zużycie konta dla zarządzanych kluczy; odczyt bez opłaty, 30/min.

HTTP: 200 · 401 · 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.

curl --get 'https://api.arbiscan.pl/v1/usage' \
  --header "X-API-Key: $ARBISCAN_API_KEY"
{
  "expires_at": null,
  "history": [
    {
      "date": "2026-10-04",
      "requests": 9
    }
  ],
  "owner": "",
  "today": {
    "limit": 10000,
    "used": 9
  },
  "total": {
    "limit": null,
    "remaining": null,
    "used": 9
  }
}

GET /events/{event_id}/ladders

Drabinki linii piłkarskich: rożne, kartki i punkty. Bez potwierdzenia zasad rozliczenia i kompletności źródła.

ParametrMiejsceZnaczenie i granice
event_idwymaganypathid lub canonical_id z /odds; obsługiwane są też zachowane aliasy. Zakoduj jako segment URL.
bookmakersqueryNazwy 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.

curl --get 'https://api.arbiscan.pl/v1/events/event-00000/ladders' \
  --header "X-API-Key: $ARBISCAN_API_KEY" \
  --data-urlencode 'bookmakers=Betclic'
{
  "availability": "available",
  "away_team": "Away",
  "bookmakers": [
    {
      "availability": "available",
      "bookmaker": "Betclic",
      "bookmaker_event_id": "fixture-native-001",
      "excluded_invalid_markets": 0,
      "ladders": [
        {
          "counting_system": {
            "basis": "market_family",
            "unit": "corners"
          },
          "family": "Total Corners",
          "line_count": 1,
          "lines": [
            {
              "line": "9.5",
              "market_key": "Total Corners|9.5",
              "over": {
                "observed_at": "2026-10-04T12:00:00Z",
                "price": 2
              },
              "pair_complete": true,
              "under": {
                "observed_at": "2026-10-04T12:00:00Z",
                "price": 2
              }
            }
          ],
          "metric": "corners",
          "participant": "match",
          "participant_name": null,
          "period": "regulation",
          "rules_verified": false,
          "settlement_rules": "operator_specific"
        }
      ],
      "source_ladder_completeness": "not_verified"
    }
  ],
  "commence_time": "2026-10-04T13:00:00Z",
  "event_id": "event-00000",
  "home_team": "Home",
  "league": "League",
  "portfolio": {
    "outcome_limit": null,
    "outcomes_after": null,
    "outcomes_before": null,
    "reason": null,
    "truncated": false
  },
  "price_format": "decimal_source",
  "requested_id": "event-00000",
  "schema_version": "1",
  "sport": "Football",
  "tax_applied": false
}

Zmiany kontraktu

  • 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.