Skip to content

Repository files navigation

goal-api

Python SDK for the GOAL API: football fixtures, live scores, standings, player stats, odds and live WebSocket updates.

Sync and async clients from the same surface. Python 3.9+.

pip install goal-api           # REST
pip install "goal-api[live]"   # + WebSocket live updates

Quick start

import os
from goal_api import GoalAPI

goal = GoalAPI(os.environ["GOAL_API_KEY"])

for match in goal.fixtures.live()["data"]:
    print(match["homeTeam"]["name"], match["homeScore"], "-", match["awayScore"], match["awayTeam"]["name"])

Get a key at goal-api.com/signup.

Use it as a context manager so the connection pool closes cleanly:

with GoalAPI(api_key) as goal:
    table = goal.leagues.standings(league_id)["data"]

Async

Same method names, awaited:

import asyncio
from goal_api import AsyncGoalAPI

async def main():
    async with AsyncGoalAPI(api_key) as goal:
        live, results = await asyncio.gather(
            goal.fixtures.live(),
            goal.results.today(),
        )
        print(len(live["data"]), "in play,", len(results["data"]), "finished today")

asyncio.run(main())

Client options

goal = GoalAPI(
    api_key,
    base_url="https://api.goal-api.com/v1",   # default
    timeout=30.0,                             # per attempt, seconds
    max_retries=2,                            # 429 + 5xx + network errors
    headers={"X-My-App": "scoreboard"},
)

Retries use exponential backoff with full jitter and always honour a server-sent Retry-After.

Endpoints

Grouped by resource. Query params are keyword arguments, passed through with the API's own camelCase names. Full reference in ENDPOINTS.md.

goal.status.get()                                    # no API key needed
goal.countries.list(search="spa")
goal.leagues.list(isActive=True, limit=100)
goal.leagues.standings(league_id)
goal.leagues.top_scorers(league_id, limit=10)
goal.teams.get(team_id, includePlayers=True)
goal.teams.statistics(team_id, season="2025-2026")
goal.fixtures.list(**{"from": "2026-08-01", "to": "2026-08-07", "status": "SCHEDULED"})
goal.fixtures.by_date("2026-08-15", leagueId=league_id)
goal.fixtures.lineups(fixture_id)
goal.fixtures.statistics(fixture_id, half="1half")
goal.standings.form(league_id)
goal.players.search("haaland", limit=5)
goal.players.compare([player_a, player_b])
goal.players.top("goals", limit=20)
goal.coaches.by_team(team_id)
goal.h2h.stats(team_a, team_b)
goal.results.today()
goal.videos.recent(leagueId=league_id, limit=10)
goal.odds.list(bookmaker="bet365")
goal.predictions.list(matchId=match_id)

from is a Python keyword, so date ranges need **{...} or a prepared dict:

window = {"from": "2026-08-01", "to": "2026-08-31"}
goal.leagues.fixtures(league_id, **window)

Every method returns the raw envelope, so pagination and source stay reachable:

page = goal.teams.list(leagueId=league_id, limit=50)
page["data"]                     # list of teams
page["pagination"]["hasMore"]    # bool
page["source"]                   # "cache" | "database"

One exception: goal.status.*

The five /public/* endpoints don't use the {"success", "data"} envelope. They return bare objects, so read them directly with no ["data"]:

status = goal.status.get()
status["status"]        # "operational"
status["components"]    # [{"name", "status", "uptime"}]

They also paginate with page/limit instead of limit/offset, so paginate() does not apply to coverage_leagues.

Pagination

paginate walks pages and yields items:

for team in goal.paginate(lambda **p: goal.leagues.teams(league_id, **p)):
    print(team["name"])

# Or collect, with a cap. /results accepts limit up to 500:
recent = goal.collect(
    lambda **p: goal.results.list(leagueId=league_id, **p),
    page_size=500,
    max_items=500,
)

Async is the same call, iterated with async for:

async for team in goal.paginate(lambda **p: goal.leagues.teams(league_id, **p)):
    ...

Default page_size is 100, the limit ceiling on most endpoints. /results and /countries take 500.

Errors

Everything raised is a GoalAPIError. Branch only where you'd actually behave differently:

from goal_api import GoalAPIError, NotFoundError, RateLimitError, ValidationError

try:
    fixture = goal.fixtures.get(fixture_id)
except NotFoundError:
    fixture = None
except RateLimitError as error:
    print(f"Quota exhausted ({error.rate_limit_type}), retry in {error.retry_after}s")
    raise
except ValidationError as error:
    print(error.details)        # which field the server rejected
    raise
except GoalAPIError as error:
    print(error.code, error.correlation_id)
    raise

Two error shapes

The API answers with one of two bodies, and the SDK normalises both:

Gateway (auth, routing, rate limits) Football service (most endpoints)
text message error
code yes yes
category yes no
correlationId yes no
details object array, on validation errors

So error.message and error.code are always populated, and error.correlation_id is only set on gateway errors. Quote it in a support ticket when you have it.

Rate limits

goal.fixtures.live()
goal.rate_limit.remaining   # int | None
goal.rate_limit.reset       # unix seconds
goal.rate_limit.type        # "DAILY" | "MONTHLY"

Live WebSocket updates

The socket is at wss://api.goal-api.com/ws, not /v1/ws. Only nginx's location ^~ /ws carries the Upgrade headers; /v1/ws is proxied as ordinary HTTP and answers 200 instead of upgrading. The SDK derives the right URL for you.

Two services authenticate: the gateway authorises the upgrade from the header or ?wsToken=, then websocket-service needs an {"type": "auth", ...} frame as the very first message. The SDK sends it, and treats auth_success as the point the connection is usable.

subscribe is capped per plan and the cap can be 0. auth_success reports maxSubscriptions; if it is 0 the socket works but no match_update will ever arrive. See the known server issue in ENDPOINTS.md.

Needs the live extra. WebSockets are async, so this is async even on the sync client.

import asyncio
from goal_api import GoalAPI

async def watch(fixture_id):
    goal = GoalAPI(api_key)
    live = goal.live()
    async with live:
        live.subscribe(fixture_id)
        async for message in live:
            if message["type"] == "match_update":
                print(message["data"])

asyncio.run(watch("fixture-id"))

Or register handlers and run in the background:

live.on("match_update", lambda m: print(m["data"]))
live.on("error", lambda m: print("error:", m))
await live.connect()
live.subscribe(fixture_id)
await live.run_forever()
  • Python clients authenticate the handshake with the Authorization header, so no token round trip.
  • Reconnects with backoff and replays your subscriptions. await live.close() opts out.
  • subscribe() before connect() is fine; it's replayed on open.
  • Server caps client messages at 60/minute and concurrent subscriptions by plan.
  • A slow consumer drops the oldest queued message rather than stalling the socket.

Message types: match_update, auth_success, status, pong, server_shutdown, error. Use on("*", ...) for everything.

Handing a token to a browser

Your frontend must never see your API key. Mint a single-use token server-side instead:

from goal_api.live import mint_connect_token

token = mint_connect_token(goal)["data"]["token"]
# browser: new WebSocket(`wss://api.goal-api.com/v1/ws?wsToken=${token}`)

Webhooks

Verify against the raw body. A parsed-and-reserialized dict has different bytes and will never match.

from fastapi import FastAPI, Request, Response
from goal_api import verify_webhook, WebhookSignatureError

app = FastAPI()

@app.post("/goal-webhooks")
async def hook(request: Request):
    try:
        event = verify_webhook(
            await request.body(),
            request.headers.get("x-goal-signature"),
            os.environ["GOAL_WEBHOOK_SECRET"],
        )
    except WebhookSignatureError:
        return Response(status_code=400)

    if request.headers.get("x-goal-event") == "goal.scored":
        ...
    return Response(status_code=200)   # ack fast; retries are ~1m, 5m, 25m, 2h, 10h

Timestamps outside 300s are rejected as replays. Override with tolerance=.

Escape hatch

For an endpoint this SDK doesn't wrap yet:

data = goal.request("/some/new/endpoint", {"limit": 10})

Constants

from goal_api import MATCH_STATUSES, PLAYER_TYPES, PLAYER_STATS, HALVES

Examples

File Shows
examples/basic.py Status, live fixtures, standings, pagination
examples/live_scores.py The live socket: connect, subscribe, print every frame
examples/webhook_server.py Verifying a webhook against the raw request bytes
examples/bulk_export.py Walking every page of a collection to CSV
GOAL_API_KEY=...          python examples/live_scores.py
GOAL_WEBHOOK_SECRET=...   python examples/webhook_server.py
GOAL_API_KEY=...          python examples/bulk_export.py > countries.csv

Testing

pip install -e ".[dev]"
pytest -q                       # unit tests, no network
GOAL_API_KEY=... pytest -q      # also runs the live tests against the real API

The live tests skip themselves without a key. Endpoint-by-endpoint coverage of the API lives in tools/sweep.py in the SDK workspace.

Other languages

Four more first-party clients over the same API, with the same resource groups, the same retry and pagination behaviour and the same error types. All five release in lockstep, so a version number means the same surface everywhere.

Language Package Install
JavaScript / TypeScript @goalapi/sdk npm install @goalapi/sdk
Go goal-api-go go get github.com/goal-api/goal-api-go
Dart / Flutter goal_api dart pub add goal_api
PHP goal-api/sdk composer require goal-api/sdk

Each one is its own repository and carries the same ENDPOINTS.md, the API contract derived from the running service.

Licence

MIT. See LICENSE.

Runtime dependencies and their licences are in THIRD_PARTY_NOTICES.md. The short version: httpx (BSD-3-Clause), plus websockets (BSD-3-Clause) only if you install the live extra. One transitive dependency, certifi, is MPL-2.0 rather than permissive, which is called out there in case your licence policy cares.

Security issues: SECURITY.md.

About

Official Python SDK for the GOAL API - football fixtures, live scores, standings, stats, webhooks and real-time WebSocket updates. Sync and async clients, fully typed.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages