Skip to content

Releases

v3.1.2

Choose a tag to compare

@github-actions github-actions released this 14 Nov 13:11
fb0a257

Downloads are fast again, and the app works on networks that inspect HTTPS. Most requests had been
refused by SofaScore and then made through the built-in browser; the request layer now presents
clients that pass. A network that re-signs HTTPS (an antivirus that scans HTTPS, a company proxy)
can be trusted with one click on the Health page, and the built-in browser now verifies certificates
like the HTTP client does. The Docker image is ghcr.io/tunjayoff/sofascore_scraper:3.1.2.

Upgrading from 3.1.1:

  • From source: the browser stack was raised, so install its browser build once more after
    pip install: python -m patchright install chromium --no-shell (ssc doctor says so when it is
    missing).
    The Docker image has it.
  • On a network that re-signs HTTPS: the built-in browser no longer accepts the re-signed
    certificate by itself. Trust the network's certificate on the Health page (or ssc trust add);
    see "Security" below.

Security

  • The built-in browser no longer accepts HTTPS certificates it cannot verify. Until now it accepted any
    certificate (a default of the browser library), so someone able to intercept the connection could read
    and change what the browser exchanged: page loads, the solved challenge and the API answers the app
    stores. The HTTP client always verified. On a network that re-signs HTTPS with its own CA the browser
    now fails with a certificate error; client.browser_accept_any_certificate
    (SOFASCORE_CLIENT__BROWSER_ACCEPT_ANY_CERTIFICATE=true) restores the old behaviour for such a
    network, knowingly (docs/deploy/docker.md).
    ssc doctor --live names the case (live_browser_tls).

Fixed

  • Most requests were refused with 403 challenge and then made through the built-in browser, which is
    slow and heavy: SofaScore refuses clients that present an outdated Chrome version (and a few
    client fingerprints whatever they present), and six of the nine browser profiles the request layer
    chose from were such. The list now holds profiles that pass, and it corrects itself: a profile
    that is challenged is set aside for six hours and the request is repeated at once with another
    one; the browser is used only when no profile passes. In a live download of 39 requests none was
    refused and the browser was not started.

Added

  • Trusting the certificate of a network that re-signs HTTPS (an antivirus that scans HTTPS, a
    company proxy), without an environment variable, a Compose change or a restart (#225):
    • Health: when the connection check fails on the certificate, the page shows the certificate
      the network presents (subject, issuer, SHA-256 fingerprint, expiry) and the button Trust this
      certificate
      . The confirmation states the risk (whoever holds that certificate's key can read
      and change what the app downloads) and wants a word typed. The connection check runs again
      afterwards. The trusted certificates are listed there and can be removed.
    • ssc trust show | add --sha256 FINGERPRINT | list | remove FINGERPRINT for servers without the
      web app. The command never asks: add needs the fingerprint or --yes.
    • The certificates live in the ca folder of the config folder (/app/config/ca/ in the image),
      and every *.pem file put there by hand is trusted as well. They are added to the CAs the app
      trusts already (the system store, or the file of SSL_CERT_FILE, CURL_CA_BUNDLE or
      REQUESTS_CA_BUNDLE). The HTTP client reads the folder with every request; the built-in browser
      reads it at its start (Linux) and verifies against those certificates, so
      client.browser_accept_any_certificate is no longer needed on such a network.
    • API: GET /api/v1/trust, POST /api/v1/trust/probe, POST /api/v1/trust
      ({"sha256": …}: the server looks at the network itself and stores the certificate only if its
      fingerprint is the one given; otherwise 409 with the new error code certificate_changed) and
      DELETE /api/v1/trust/{fingerprint}.
    • The button is offered only when it can work: not when the network sends no CA certificate
      (the page then names the issuer whose certificate to export), and not with a proxy configured.
    • ssc doctor --live names these ways first for a certificate failure.
  • The connection check (Health, POST /api/v1/status/check) says what kind of network failure it
    met when SofaScore could not be reached: cause is timeout, dns, proxy, tls, connect or
    reset, and the Health page shows it with a hint (#225).
  • Docker: how to run the image on a network that inspects HTTPS (an antivirus that scans HTTPS, a
    company proxy): give the container the CA certificate with SSL_CERT_FILE, or build it into an
    image of your own (docs/deploy/docker.md,
    FAQ in the README). The Compose example carries the two lines as comments (#225).

Changed

  • ssc doctor --live also sends one request the way downloads do, with the app's HTTP client, after
    the one through the browser. Until now it reported success on a network that re-signs HTTPS, where
    the browser gets through and every download fails with a certificate error; it now fails there
    (live_http_tls) and names the fix (#225).
  • Health: the hint for a connection check that failed on TLS names the CA certificate and
    SSL_CERT_FILE (#225).
  • The browser stack is raised to scrapling 0.5.1, patchright 1.64.4 and playwright 1.65.0,
    after the challenge solve was verified live with them against the previous versions.

v3.1.1

Choose a tag to compare

@github-actions github-actions released this 14 Nov 08:03
3a8e063

A maintenance release: dependency versions with published advisories are raised, a default of the
follows API is corrected, and four sports no longer count data SofaScore does not have for them as
missing. Nothing has to be changed after upgrading from 3.1.0; the Docker image is
ghcr.io/tunjayoff/sofascore_scraper:3.1.1.

Changed

  • Rugby, volleyball, floorball and minifootball: the data types SofaScore leaves out for part or all
    of their finished matches no longer make a match incomplete. Rugby and volleyball: statistics,
    line-ups, incidents and pre-game form are optional. Floorball: line-ups are not requested,
    statistics and pre-game form are optional. Minifootball: statistics, incidents and pre-game form
    are optional.
  • The pinned dependency versions (constraints.txt, the web app's lockfile) are raised to the
    current ones, among them curl-cffi 0.17.0, fastapi 0.162.4 and pydantic 2.14.3. The browser
    stack (scrapling, patchright, playwright) stays at the versions the challenge solve was
    verified with.

Fixed

  • POST /api/v1/follows without seasons now takes defaults.seasons (current unless changed), as
    ssc follows add and a [[follow]] of the config file do. It took all before: a team or a player
    followed that way had its match list read back to the page limit (about 150 matches and 1,600
    requests for a national team). The web app always sends its own choice and is not affected; pass
    "seasons": "all" to keep the old behaviour.

Security

  • Pinned versions with published advisories were raised: certifi 2026.10.23 and
    opentelemetry-api 1.45.2 (constraints.txt), and in the web app vue-i18n (two
    escapeParameter XSS advisories) and vite (build tooling), through the lockfile.

v3.1.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 09:27
054d8a2

3.1.0 removes what 3.0.0 deprecated (the main.py flags, the 2.x /api routes, the 2.x
environment names but four, which are deprecated until 3.2, and the old backup scopes) and adds what the first use of 3.0.0 asked for:
team records and a team and player filter for exports, counts for every follow, request counters
in the job progress, odds providers by name, single-set darts and e-sports game scores, and slice
rows from new evidence for several sports.

Like 3.0.0 it is installed from source: a checkout of the v3.1.0 tag with pip install -e .,
the release archive (the source with the built web app), or the Docker image
ghcr.io/tunjayoff/sofascore_scraper:3.1.0. It is not published on PyPI.

Upgrading from 3.0 (also in the README, "Upgrading from 3.0"):

  • The 2.x environment names are no longer read, except four deprecated ones (see Deprecated).
    A value set as DATA_DIR, REQUEST_RATE_LIMIT, APP_LANGUAGE, LOG_LEVEL, … (in .env, the
    shell, a service file or the container settings) is ignored, and the default applies: an old
    DATA_DIR leaves the app on the default data folder. SOFASCORE_API_TOKEN,
    SOFASCORE_ALLOWED_HOSTS, USE_PROXY and PROXY_URL still work in 3.1 with a warning and are
    removed in 3.2. Rename each to SOFASCORE_<SECTION>__<KEY> (SOFASCORE_STORAGE__DATA_DIR,
    SOFASCORE_CLIENT__RATE, SOFASCORE_SERVER__TOKEN, …). ssc doctor and ssc config show list
    every old name still set, with its new name and where it is set; ssc config init --from-legacy > sofascore.toml writes the old .env and config/leagues.txt as a config file.
  • The catalog is rebuilt on its first open (catalog schema 2, derive version 8). catalog.db
    is rebuilt from the stored files once, without a request to SofaScore; on a large data folder
    the first start takes longer.
  • Rebuild the web app of a checkout (cd frontend && npm install && npm run build). A build
    made before 3.0.0 does not work under the strict Content-Security-Policy: the 'unsafe-eval'
    policy it needed is gone. The release archive and the Docker image carry a built web app.
  • The main.py flags, the 2.x /api/... routes and the backup scopes config, seasons,
    matches and match_details are removed
    (see Removed). An old flag is a usage error (exit 2)
    that names the ssc command replacing it; a 2.x route answers 404; backups made with an old
    scope still restore.
  • ssc export names its files differently when --out is not given: like a web export
    (exports/premier-league_2026-10-09_142530.jsonl), and the wide CSV in
    match_details/processed/ is events-wide_<date>_<time>.csv, no longer
    all_matches_<epoch>.csv. Scripts that look for all_matches_*.csv must follow, or pass --out.
  • Run pip install -e . again after updating a checkout, as after every update.

Added

  • Team record. GET /api/v1/teams/{team_id} returns a stored team (or, in tennis, darts,
    MMA, …, a player or pair) with its gender, national-team flag, country and sport, and whether it
    is followed; no request to SofaScore. The follow page of a team shows them in its header
    ("Volleyball · Türkiye · Team · Women") instead of telling same-named teams apart only by
    number, and the catalog suggestions carry gender and national team too (#190).
  • The sport registry flag individual (GET /api/v1/sports): the sports whose players SofaScore
    lists as teams (tennis, badminton, table tennis, padel, snooker, darts, MMA). The web UI reads
    it instead of keeping its own list (#190).
  • Exports by team and player. team_ids and player_ids in the export filter (API and
    jobs), ssc export --team ID --player ID, and the added teams and players in the web export
    dialog. A player's matches are those of the player follow's stored match list and those whose
    stored line-ups name the player. Teams and players form one filter (a match of any of them),
    combined with the other filters (#190, #194).
  • Counts for every follow. /api/v1/status has summary.follows[] (stored, finished and
    detailed events and the coverage of each follow, a player follow's too), and
    GET /api/v1/events?follow=kind:id lists a follow's matches. A sync keeps the match ids of a
    player's last match list, so a player follow's page has a Matches tab; before the player's first
    counted download it says that the next download stores the matches of the player's list (#191).
  • Request counters. The job progress and result carry requests (sent,
    budget_wait_seconds, backoff_seconds), and Job detail says how many requests a download sent
    and how long they waited for the request budget (#191).
  • Sync log lines carry the league and season names (league_name, season_name,
    season_year) (#191).
  • The setting fetch.confirm_empty_after_seconds (default 60; see Changed) (#191).
  • E-sports game (map) scores. score.sets of an e-sports match lists each game: the round
    score where SofaScore gives one (finished CS2 series), else 1-0 for the game's winner; games not
    played yet are left out. The match header shows them under the games won, and the normalized
    exports carry them in score_sets (#189).
  • Odds providers by name. The Odds tab, the raw odds list and the Data table name the
    bookmaker ("bet365") instead of "Bookmaker 1"; an id without a known name stays numbered.
    GET /api/v1/odds/providers lists the known bookmakers from a built-in table (id, name,
    country, configured; no request to SofaScore, no betting link stored), and the Settings row
    of client.odds_provider offers them next to the free entry of an id (#192).
  • connection.last_check.superseded in /api/v1/status: an answer came after a failed
    connection check (#191).
  • replaced_by on each setting of GET /api/v1/settings and each row of ssc config show: the
    new name of a deprecated 2.x name the value came from (null otherwise). The Settings page shows
    such a row as locked by the old variable and names the new one; the text output of
    ssc config show reads [env: SOFASCORE_API_TOKEN, deprecated, use SOFASCORE_SERVER__TOKEN], and
    the diagnostics bundle lists the loader's warnings (settings.warnings, names only) (#196).

Changed

  • Slice rows from new evidence. The repaired research explorer recorded the match pages of
    21 sports (finished and not started; run lv-20261009), and the slice registry follows it
    (#183, #185):

    • baseball no longer requests incidents, and badminton and table tennis no longer request
      line-ups: their pages never ask for them;
    • badminton and table tennis request point-by-point, which counts for a finished match's
      completeness (as in tennis);
    • pre-game form is optional in American football, badminton, table tennis, baseball and MMA,
      and incidents are optional in futsal: a "no data" answer no longer keeps a match incomplete;
    • e-sports games count for completeness;
    • statistics and point-by-point are no longer requested before kick-off, in any sport.

    A finished baseball match costs one request fewer per download, badminton and table tennis ask
    for point-by-point instead of line-ups, and a not-started match no longer asks for statistics
    (nor, in tennis, badminton and table tennis, for point-by-point).

  • .env lines are part of the environment layer: they win over sofascore.toml and pin the
    setting on the Settings page, which writes config/overrides.json only. An invalid value in the
    environment stops the app with a configuration error instead of being ignored (#186).

  • STORE_OPEN_RECONCILE_SECONDS is the setting storage.open_reconcile_seconds (#186).

  • The catalog schema is 2 (the two unused current-score columns are dropped) and the derive
    version 8 (darts and e-sports, see Fixed and Added): the first open after the upgrade rebuilds
    catalog.db from the files (#186, #189).

  • Slice summaries, the not_requested slices of an event, the coverage report and the season
    counts follow the configured slice selection and the follows table (#186).

  • The Docker entrypoint runs python -m sofascore_scraper.cli.main; its web alias is gone
    (#186).

  • The installers and start scripts choose their language from SOFASCORE_DISPLAY__LANGUAGE (no
    longer APP_LANGUAGE or LANGUAGE) and print python -m sofascore_scraper.cli.main doctor as
    the check command (#188).

  • ssc doctor says whether a 2.x name is set in the environment or in .env, and points at
    .env only for what is there (#188).

  • The app no longer creates the empty 2.x folders match_details/ and datasets/ in the data
    folder; the Store creates the data folder on its first open (#188).

  • Search hits no longer report SofaScore's placeholder team "No team" as a player's team (team
    is null) (#190).

  • The web export dialog sends chosen teams as team_ids instead of their stored match numbers; a
    chosen team and a chosen single match now narrow each other like every other filter (before,
    the team's matches and the match were added together) (#190).

  • The request that confirms a "no data" answer waits at least
    fetch.confirm_empty_after_seconds (60 s; 0 asks at the next download as before). It is a
    planning rule, not a sleep: nothing is sent while it waits. A job resumed right after a stop no
    longer asks again for the matches it just stored (#191).

  • The ETA of a league download counts its matches' requests by need (full download, refill or
    refresh) (#191).

  • catalog verify, the scans and the circuit breaker's cause report in English; the CLI's text
    output adds a description in the user's language (#191).

  • ssc export without --out names its file like a web export: the league, the single team
    or player, or the dataset, then the UTC date and time
    (exports/premier-league_2026-10-09_142530.jsonl), instead of epoch seconds. The wide CSV in
    match_details/processed/ is `events-wide_<da...

Read more

v3.0.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 20:24
1c5bab2

The first tagged release: everything since the version number was set to 2.0.0 (2026-07-27,
when the Vue web app replaced the server-rendered pages).

3.0.0 is installed from source: a checkout of the v3.0.0 tag with pip install -e . (see
"Quick start" in the README), the release archive (the source with the built web app), or the
Docker image ghcr.io/tunjayoff/sofascore_scraper:3.0.0. It is not published on PyPI. After
updating an existing checkout, run pip install -e . again: the import package is now
sofascore_scraper (see Changed). What 3.0.0 deprecates is listed under Deprecated; 3.1
removes it.

Added

  • Basketball and tennis. League search, fixtures and match details for football, basketball
    and tennis; eighteen more sports followed in #112, #115 and #118. A league's sport is stored
    in config/league_sports.json, and the web app has a sport switch that filters every page.
  • BrowserBridge. SofaScore answers plain HTTP clients with 403 challenge, so API
    requests are now made from inside a headless Chromium (Scrapling StealthySession) that
    passes the Cloudflare Turnstile challenge on its own. It runs headless on a desktop and on
    a display-less server alike. SOFASCORE_BROWSER_HEADED=1 shows the window,
    SOFASCORE_BROWSER_PROFILE moves the browser profile. After a challenge, requests go
    straight to the browser for 10 minutes ("browser-first" mode).
  • Web app rebuilt around Leagues, Download, Matches, Match, Activity and Settings pages,
    with dark mode, English and Turkish text, error toasts and a job card with a Stop button. The
    first visit follows the browser's language, or APP_LANGUAGE when it is set. Since #107 these
    pages were the classic interface under /classic, which #161 removed; the new web UI is
    described under Changed (#39).
  • Job progress with phases (seasons, matches, details; the export phase was dropped in #99),
    job-wide counters, the list of failed matches, an ETA and SofaScore wait countdowns (#10).
  • Status classification. classify_status separates played, decided-without-play and void
    matches; extract_scores reads scores per sport without the ambiguous fields; each saved
    match records the time it was read and SofaScore's change timestamp (in observation.json,
    since #104 in the match's manifest.json). docs/settlement-notes.md describes how to use
    them (#9).
  • Refresh policy. A finished match stays provisional for REFRESH_WINDOW_HOURS (default 72)
    after kick-off and is re-read by later downloads, at most once every
    REFRESH_MIN_INTERVAL_HOURS (default 6). Changes are appended to the change log
    (DATA_DIR/score_changes.jsonl; since #104 DATA_DIR/changes/<yyyy>-<mm>.jsonl).
    main.py --refresh-only runs refreshes alone and --refresh-legacy also covers records saved
    before observation.json existed (#11).
  • Watch mode. main.py --watch --sport … --league-ids …|--event-ids … follows live
    matches and reports status, score and stuck-match events (#12). Since #119 it runs
    ssc watch --source poll --stdout: the events are printed and stored in the event log
    (ssc events), and DATA_DIR/watch_events.jsonl is no longer written.
  • Shared request budget. REQUEST_RATE_LIMIT is one requests-per-second budget for all
    processes on the machine together (web app, CLI, every --watch, --refresh-only),
    whether a request goes through curl or through the browser. The default is 5 requests
    per second; a higher value is faster and 0 or off removes the limit, both at a higher
    risk of being blocked by SofaScore. It is also on the Settings page (Advanced), which
    explains the trade-off and shows a warning while the value is above the default or off,
    and in /api/settings. The state is a small locked file under
    ~/.cache/sofascore_scraper/throttle/; SOFASCORE_THROTTLE_DIR moves it (#19, #33).
  • Bridge health. The browser bridge reports whether SofaScore is answering: ok,
    degraded after BRIDGE_DEGRADED_AFTER (default 3) failed requests in a row, blocked
    after BRIDGE_BLOCKED_AFTER (10) that span at least BRIDGE_BLOCKED_MIN_SECONDS (200).
    It is in GET /health (bridge, next to a new throttle block) and
    GET /api/bypass/status (health), in a dismissible banner in the web app, and in the
    terminal modes as one line on stderr per state change; the log gets one warning per state
    change (#19).
  • Configuration file, first step. A sofascore.toml is read at start if there is one. It is
    looked for in SOFASCORE_CONFIG, then ./sofascore.toml, then config/sofascore.toml. Its
    values win over .env; variables set in the process environment win over the file, and every
    setting can be set as SOFASCORE_<SECTION>__<KEY> (for example SOFASCORE_CLIENT__RATE=2).
    Relative paths in the file are resolved against the file's folder. An unknown key or a value
    of the wrong type stops the start with a message that names the file and the key. Without the
    file nothing changes: .env and the existing variables are read as before. [[follow]] is
    what downloads and the live service follow (#91, #152), [[sink]] and [live] are used by
    the live service and ssc serve (#91, #95, #101, #125), [server] holds the host, the port
    and the access token's variable (#74, #125), [schedule] the in-app scheduler (#128), and
    [defaults] and [slices.*] the choice of what is downloaded (#134, #140). The Settings
    screen of the web UI saves through PATCH /api/v1/settings and shows where each value comes
    from (#74, #107); the classic Settings page, which wrote .env, went with the classic
    interface (#161). On Python 3.10 this adds the tomli package to requirements.txt.
  • Setup check. python main.py --doctor checks the environment without contacting
    SofaScore: Python, packages, the browser the bridge starts, the browser profile, the data
    and config folders, the web app build and .env. Output is text or --json, and the exit
    code is non-zero when a check fails; --strict, --only, --skip, --lang and --live
    refine it. The launcher runs the check on every start and installs missing Python packages
    and the missing browser (#26).
  • A help page (English and Turkish) when the web app is not built, instead of a JSON error
    (#26).
  • Log file. Everything the app logs is also written to logs/sofascore_scraper.log
    (LOG_DIR), rotated at LOG_MAX_MB (5 MB) with LOG_BACKUP_COUNT (5) older files;
    LOG_TO_FILE=false turns it off. The web app, the CLI and --watch share the file. On
    Windows the file is not rotated while more than one process is writing to it (#24).
  • Diagnostics bundle for bug reports: python main.py --diagnostics [PATH] or
    GET /api/diagnostics/bundle (versions, settings with secrets masked, bridge health,
    request budget, setup check results, last jobs, log tail). GET /api/diagnostics returns
    the summary as JSON (#24).
  • GET /api/logs: the most recent log entries, with limit and a minimum level (#24).
  • Settings → Connection. A connection check that sends one request to SofaScore when you
    press Test connection and shows what happened, together with the request health state;
    and the proxy fields (use a proxy, proxy address) (#23).
  • main.py --recheck-unavailable [legacy|all] reopens "slice not available" markers written
    by earlier versions so the next download asks again. It sends no requests itself, and
    --league-id limits it to one league (#25).
  • _slice_status.json in match folders: the last failed request per slice and how many "not
    available" counts were confirmed by a definitive answer; since #104 this is kept in the
    match's manifest.json (#25).
  • GET /api/sports lists the supported sports and, for each, the match-detail slices
    requested for it (#18).
  • main.py --web options --host, --port and --dev.
  • Optional access token. SOFASCORE_API_TOKEN (off by default) protects the web app and
    its API. Once it is set, every /api request needs Authorization: Bearer <token> or the
    web app's session cookie; the web app asks for the token once (POST /api/auth/login, an
    HttpOnly, SameSite=Strict cookie) and Settings → General → Sign out ends the
    session. The live status stream works with the cookie, and GET /health answers only
    {"status": "ok"} to callers without the token. The token never appears in the log, the
    diagnostics bundle or an API response (#43).
  • main.py --web --allow-any-host: answer to any Host header instead of requiring
    SOFASCORE_ALLOWED_HOSTS for --host 0.0.0.0. Insecure; see "Security model" in the
    README (#43).
  • POST /api/export/csv; since #99 it gives the same answer as GET /api/export/csv (the
    export, built at the moment of the request) and is kept for one release (#43).
  • A "Security model" section in both READMEs: what the app does and what you must do before
    opening it to a network (#43).
  • main.py --version; the version is also reported by GET /health and shown on the
    Settings page. pyproject.toml is the single place it is written.
  • Docker image (Dockerfile, docker-compose.yml): the web app together with the
    headless browser it needs, running as a non-root user, with data, configuration, the
    browser profile and the log file in volumes.
  • Release workflow (.github/workflows/release.yml): pushing a v* tag runs the CI
    checks, pushes the Docker image to GHCR and publishes a GitHub Release with an archive of
    the source plus the built web app.
  • This changelog.
  • config/leagues.example.txt, copied to config/leagues.txt on the first run.
  • SOFASCORE_CONFIG_DIR and SOFASCORE_ENV_FILE to move the config folder and the .env
    file.
  • Continuous integration (GitHub Actions): frontend lint, tests and build; ruff and
    pytest on Linux with Python 3.10 and 3.14, with a coverage floor (85% since #83); the
    BrowserB...
Read more