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.23The 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).
- 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+.
pip install sportapiWith 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.
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.
| 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).
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.
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.
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.
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- 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
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
Issues and pull requests are welcome — see CONTRIBUTING.md. Please never include an API key in an issue, log or test fixture.
MIT © 2026 SportAPI