Repository navigation
Releases
v3.1.2
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 doctorsays 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 (orssc 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 --livenames the case (live_browser_tls).
Fixed
- Most requests were refused with
403 challengeand 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 FINGERPRINTfor servers without the
web app. The command never asks:addneeds the fingerprint or--yes.- The certificates live in the
cafolder of the config folder (/app/config/ca/in the image),
and every*.pemfile put there by hand is trusted as well. They are added to the CAs the app
trusts already (the system store, or the file ofSSL_CERT_FILE,CURL_CA_BUNDLEor
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_certificateis 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 codecertificate_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 --livenames these ways first for a certificate failure.
- Health: when the connection check fails on the certificate, the page shows the certificate
- The connection check (Health,
POST /api/v1/status/check) says what kind of network failure it
met when SofaScore could not be reached:causeistimeout,dns,proxy,tls,connector
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 withSSL_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 --livealso 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
scrapling0.5.1,patchright1.64.4 andplaywright1.65.0,
after the challenge solve was verified live with them against the previous versions.
v3.1.1
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 themcurl-cffi0.17.0,fastapi0.162.4 andpydantic2.14.3. The browser
stack (scrapling,patchright,playwright) stays at the versions the challenge solve was
verified with.
Fixed
POST /api/v1/followswithoutseasonsnow takesdefaults.seasons(currentunless changed), as
ssc follows addand a[[follow]]of the config file do. It tookallbefore: 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:
certifi2026.10.23 and
opentelemetry-api1.45.2 (constraints.txt), and in the web appvue-i18n(two
escapeParameterXSS advisories) andvite(build tooling), through the lockfile.
v3.1.0
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 asDATA_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_DIRleaves the app on the default data folder.SOFASCORE_API_TOKEN,
SOFASCORE_ALLOWED_HOSTS,USE_PROXYandPROXY_URLstill work in 3.1 with a warning and are
removed in 3.2. Rename each toSOFASCORE_<SECTION>__<KEY>(SOFASCORE_STORAGE__DATA_DIR,
SOFASCORE_CLIENT__RATE,SOFASCORE_SERVER__TOKEN, …).ssc doctorandssc config showlist
every old name still set, with its new name and where it is set;ssc config init --from-legacy > sofascore.tomlwrites the old.envandconfig/leagues.txtas 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.pyflags, the 2.x/api/...routes and the backup scopesconfig,seasons,
matchesandmatch_detailsare removed (see Removed). An old flag is a usage error (exit 2)
that names thessccommand replacing it; a 2.x route answers 404; backups made with an old
scope still restore. ssc exportnames its files differently when--outis not given: like a web export
(exports/premier-league_2026-10-09_142530.jsonl), and the wide CSV in
match_details/processed/isevents-wide_<date>_<time>.csv, no longer
all_matches_<epoch>.csv. Scripts that look forall_matches_*.csvmust 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_idsandplayer_idsin 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/statushassummary.follows[](stored, finished and
detailed events and the coverage of each follow, a player follow's too), and
GET /api/v1/events?follow=kind:idlists 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.setsof 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 inscore_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/providerslists the known bookmakers from a built-in table (id,name,
country,configured; no request to SofaScore, no betting link stored), and the Settings row
ofclient.odds_provideroffers them next to the free entry of an id (#192). connection.last_check.supersededin/api/v1/status: an answer came after a failed
connection check (#191).replaced_byon each setting ofGET /api/v1/settingsand each row ofssc 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 showreads[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; runlv-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). - baseball no longer requests incidents, and badminton and table tennis no longer request
-
.envlines are part of the environment layer: they win oversofascore.tomland pin the
setting on the Settings page, which writesconfig/overrides.jsononly. An invalid value in the
environment stops the app with a configuration error instead of being ignored (#186). -
STORE_OPEN_RECONCILE_SECONDSis the settingstorage.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.dbfrom the files (#186, #189). -
Slice summaries, the
not_requestedslices 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; itswebalias is gone
(#186). -
The installers and start scripts choose their language from
SOFASCORE_DISPLAY__LANGUAGE(no
longerAPP_LANGUAGEorLANGUAGE) and printpython -m sofascore_scraper.cli.main doctoras
the check command (#188). -
ssc doctorsays whether a 2.x name is set in the environment or in.env, and points at
.envonly for what is there (#188). -
The app no longer creates the empty 2.x folders
match_details/anddatasets/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_idsinstead 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;0asks 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 exportwithout--outnames 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...
v3.0.0
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
inconfig/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 (ScraplingStealthySession) 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=1shows the window,
SOFASCORE_BROWSER_PROFILEmoves 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, orAPP_LANGUAGEwhen 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_statusseparates played, decided-without-play and void
matches;extract_scoresreads scores per sport without the ambiguous fields; each saved
match records the time it was read and SofaScore's change timestamp (inobservation.json,
since #104 in the match'smanifest.json).docs/settlement-notes.mddescribes 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 #104DATA_DIR/changes/<yyyy>-<mm>.jsonl).
main.py --refresh-onlyruns refreshes alone and--refresh-legacyalso covers records saved
beforeobservation.jsonexisted (#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), andDATA_DIR/watch_events.jsonlis no longer written. - Shared request budget.
REQUEST_RATE_LIMITis 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 and0oroffremoves 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_DIRmoves it (#19, #33). - Bridge health. The browser bridge reports whether SofaScore is answering:
ok,
degradedafterBRIDGE_DEGRADED_AFTER(default 3) failed requests in a row,blocked
afterBRIDGE_BLOCKED_AFTER(10) that span at leastBRIDGE_BLOCKED_MIN_SECONDS(200).
It is inGET /health(bridge, next to a newthrottleblock) 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.tomlis read at start if there is one. It is
looked for inSOFASCORE_CONFIG, then./sofascore.toml, thenconfig/sofascore.toml. Its
values win over.env; variables set in the process environment win over the file, and every
setting can be set asSOFASCORE_<SECTION>__<KEY>(for exampleSOFASCORE_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:.envand 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 andssc 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 throughPATCH /api/v1/settingsand 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 thetomlipackage torequirements.txt. - Setup check.
python main.py --doctorchecks 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,--langand--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 atLOG_MAX_MB(5 MB) withLOG_BACKUP_COUNT(5) older files;
LOG_TO_FILE=falseturns it off. The web app, the CLI and--watchshare 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/diagnosticsreturns
the summary as JSON (#24). GET /api/logs: the most recent log entries, withlimitand a minimumlevel(#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-idlimits it to one league (#25)._slice_status.jsonin 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'smanifest.json(#25).GET /api/sportslists the supported sports and, for each, the match-detail slices
requested for it (#18).main.py --weboptions--host,--portand--dev.- Optional access token.
SOFASCORE_API_TOKEN(off by default) protects the web app and
its API. Once it is set, every/apirequest needsAuthorization: Bearer <token>or the
web app's session cookie; the web app asks for the token once (POST /api/auth/login, an
HttpOnly,SameSite=Strictcookie) and Settings → General → Sign out ends the
session. The live status stream works with the cookie, andGET /healthanswers 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 anyHostheader instead of requiring
SOFASCORE_ALLOWED_HOSTSfor--host 0.0.0.0. Insecure; see "Security model" in the
README (#43).POST /api/export/csv; since #99 it gives the same answer asGET /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 byGET /healthand shown on the
Settings page.pyproject.tomlis 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 av*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 toconfig/leagues.txton the first run.SOFASCORE_CONFIG_DIRandSOFASCORE_ENV_FILEto move the config folder and the.env
file.- Continuous integration (GitHub Actions): frontend lint, tests and build;
ruffand
pyteston Linux with Python 3.10 and 3.14, with a coverage floor (85% since #83); the
BrowserB...