sportapi-python
Official Python client for the SportAPI Sport Line API — Prematch and Live sports odds, scores and live statistics, with typed models and a demo mode that works without an API key.
from sportapi import SportAPI
api = SportAPI() # no key yet? demo data
match = api.event(746146992, "live")
print(match.name, match.score_full) # Arsenal — Coventry City 3:0
print(match.outcome("Total", "Over 3.5").odds) # 1.23
The library is free and MIT-licensed. The SportAPI data service it talks to is a commercial product: live data needs a personal API key (how to get one).
Features
- Every documented Sport Line endpoint:
menu,events, the match calendar,event,search, plus the optionalsports,countries,tournaments,topmatches,toplist,topchampionshipsandaccount. - Typed models (
dataclasses,py.typed) for sports, countries, tournaments, matches, markets, outcomes, live statistics, sub-events and account keys. The original JSON is kept in.raw. - Demo mode: without credentials the client serves the full example responses published in the SportAPI documentation, so you can build and test before you have a key.
- Documented errors as exceptions:
InvalidKeyError,KeyExpiredError,LanguageNotAvailableError,GameFinishedError… mapped fromerror_code/error_messageand fromeventservice messages, not just from HTTP status codes. - Odds helpers: find markets and outcomes by name or
group_id, decimal odds, blocked selections,oc_pointerlookups, the API's column layout preserved. - Polite polling:
watch_events()/watch_event()andpoll()follow the documented minimum intervals and back off 5 → 10 → 20 → 40 s on temporary failures only. - Sync and async clients (
SportAPI,AsyncSportAPI) on top of httpx. Python 3.9+.
Install
pip install sportapi
Quick start (60 seconds, no key needed)
With no SPORTAPI_KEY / SPORTAPI_BASE_URL set, the client runs in demo mode and emits one
SportAPIDemoWarning, so demo data is never mistaken for live data.
from sportapi import LIVE, SportAPI
with SportAPI() as api:
# 1. Navigation: sports -> countries -> tournaments that have matches right now
for sport in api.menu(LIVE)[:3]:
print(sport.name, sport.counter)
# 2. Live football matches, grouped by tournament, with a short list of main markets
for group in api.events(1, LIVE):
for match in group.matches:
print(f"{match.timer // 60:>3}' {match.score_full} {match.name}")
# 3. One match with every market, live statistics and sub-events
match = api.event(746146992, LIVE)
total = match.market("Total")
for over, under in zip(*total.column_outcomes): # columns as arranged by the API
print(over.name, over.odds, "|", under.name, under.odds)
print(match.stat(29)) # Stat(id=29, name='Possession %', ...)
Demo mode answers exactly the requests that have a published example response (all in English):
| Call | Example data |
|---|---|
menu("live"), menu("line") |
full menu snapshots |
sports(...), countries(1, ...), tournaments(1, 1, ...) |
navigation snapshots |
events(1, "live"), events(1, "line") |
Live football (39 matches), Prematch "Top" (50 matches) |
event(746146992, "live"), event(730321837, "line") |
Arsenal — Coventry City (Live), Manchester City — Bournemouth (Prematch) |
search("Perth", "live"), search("Manchester", "line") |
search results |
topmatches(...), toplist(1) with or without full=True |
top selections |
odds=False is emulated on top of these files. Any other call raises
DemoDataUnavailableError with the list of available calls and how to switch to live data.
Live data
You get a personal base URL and an API key from the SportAPI manager. Set them as environment variables (or pass them to the constructor):
export SPORTAPI_BASE_URL="https://YOUR_API_DOMAIN"
export SPORTAPI_KEY="your-api-key"
from sportapi import SportAPI
api = SportAPI() # reads SPORTAPI_KEY and SPORTAPI_BASE_URL
# or: SportAPI(api_key="...", base_url="https://YOUR_API_DOMAIN", lang="en")
print(api.account().current_key.days_left)
print(api.key_expires) # from the X-Key-Expires header of the last response
- The key is sent only in the
PackageHTTP header — never in the URL — and is hidden fromrepr()and exception messages. Keep it in environment variables or a secrets manager, and call the API from your backend, not from a browser. - Setting only one of the two variables raises
ConfigurationError;SportAPI(demo=False)refuses to fall back to demo data. langis any language enabled for your key. The API supports 69 languages with SportAPI-specific codes (for exampleua,cn,br); see Languages.
Step-by-step setup: Authentication and access.
API coverage
| Client method | Endpoint | Documentation |
|---|---|---|
menu(line_type, cybersport=) |
GET /v1/menu/{type}/{lang} |
menu |
events(sport_id, line_type, tournament_id=0, full_line=, odds=, cybersport=) |
GET /v1/events/{sportId}/{tournamentId}/sub/50/{type}/{lang} |
events |
events_by_period(sport_id, hours=, days=, tournament_id=, odds=) |
GET /v1/events/{sportId}/{tournamentId}/sub/50/line/{hours}/{days}/{lang} |
match calendar |
event(game_id, line_type, odds=) |
GET /v1/event/{gameId}/group/{type}/{lang} |
event |
search(text, line_type) |
GET /v1/search/{type}/{lang}/{text} |
search |
sports(line_type, cybersport=) |
GET /v1/sports/{type}/{lang} |
sports |
countries(sport_id, line_type) |
GET /v1/countries/{sportId}/{type}/{lang} |
countries |
tournaments(sport_id, country_id, line_type, cybersport=) |
GET /v1/tournaments/{sportId}/{countryId}/{type}/{lang} |
tournaments |
topmatches(line_type, full=, odds=) |
GET /v1/topmatches/{type}/{lang} |
topmatches |
toplist(sport_id, full=, odds=) |
GET /v1/toplist/{sportId}/{lang} |
toplist |
topchampionships(line_type) |
GET /v1/topchampionships/{type}/{lang} |
topchampionships |
account() |
GET /v1/account |
account |
line_type is "live" or "line" (Prematch); the constants LIVE, LINE and PREMATCH are
exported. lang defaults to the client's lang ("en"). AsyncSportAPI has the same methods
as coroutines.
Helpers: iter_live_matches(), watch_events(), watch_event(), iter_matches(),
poll() / apoll(), recommended_interval(), and icon URLs in sportapi.media
(media assets).
Working with odds
match = api.event(730321837, "line")
w1 = match.outcome("1X2", "W1") # case-insensitive market and outcome names
w1.odds, w1.odds_decimal # 1.525, Decimal('1.525')
w1.blocked # oc_block: True means unavailable
w1.pointer # oc_pointer: '730321837|1|1|0'
match.market(id=17) # by group_id (names are localised)
match.find_markets("Fouls") # several markets can share a name
match.outcome_by_pointer(w1.pointer) # track a selection across refreshes
[o.size_value for o in match.market("Total").outcomes] # oc_size as float (str or int in JSON)
eventsreturns a short market list per match (Market.column_outcomes is None);eventreturns all markets with columns already arranged and sorted by the API.- Market sets differ by sport: don't assume
1X2or a draw exists (tennis has none). oc_pointeridentifies a selection for bet placement with the SportAPI Coupon API; see Bet pointer.
Data models: match · odds · live statistics · sub-events · field reference.
Polling and update intervals
The API is REST: each response is a snapshot. The documented minimum intervals are built in:
| Data | Live | Prematch |
|---|---|---|
menu |
20 s | 60 s |
sports, countries, tournaments |
60 s | 120 s |
events |
7 s | 30 s |
event |
5 s | 30 s |
topmatches |
30 s | 120 s |
toplist |
— | 120 s |
topchampionships |
60 s | 300 s |
| match calendar | — | 60 s |
from sportapi import LIVE, GameFinishedError, SportAPI
api = SportAPI()
try:
for match in api.watch_event(746146992, LIVE): # every 5 s, never overlapping
print(match.score_full, match.timer // 60)
except GameFinishedError:
print("No longer in the line under this game_id: refresh the match list")
watch_* refuses intervals below the documented minimum. Temporary failures (network, HTTP 5xx)
back off 5 → 10 → 20 → 40 s; key, language, sport and parameter errors are raised, not retried.
Update a displayed match clock locally from timer instead of polling every second.
Details: Data update guidelines.
Errors
SportAPIError
├── ConfigurationError, DemoDataUnavailableError
├── TransportError network error or timeout (retryable)
├── UnexpectedResponseError
│ └── HTTPStatusError HTTP 4xx/5xx without error_code (5xx retryable)
├── APIError error_code + error_message
│ ├── AuthenticationError MissingKeyError, InvalidKeyError, KeyExpiredError,
│ │ KeyBlockedError, KeyNotActiveError, AccessRestrictedError
│ ├── PermissionDeniedError AccessDeniedError, LanguageNotAvailableError
│ └── InvalidRequestError InvalidLanguageError, InvalidLineTypeError
└── EventUnavailableError GameNotFoundError, GameFinishedError
An empty list is not an error: nothing is available for that request right now. See Error handling.
Examples
All examples run in demo mode out of the box:
| Script | What it shows |
|---|---|
examples/live_board.py |
Live scoreboard with clock and main odds; --watch refreshes at the documented interval |
examples/match_odds.py |
Every market of one match in columns, blocked selections, live stats, sub-events |
examples/search_matches.py |
Search by team name, then open the first match's main markets |
examples/menu_tree.py |
Sports → countries → tournaments with counters and icon URLs |
python examples/live_board.py
python examples/match_odds.py 730321837 --line
Documentation
- Quick start · Core concepts · Integration guide
- API methods overview ·
Full JSON responses ·
Sports and
sport_idvalues - SportAPI documentation — Sport Line API, Coupon API and more
Get an API key
Live data requires a personal base URL and API key. Plans start from $30/month, and there is a free 2-day trial.
- Request access from the SportAPI manager on Telegram: @sportapinet_bot
- Product page: Sport Line API on sportapi.net
- Related: bet placement and settlement, results
Contributing
Issues and pull requests are welcome — see CONTRIBUTING.md. Please never include an API key in an issue, log or test fixture.
License
MIT © 2026 SportAPI
Metadata
Release files for sportapi 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sportapi-0.1.0.tar.gz | 142.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sportapi-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 296.6 kB
Release files / sportapi-0.1.0.tar.gz
| Download URL | sportapi-0.1.0.tar.gz |
|---|---|
| Size | 142.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9027475dfb1b44ca87aea4468469d78e19736434611d967db5d0fd9210a80a98
|
|
BLAKE2b-256 checksum How to use checksums |
0c5383bf4e4628e02d23c65b268690522e0e0d669ce730604dc21659be33ebf2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / sportapi-0.1.0-py3-none-any.whl
| Download URL | sportapi-0.1.0-py3-none-any.whl |
|---|---|
| Size | 154.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7e7831daac548936ee10549d58f66e50461f85628ae651969e6821692b100a99
|
|
BLAKE2b-256 checksum How to use checksums |
193a385ba5610d5beeb9765a418835016ac49b547853f5aabf924672357fa854
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log