> Full text of the [A-League API Docs: Football Odds, History and Streams](https://odds-api.net/docs/a-league).

Quick start with the [A-League API](https://odds-api.net/sports/a-league).

# A-League API Docs: Football Odds, History and Streams

Connect A-League Men fixtures to three-way result, goal, corner and player markets. Keep each match, period and selection attached to its bookmaker price, historical series and stream updates.

## Integration path

### Find A-League matches

Request A-League events and keep each `event_id` for follow-up calls.

### Load the odds

Page through the match snapshot to collect every available market.

### Follow changes

Apply odds deltas and read the result when the match finishes.

```bash
curl -sS -G 'https://api.odds-api.net/v1/events' \
  -H "X-API-Key: $ODDS_API_KEY" \
  --data-urlencode 'sport=soccer' \
  --data-urlencode 'league=A-League'
```

- **Base URL:** https://api.odds-api.net/v1
- **Auth:** `X-API-Key: $ODDS_API_KEY`
- **League filter:** `sport=soccer&league=A-League`

## What are you building?

[A line-movement chart](https://odds-api.net/docs/a-league#history)

[Odds comparisons and statistics](https://odds-api.net/docs/a-league#statistics)

[A fixture and results app](https://odds-api.net/docs/a-league#matches)

## Use the right A-League endpoint

The public API separates match discovery, match records, odds, and results. A stream sends changes after an initial snapshot.

| Need | Endpoint | What it returns |
| --- | --- | --- |
| Current coverage | `GET /v1/coverage?sport=soccer&league=A-League` | A-League league, bookmaker, and approximate market coverage. |
| Fixtures | `GET /v1/events?league=A-League` | Teams, scheduled kick-off time, canonical IDs, and pages of matches. |
| Match record | `GET /v1/events/{event_id}` | The source match record under `data`. |
| Match result | `GET /v1/events/{event_id}/results` | `pending` or `available`, with the result record when present. |
| All match odds | `GET /v1/events/{event_id}/odds/snapshot` | Current rows across available bookmakers, markets, and periods. Follow every `next_cursor`. |
| All match odds changes | `GET /v1/events/{event_id}/odds/stream` | SSE changes for that match's odds. WebSocket: `/odds/ws`. |
| Line movement | `GET /v1/events/{event_id}/odds/history` | Retained points for one `selection_key` and time range when history access is enabled. |
| A-League main lines | `GET /v1/odds/main-lines/stream?leagues=A-League` | A league-wide feed of main lines. It is a smaller view than every match market. |

## Find A-League coverage and bookmakers

Request `GET /v1/coverage?sport=soccer&league=A-League` to build your league and bookmaker filters. The coverage response contains `bookmakers`, `leagues`, and approximate `markets` with an `as_of` timestamp. Use the canonical keys returned by `GET /v1/bookmakers` when filtering odds.

```bash
curl -sS -G 'https://api.odds-api.net/v1/coverage' \
  --data-urlencode 'sport=soccer' \
  --data-urlencode 'league=A-League'
```

The `bookmakers` field on a A-League event shows which bookmakers are attached to that match. The odds snapshot shows their current selections. For bookmaker-specific context, see [Sportsbet](https://odds-api.net/bookmakers/sportsbet), [TAB](https://odds-api.net/bookmakers/tab), and [Ladbrokes](https://odds-api.net/bookmakers/ladbrokes).

## Discover matches and read results

Page `/v1/events` with `sport=soccer` and `league=A-League`. Set `start_from` and `start_to` as Unix seconds when you need live matches as well as upcoming ones. Without `start_from`, the endpoint starts at the current time.

```bash
curl -sS -G 'https://api.odds-api.net/v1/events' \
  -H "X-API-Key: $ODDS_API_KEY" \
  --data-urlencode 'sport=soccer' \
  --data-urlencode 'league=A-League' \
  --data-urlencode 'limit=200'
```

Illustrative response. Use the returned IDs and times; these values are examples.

```json
{
  "count": 1,
  "items": [
    {
      "away_team": "Sydney FC",
      "bookmakers": {
        "sportsbet": null,
        "tab": null
      },
      "event_id": "a-league-example-001",
      "home_team": "Melbourne Victory",
      "league": "A-League",
      "sport": "soccer",
      "start_time": 1788472800
    }
  ],
  "next_cursor": null
}
```

### Follow the cursor

Pass `next_cursor` back as `cursor` with the same time filters until it is null. Use `event_id` for every follow-up request.

### Match record

```text
GET /v1/events/{event_id}
```

The response contains `event_id` and a `data` object. Read the fields actually supplied for that match; the record can carry more source detail than the event list.

### Result

```text
GET /v1/events/{event_id}/results
```

The result endpoint returns `status` as `pending` or `available`. An available response contains the result record. Read the available source result record for score fields; do not infer a match score from a goal-total betting line.

A match without a result record returns:

```json
{
  "event_id": "a-league-example-001",
  "result": null,
  "status": "pending"
}
```

## Request every available odds market

Leave `bookmakers`, `types`, `market_keys`, and `periods` out of the request. That returns all odds rows available to your account for the chosen match. `price_fields=all` includes the supported price fields; `include_unavailable=true` also returns suspended rows.

```bash
curl -sS -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/snapshot" \
  -H "X-API-Key: $ODDS_API_KEY" \
  --data-urlencode 'limit=10000' \
  --data-urlencode 'price_fields=all' \
  --data-urlencode 'include_unavailable=true' \
  --data-urlencode 'include_source=true'
```

Continue with the snapshot's `next_cursor` until it is null. Save the `resume` token from the first page before opening the stream, so changes during later pages can be replayed. Match stream changes by the odds-row `id`.

Illustrative snapshot with match odds and a player goals prop. API prices are decimal.

```json
{
  "as_of_ts_ms": 1788472500000,
  "event_id": "a-league-example-001",
  "items": [
    {
      "bet_type": "moneyline 3w",
      "bookmaker": "sportsbet",
      "event_id": "a-league-example-001",
      "fair_odds": 2.32,
      "id": "line-example-001",
      "is_available": true,
      "market_key": "moneyline 3w",
      "odds": 2.2,
      "odds_no_vig": 2.32,
      "period": "full time",
      "selection_key": "selection-example-home",
      "selection_name": "Melbourne Victory",
      "side": "home"
    },
    {
      "bet_type": "moneyline 3w",
      "bookmaker": "sportsbet",
      "event_id": "a-league-example-001",
      "fair_odds": 3.8,
      "id": "line-example-draw",
      "is_available": true,
      "market_key": "moneyline 3w",
      "odds": 3.6,
      "odds_no_vig": 3.8,
      "period": "full time",
      "selection_key": "selection-example-draw",
      "selection_name": "Draw",
      "side": "draw"
    },
    {
      "bet_type": "moneyline 3w",
      "bookmaker": "sportsbet",
      "event_id": "a-league-example-001",
      "fair_odds": 3.27,
      "id": "line-example-away",
      "is_available": true,
      "market_key": "moneyline 3w",
      "odds": 3.1,
      "odds_no_vig": 3.27,
      "period": "full time",
      "selection_key": "selection-example-away",
      "selection_name": "Sydney FC",
      "side": "away"
    },
    {
      "bet_type": "player prop",
      "bookmaker": "sportsbet",
      "event_id": "a-league-example-001",
      "id": "line-example-002",
      "is_available": true,
      "line": "0.5",
      "market_key": "player goals",
      "odds": 2.05,
      "period": "full time",
      "player_name": "Example Player",
      "selection_key": "selection-example-prop-over",
      "side": "over"
    }
  ],
  "next_cursor": null,
  "resume": "1788472500000-0",
  "ttl_seconds": 120
}
```

## Read A-League market keys

`market_key` identifies the normalized market in an odds row. Use `market_keys` in a query only when your product needs a subset. Keep `period`, `line`, `side`, and `player_name` when comparing two prices.

| Market | API market keys | Meaning |
| --- | --- | --- |
| **Match result: home, draw, away** | `moneyline 3w` | Three-way result prices with a distinct draw selection. Keep two-way moneyline markets separate. |
| **Goal totals** | `total` | Over and under goal lines. Retain the goals metric and the full-match or half period. |
| **Both teams to score** | `both teams to score` | Yes and no prices for whether both sides score in the specified period. |
| **Handicaps** | `handicap` | Goal-start lines, with each threshold and period kept as its own market. |
| **Team and corner totals** | `team total`<br>`total` | Team goal totals and corner totals. The metric, team and period identify what is being counted. |
| **Goalscorers** | `player goals`<br>`player first goal scorer` | Player goal thresholds and first-goalscorer prices, matched by player and selection. |
| **Player assists and shots** | `player assists`<br>`player shots on target` | Player assist and shots-on-target betting lines alongside the match markets. |

moneyline 3w carries home, draw and away outcomes. Match the metric as well as the key: goal totals and corner totals must remain separate. Both-teams-to-score and player-goal prices describe their own selections. Keep the period, line, player, and side when comparing exact prop selections across bookmakers.

## Keep the draw, metric and match period

A-League comparisons need more than a team and price. The draw, goal or corner metric, period and player distinguish the bet.

### Start with home, draw and away

Request `moneyline 3w` for three-way results and retain all three outcomes. A two-way `moneyline` market has different settlement rules; keep it in a separate group.

```bash
curl -sS -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/snapshot" \
  -H "X-API-Key: $ODDS_API_KEY" \
  --data-urlencode 'bookmakers=sportsbet,tab,ladbrokes' \
  --data-urlencode 'market_keys=moneyline 3w' \
  --data-urlencode 'price_fields=all'
```

### Separate goals, corners and halves

A `total` row can measure goals or corners. Keep `metric` with `market_key` and `line`. Use `period_str` when present, otherwise the returned `period` label, to distinguish the full match from its first or second half.

Load all snapshot pages before building the market picker. Retain the exact returned key and metric rather than replacing them with your display label.

### Request BTTS and player goals

```bash
curl -sS -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/snapshot" \
  -H "X-API-Key: $ODDS_API_KEY" \
  --data-urlencode 'market_keys=both teams to score,player goals,player first goal scorer' \
  --data-urlencode 'price_fields=all' \
  --data-urlencode 'include_unavailable=true'
```

BTTS uses yes/no selections. For a goalscorer, retain `player_name`, threshold and side. A player's 0.5-goal line describes a bet, not a recorded goal in the match.

### Use the exact competition key

`sport=soccer&league=A-League` selects the men's competition. The catalogue lists the women's competition separately as `A-League Women`. Keep those fixtures and their markets in separate competition views.

## Follow A-League selection prices over time

Chart an A-League home-win, draw, goal-total or goalscorer price across bookmakers. Fix the match, period and exact selection before comparing historical points.

History access is required. [History Lite includes 30 days of odds history](https://odds-api.net/historical-odds-api); your account's history access determines the query window.

### Choose the match and selection

Keep the `event_id` and `selection_key` from a match odds snapshot. Copy the selection key exactly. Set `FROM_TS` and `TO_TS` to ISO 8601 UTC timestamps within your history window, then run:

```bash
curl -sS -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/history" \
  -H "X-API-Key: $ODDS_API_KEY" \
  --data-urlencode "selection_key=$SELECTION_KEY" \
  --data-urlencode "from_ts=$FROM_TS" \
  --data-urlencode "to_ts=$TO_TS" \
  --data-urlencode 'bookmakers=sportsbet,tab' \
  --data-urlencode 'price_type=odds' \
  --data-urlencode 'price_fields=all' \
  --data-urlencode 'include_unavailable=true' \
  --data-urlencode 'limit_points_per_bookmaker=2000' \
  --output a-league-history.json
```

### Read a bookmaker's price series

`series` contains one entry per bookmaker. Each point carries `tick_ts`, `is_available` and its price fields. Use the timestamp for the chart's horizontal axis and the price for its vertical axis.

```json
{
  "bookmaker_name": "sportsbet",
  "points": [
    {"tick_ts": "2026-10-01T06:00:00Z", "is_available": true, "odds": 2.5},
    {"tick_ts": "2026-10-01T06:15:00Z", "is_available": true, "odds": 2.4},
    {"tick_ts": "2026-10-01T06:30:00Z", "is_available": true, "odds": 2.25}
  ]
}
```

### Keep the bet consistent

Compare the same match, market, period, player, side and line. Melbourne Victory over 1.5 team goals and over 2.5 team goals are different selections. Total goals over 2.5 and total corners over 2.5 also measure different things. Keep suspended points as gaps in a chart rather than carrying them forward as offered prices.

### Choose the price field

`odds` is the bookmaker quote. `odds_no_vig` removes the bookmaker margin. `fair_odds` is the composite fair-price estimate. The response's `meta.available_price_types` lists its price types.

### Opening, latest and pre-match closing prices

The first and last points describe your requested time window. For a pre-match closing price, use the last available point at or before kick-off time. A sample limit can shorten a series; narrow the time window when a bookmaker reaches `limit_points_per_bookmaker`.

### Continue a line-movement chart

After loading historical points, follow that selection through the history SSE stream. Save each stream `resume` token and pass it as `since` when reconnecting.

```bash
curl -N -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/history/stream" \
  -H "X-API-Key: $ODDS_API_KEY" \
  --data-urlencode "selection_key=$SELECTION_KEY" \
  --data-urlencode 'bookmakers=sportsbet,tab' \
  --data-urlencode 'price_fields=all' \
  --data-urlencode 'include_unavailable=true'
```

Use `/v1/events/{event_id}/odds/history/ws` for WebSockets with the same filters. A `resync` message means reload the bounded history query before continuing the chart.

## Calculate A-League probabilities and market margin

Measure a complete three-way result market, compare equivalent bookmaker quotes and calculate price movement for goal lines or player selections.

The API returns `odds`, `odds_no_vig` and `fair_odds` with `price_fields=all`. Calculate the statistics below in your app from snapshot rows or historical points.

| Statistic | Calculation | Use it for |
| --- | --- | --- |
| Implied probability | `100 / decimal_odds` | Expressing a price as a percentage. Odds of 2.00 imply 50%. |
| Bookmaker overround | `(sum(1 / odds) - 1) * 100` | Measuring the excess implied probability across every outcome in one bookmaker's market. |
| Proportional no-vig probability | `(1 / odds) / sum(1 / odds) * 100` | Normalising a complete market to 100%. See [other margin-removal methods](https://odds-api.net/guides/how-to-remove-vig-from-betting-odds). |
| Best price | `max(matched_available_odds)` | Comparing bookmakers for exactly the same selection at a comparable time. |
| Price movement | `(last / first - 1) * 100` | Showing how far the decimal price moved within your chosen time window. |
| Probability movement | `100 / last - 100 / first` | Showing the change in implied probability in percentage points. |
| Price range | `min(prices), max(prices)` | Finding the shortest and longest recorded prices for one bookmaker and selection. |

### A three-way match-result example

For home, draw and away prices of 2.20, 3.60 and 3.10, implied probabilities total 105.49%, giving a 5.49% overround. Include the draw when calculating the result market's margin.

```text
prices = [2.2, 3.6, 3.1]
implied = [1 / price for price in prices]
book = sum(implied)
overround_pct = (book - 1) * 100
no_vig_pct = [probability / book * 100 for probability in implied]
```

Use all outcomes from the same bookmaker, market, line and period. A three-way market includes the draw. Do not mix opposing prices from different bookmakers when measuring one bookmaker's margin.

### Summarise historical prices with Python

Download [a_league_odds_analysis.py](https://odds-api.net/static/examples/a_league_odds_analysis.py?v=1793501809). It reads the history JSON saved above and reports each bookmaker's first, last, minimum and maximum price, sample count, price change and probability change. It sorts by timestamp and excludes suspended or invalid prices.

```bash
python a_league_odds_analysis.py a-league-history.json

# Add a market calculation using your own matched prices.
python a_league_odds_analysis.py a-league-history.json --home-odds 2.2 --draw-odds 3.6 --away-odds 3.1
```

A goalscorer price shortening from 2.50 to 2.25 changes by −10.00%. Its implied probability rises from 40.00% to 44.44%, a gain of 4.44 percentage points.

### Match results and player markets

Read match result records through GET /v1/events/{event_id}/results. Goalscorer, assist and shots-on-target prices are betting markets, separate from recorded player statistics.

Combine the result record with your stored match and price observations to analyse completed matches. Keep the event ID, selection identity, bookmaker and observation time alongside each calculation.

## Follow A-League odds updates

Open one odds stream for each A-League match you are tracking. Pass the snapshot's `resume` token as `since`, with the same price and availability options used for the snapshot. The stream returns changes, not another full odds list.

```bash
curl -N -G "https://api.odds-api.net/v1/events/$EVENT_ID/odds/stream" \
  -H "X-API-Key: $ODDS_API_KEY" \
  --data-urlencode "since=$RESUME" \
  --data-urlencode 'catchup=true' \
  --data-urlencode 'price_fields=all' \
  --data-urlencode 'include_unavailable=true' \
  --data-urlencode 'include_source=true'
```

Illustrative payload for an SSE `delta` event. The `odd.id` identifies the row to replace.

```json
{
  "changes": [
    {
      "odd": {
        "bet_type": "moneyline 3w",
        "bookmaker": "sportsbet",
        "event_id": "a-league-example-001",
        "fair_odds": 2.32,
        "id": "line-example-001",
        "is_available": true,
        "market_key": "moneyline 3w",
        "odds": 2.25,
        "odds_no_vig": 2.32,
        "period": "full time",
        "selection_key": "selection-example-home",
        "selection_name": "Melbourne Victory",
        "side": "home"
      },
      "op": "upsert"
    }
  ],
  "event_id": "a-league-example-001",
  "resume": "1788472515000-0"
}
```

### Full A-League market feed

Discover matches with `/v1/events`, then snapshot and stream each match. Repeat discovery to add newly scheduled matches. This covers all markets offered through each event's odds endpoint.

### League-wide main lines

Use `/v1/odds/main-lines/snapshot?leagues=A-League` and `/v1/odds/main-lines/stream?leagues=A-League` for a lighter A-League scoreboard. Main lines are a subset of the per-match markets.

2. **`delta`:** apply each change to the local odds row keyed by `odd.id`. Save the latest `resume` token.
4. **`heartbeat`:** keep the connection open. It has no odds changes.
6. **`resync`:** discard the local odds view, reload every snapshot page, then reopen the stream with the new token.
8. **Disconnect:** reconnect with `since=<last_resume>` and `catchup=true`. Back off after errors.

The WebSocket route is `/v1/events/{event_id}/odds/ws`. It accepts the same query filters and sends JSON envelopes shaped as `{"event":"delta","data":{...}}`. Keep API keys on your server. Match discovery and results still use their REST endpoints.

For a line chart or audit, request `/v1/events/{event_id}/odds/history` with a `selection_key` from the snapshot and bounded ISO 8601 `from_ts` and `to_ts` values. The history stream follows one selection. Both history routes require history access; use the match snapshot and odds stream for current state.

## Understand the A-League data

These fields are the pieces an A-League product needs to join matches, compare the same bet, show current prices, and recover a live connection. Optional fields can be absent or null.

### Match identity and timing

| Field | What it means |
| --- | --- |
| `event_id` | The canonical match ID. Use it in match, result, snapshot, and stream paths. |
| `sport / league` | Use soccer and A-League to identify the competition in events and coverage. |
| `home_team / away_team` | The clubs or teams assigned to the home and away sides. |
| `start_time` | Scheduled kick-off time as Unix seconds. Convert it to the viewer's time zone. |
| `bookmakers` | Bookmakers attached to the match. Check the odds snapshot for the actual selections. |

### Markets and selections

| Field | What it means |
| --- | --- |
| `id` | An odds-row ID. Use it to update a row when a stream delta arrives; treat its format as opaque. |
| `bookmaker` | The canonical bookmaker key. Keep it when comparing prices across books. |
| `market_key` | The exact normalized market key in an odds row. The market_keys query filter matches this value. |
| `bet_type` | The normalized market family, such as moneyline, handicap, total, or player prop. |
| `period / period_str` | The period the bet covers. Keep it when matching selections. |
| `metric` | The measured scoring or player-stat quantity when the market supplies one. |
| `line` | The line or threshold. It is nullable and may be a string. |
| `side` | The side of the market, such as home, away, over, or under. |
| `player_name` | The player for a prop market, when present. |
| `selection_name` | The display name of the offered selection, when present. |
| `selection_key` | An optional normalized selection identity. Treat it as opaque and also compare period, line, player, and side. |

### Prices and state

| Field | What it means |
| --- | --- |
| `odds` | The bookmaker's decimal price. A value of 1.91 implies about a 52.4% break-even probability before fees. |
| `odds_no_vig` | A nullable price with the bookmaker margin removed. |
| `fair_odds` | A nullable composite fair-odds estimate. Request price_fields=all for every supported price field. |
| `is_available` | Whether the row is currently offered. Request include_unavailable=true to track suspensions. |
| `as_of_ts_ms` | Snapshot freshness time in Unix milliseconds. It is distinct from the match start time. |
| `ttl_seconds` | The snapshot cache lifetime when supplied. Show age rather than presenting stale odds as current. |
| `next_cursor` | The next page token. Continue until it is null for both event lists and odds snapshots. |
| `resume / since` | Pass the snapshot resume token as the stream's since parameter, then persist the latest delta token. |

The public API has no dedicated A-League standings, xG, lineup or player match-statistics endpoint. Player goals, assists and shots on target here are bookmaker markets.

## Handle normal production states

| Response or state | What to do |
| --- | --- |
| No A-League events | Keep the selected time window visible. An empty page means no matches matched that request; widen the window or wait for another slate. |
| No odds rows | Keep the match record. Odds can be posted later, and a specific bookmaker or prop may have no rows. |
| `status=pending` | The result record is not ready. Poll the match result endpoint separately from the odds stream. |
| Old `as_of_ts_ms` | Show the snapshot age in your product and reload rather than presenting a stale price as current. |
| 401 or 403 | Check the server-side API key and whether the account has event odds access. See [plans](https://odds-api.net/pricing). |
| 429 or stream disconnect | Back off and reconnect with the last `resume` token. Respect rate-limit headers when present. |
| `resync` | Reload all snapshot pages before opening a new stream. The older changes cannot safely be replayed. |

Keep `ODDS_API_KEY` on your server. Use [the API reference](https://odds-api.net/docs) for current response codes and parameter limits, and [status and support](https://odds-api.net/sla-status-support) when an endpoint is unavailable.

## Run the complete A-League example

The Python example pages a rolling A-League schedule, requests match records and results, loads every odds snapshot page, and opens a stream for each match. It writes JSON Lines, so another process can consume the complete records. Set a suitable discovery window and connection budget for your product.

```bash
python -m pip install httpx
export ODDS_API_KEY='your_api_key'
python a_league_full_feed.py
```

Save [a_league_full_feed.py](https://odds-api.net/static/examples/a_league_full_feed.py?v=1793501809) and run it from your server. Set `A_LEAGUE_PAST_HOURS` and `A_LEAGUE_FUTURE_HOURS` to change the rolling match window. Set `ODDS_API_BASE_URL` for a different API environment.

### Read the full Python example

```python
"""Discover A-League fixtures, load every available odds row, and follow each game.

Install: python -m pip install httpx
Run:     ODDS_API_KEY=your_key python a_league_full_feed.py

The program writes JSON Lines to stdout. Set A_LEAGUE_PAST_HOURS and
A_LEAGUE_FUTURE_HOURS to change the rolling discovery window.
"""

from __future__ import annotations

import asyncio
import json
import os
import random
import sys
import time
from typing import Any, AsyncIterator

import httpx


BASE_URL = os.getenv("ODDS_API_BASE_URL", "https://api.odds-api.net/v1").rstrip("/") + "/"
API_KEY = os.getenv("ODDS_API_KEY", "").strip()
PAST_HOURS = int(os.getenv("A_LEAGUE_PAST_HOURS", "8"))
FUTURE_HOURS = int(os.getenv("A_LEAGUE_FUTURE_HOURS", "48"))
DISCOVERY_SECONDS = int(os.getenv("A_LEAGUE_DISCOVERY_SECONDS", "300"))
ODDS_SHAPE = {
    "price_fields": "all",
    "include_unavailable": "true",
    "include_source": "true",
}


def emit(kind: str, **payload: Any) -> None:
    print(json.dumps({"kind": kind, **payload}, separators=(",", ":")), flush=True)


async def get_json(client: httpx.AsyncClient, path: str, params: dict | None = None) -> dict:
    response = await client.get(path, params=params)
    response.raise_for_status()
    return response.json()


async def a_league_events(client: httpx.AsyncClient) -> list[dict]:
    now = int(time.time())
    params = {
        "sport": "soccer",
        "league": "A-League",
        "start_from": now - PAST_HOURS * 3600,
        "start_to": now + FUTURE_HOURS * 3600,
        "limit": 1000,
    }
    events: list[dict] = []
    while True:
        page = await get_json(client, "events", params)
        events.extend(page.get("items") or [])
        cursor = page.get("next_cursor")
        if not cursor:
            return events
        params["cursor"] = cursor


async def complete_snapshot(client: httpx.AsyncClient, event_id: str) -> tuple[dict, dict[str, dict]]:
    params = {"limit": 10000, **ODDS_SHAPE}
    rows: dict[str, dict] = {}
    first_page: dict | None = None
    while True:
        page = await get_json(client, f"events/{event_id}/odds/snapshot", params)
        if first_page is None:
            first_page = page
        for odd in page.get("items") or []:
            if odd.get("id"):
                rows[str(odd["id"])] = odd
        cursor = page.get("next_cursor")
        if not cursor:
            break
        params["cursor"] = cursor

    # Start the stream at the first page's resume token. Changes that arrive
    # while later snapshot pages load can then be replayed by the stream.
    return first_page or {}, rows


async def sse_messages(response: httpx.Response) -> AsyncIterator[tuple[str, dict]]:
    event = "message"
    data: list[str] = []
    async for line in response.aiter_lines():
        if line == "":
            if data:
                yield event, json.loads("\n".join(data))
            event, data = "message", []
        elif line.startswith("event:"):
            event = line[6:].strip()
        elif line.startswith("data:"):
            data.append(line[5:].lstrip())
    if data:
        yield event, json.loads("\n".join(data))


def apply_delta(rows: dict[str, dict], payload: dict) -> None:
    for change in payload.get("changes") or []:
        odd = change.get("odd") or {}
        row_id = str(odd.get("id") or "")
        if not row_id:
            continue
        if change.get("op") in {"remove", "delete"}:
            rows.pop(row_id, None)
        elif change.get("op") == "upsert":
            # Keep is_available=false rows so the product can show suspensions.
            rows[row_id] = odd


async def follow_event(client: httpx.AsyncClient, event_id: str) -> None:
    resume = ""
    rows: dict[str, dict] = {}
    needs_snapshot = True
    retry_seconds = 1.0

    while True:
        try:
            if needs_snapshot:
                snapshot, rows = await complete_snapshot(client, event_id)
                resume = str(snapshot.get("resume") or "$")
                emit(
                    "odds_snapshot",
                    event_id=event_id,
                    as_of_ts_ms=snapshot.get("as_of_ts_ms"),
                    ttl_seconds=snapshot.get("ttl_seconds"),
                    resume=resume,
                    items=list(rows.values()),
                )
                needs_snapshot = False

            params = {"since": resume, "catchup": "true", "max_batch": 2000, **ODDS_SHAPE}
            async with client.stream(
                "GET", f"events/{event_id}/odds/stream", params=params,
                headers={"Accept": "text/event-stream"},
            ) as response:
                response.raise_for_status()
                retry_seconds = 1.0
                async for event, payload in sse_messages(response):
                    if event == "delta":
                        apply_delta(rows, payload)
                        resume = str(payload.get("resume") or resume)
                        emit("odds_delta", event_id=event_id, resume=resume, changes=payload.get("changes") or [])
                    elif event == "resync":
                        emit("odds_resync", event_id=event_id, reason=payload.get("reason"))
                        needs_snapshot = True
                        break
                    # A heartbeat contains no price change.

        except asyncio.CancelledError:
            raise
        except (httpx.HTTPError, ValueError, json.JSONDecodeError) as exc:
            emit("stream_error", event_id=event_id, error=str(exc))

        if needs_snapshot:
            continue
        await asyncio.sleep(retry_seconds + random.uniform(0, 0.5))
        retry_seconds = min(retry_seconds * 2, 30.0)


async def game_record(client: httpx.AsyncClient, event_id: str) -> None:
    try:
        detail, result = await asyncio.gather(
            get_json(client, f"events/{event_id}"),
            get_json(client, f"events/{event_id}/results"),
        )
        emit("event", event_id=event_id, data=detail)
        emit("result", event_id=event_id, data=result)
    except httpx.HTTPError as exc:
        emit("record_error", event_id=event_id, error=str(exc))


async def bounded_game_record(client: httpx.AsyncClient, semaphore: asyncio.Semaphore, event_id: str) -> None:
    async with semaphore:
        await game_record(client, event_id)


async def main() -> None:
    if not API_KEY:
        raise SystemExit("Set ODDS_API_KEY before running this example.")
    timeout = httpx.Timeout(connect=10.0, read=None, write=10.0, pool=10.0)
    tasks: dict[str, asyncio.Task] = {}
    record_limit = asyncio.Semaphore(6)
    async with httpx.AsyncClient(
        base_url=BASE_URL,
        headers={"X-API-Key": API_KEY, "Accept": "application/json"},
        timeout=timeout,
    ) as client:
        try:
            while True:
                try:
                    events = await a_league_events(client)
                    ids = {str(item["event_id"]) for item in events if item.get("event_id")}
                    emit("event_list", count=len(events), items=events)

                    for event_id in ids:
                        if event_id not in tasks or tasks[event_id].done():
                            tasks[event_id] = asyncio.create_task(follow_event(client, event_id))
                    for event_id in set(tasks) - ids:
                        tasks.pop(event_id).cancel()

                    await asyncio.gather(
                        *(bounded_game_record(client, record_limit, event_id) for event_id in ids)
                    )
                except httpx.HTTPError as exc:
                    emit("discovery_error", error=str(exc))
                await asyncio.sleep(DISCOVERY_SECONDS)
        finally:
            for task in tasks.values():
                task.cancel()
            await asyncio.gather(*tasks.values(), return_exceptions=True)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("Stopped.", file=sys.stderr)
```

## A-League endpoint parameters

The parameter lists come from the current public OpenAPI document. The [full API reference](https://odds-api.net/docs) contains response schemas and other sports routes.

### GET Coverage `/v1/coverage`

Returns public bookmaker, sport, league, and recently observed market coverage. Market records are approximate and based on normalized odds lines seen in the configured lookback window, not a guarantee that every market is available for every event at request time.

### Query parameters

- `bookmaker`: string. Canonical bookmaker filter. Use `/bookmakers` or `/coverage` to discover supported keys.

- `sport`: string. Sport filter. Use `/sports` to discover supported values.

- `league`: string. League filter. Use `/leagues?sport=...` to discover supported values.

- `country_code`: string. Comma-separated country code filter, for example `AU` or `AU,UK`.

- `lookback_days`: integer. Number of days of recently observed approximate market coverage to include. Maximum is 90.

- `view`: string. `summary` returns the bookmaker, sport and league catalog without market rows (small and cacheable; use it for counts and pickers). `full` (default) also returns every observed market row.

[Open full response schema](https://odds-api.net/docs#/operations/coverage_coverage_get)

### GET Bookmakers `/v1/bookmakers`

Lists active bookmakers accepted by bookmaker filters across odds and betting endpoints. Each item includes the country codes where that bookmaker is available. Pass `country_code=AU` or `country_code=AU,UK` to filter the catalog.

### Query parameters

- `country_code`: string. Comma-separated country code filter, for example `AU` or `AU,UK`.

[Open full response schema](https://odds-api.net/docs#/operations/bookmakers_bookmakers_get)

### GET Search `/v1/events`

Searches sports events by sport, league, team, time window, status, bookmaker coverage, and pagination cursor. For production polling, use bounded time windows, keep filters stable across pages, and pass `next_cursor` back as `cursor` until there is no next cursor. For live discovery start with `/v1/events/live`; `event_states=in_play` returns only events with a fresh live observation and adds a `hint` when started but unconfirmed events were excluded. Every item has a plain `live_status`.

### Query parameters

- `sport`: string. Sport filter. Use `/sports` to discover supported values.

- `league`: string. League filter. Use `/leagues?sport=...` to discover supported values.

- `start_from`: integer. Unix seconds lower bound for event start time. Use bounded windows in production polling.

- `start_to`: integer. Unix seconds upper bound for event start time. Keep windows narrow for hot sync jobs.

- `cursor`: string. Pagination cursor from the previous `next_cursor`. Keep filters identical between pages.

- `limit`: integer. Maximum items to return. Respect the caps returned by `/limits`.

- `include_bookmaker_ids`: boolean. When true, include bookmaker-to-odds-data IDs and preview ID maps on event responses.

- `include_source`: boolean. When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.

- `include_opportunity_counts`: boolean.

- `not_started_only`: boolean.

- `not_started_buffer_seconds`: integer.

- `event_states`: string. Lifecycle filter. Requesting in_play automatically applies the live lookback window.

- `live_candidates`: boolean. Include already-started events that remain candidates for live play; this is not confirmation of current play.

[Open full response schema](https://odds-api.net/docs#/operations/list_events_events_get)

### GET Event details `/v1/events/{event_id}`

Returns the current event record for a canonical sports event ID.

### Path parameters

- `event_id` (required): string. Canonical event or race identifier from an event list response.

### Query parameters

- `include_links`: boolean. When true, include bookmaker/deep-link fields such as match links and racing links.

- `include_raw_payload`: boolean. When true, include raw stored payload/data objects where the endpoint exposes them.

- `include_source`: boolean. When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.

- `include_bookmaker_ids`: boolean. When true, include bookmaker-to-odds-data IDs and preview ID maps on event responses.

- `include_debug_ids`: boolean. When true, include internal/subgroup/opposing IDs useful for reconciliation.

[Open full response schema](https://odds-api.net/docs#/operations/get_event_events__event_id__get)

### GET Event result `/v1/events/{event_id}/results`

Returns the latest known result for a sports event, or `pending` until settled. Poll every 1-5 minutes after start, then back off once the event is final.

### Path parameters

- `event_id` (required): string. Canonical event or race identifier from an event list response.

[Open full response schema](https://odds-api.net/docs#/operations/get_event_results_events__event_id__results_get)

### GET Snapshot `/v1/events/{event_id}/odds/snapshot`

Returns the current odds lines for one event. Use filters to narrow bookmakers, market types, market keys, and periods. An explicit `bookmakers` filter returns every matching line for those bookmakers in one response, up to the 25,000-item safety cap. Without a bookmaker filter, pages contain whole bookmakers, so a bookmaker's matching markets are never split across pages. Follow the opaque `next_cursor` until `complete=true`. Cache by request shape. `as_of_ts_ms` is event-wide: the latest acceptance from any bookmaker on the event, including bookmakers this response does not return, so it can look fresh while the bookmakers you asked for are old. Use `bookmaker_as_of_ts_ms` for bookmaker-specific freshness and compare it with `target_refresh_interval_seconds`; `oldest_bookmaker_as_of_ts_ms`, `newest_bookmaker_as_of_ts_ms` and `stale_bookmakers` (returned bookmakers last accepted more than twice the target interval ago) summarise it (prediction markets report when their current contract prices were accepted). Respect `ttl_seconds` when present, and persist `resume` when you plan to subscribe to updates. Pass `price_fields=odds,fair` to include nullable composite fair odds alongside bookmaker odds. Exchange orderbooks are excluded from this sportsbook-style odds surface.

### Path parameters

- `event_id` (required): string. Canonical event or race identifier from an event list response.

### Query parameters

- `limit`: integer. Soft item target when `bookmakers` is omitted. Pages contain whole bookmakers and may exceed this target. With an explicit bookmaker filter, all matching lines are returned up to the 25,000-item safety cap.

- `cursor`: string. Opaque bookmaker-page cursor from the previous `next_cursor`. Use only when `bookmakers` is omitted and keep all filters identical between pages.

- `bookmakers`: string. Comma-separated bookmaker allow-list. Explicitly requested bookmakers are returned complete in one response, up to the 25,000-item safety cap. Use `/bookmakers` to discover supported keys. Requested keys this API key cannot access, or that are not bookmakers, are listed in `excluded_bookmakers` and the `X-Excluded-Bookmakers` header.

- `types`: string. Comma-separated market type allow-list for odds filters.

- `market_keys`: string. Comma-separated market key allow-list for odds filters.

- `periods`: string. Comma-separated period allow-list for odds filters. Each value matches a line's numeric `period` or its period label, for example `full time`, `1st half` or `after 15 minutes`. Numeric `0` also matches timed checkpoints such as `after 15 minutes`; use `full time` to select only full-time lines.

- `price_fields`: string. `odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.

- `include_source`: boolean. When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.

- `include_debug_ids`: boolean. When true, include internal/subgroup/opposing IDs useful for reconciliation.

- `include_unavailable`: boolean. When true, include unavailable or suspended rows where the endpoint supports them.

- `include_stale`: boolean. Return bookmakers whose prices are very stale (last accepted more than max(24 h, 20 x target_refresh_interval_seconds) ago, or only pre-match prices after kickoff). By default they are left out and listed in excluded_bookmakers with reason `stale`.

[Open full response schema](https://odds-api.net/docs#/operations/odds_snapshot_events__event_id__odds_snapshot_get)

### GET Stream (SSE) `/v1/events/{event_id}/odds/stream`

Server-Sent Events feed for odds changes on one event. Subscribe after reading the snapshot and pass the snapshot `resume` value as `since` to receive catch-up changes when available. Handle ordered semantic `delta` batches idempotently and persist each batch's `resume`; a `heartbeat` carries current freshness even when prices did not change. Reload the snapshot after `resync`. Exchange orderbook changes are served only from the exchange orderbook stream.

### Path parameters

- `event_id` (required): string. Canonical event or race identifier from an event list response.

### Query parameters

- `bookmakers`: string. Comma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys. Requested keys this API key cannot access, or that are not bookmakers, are reported in the `X-Excluded-Bookmakers` header.

- `types`: string. Comma-separated market type allow-list for odds filters.

- `market_keys`: string. Comma-separated market key allow-list for odds filters.

- `periods`: string. Comma-separated period allow-list for odds filters. Each value matches a line's numeric `period` or its period label, for example `full time`, `1st half` or `after 15 minutes`. Numeric `0` also matches timed checkpoints such as `after 15 minutes`; use `full time` to select only full-time lines.

- `price_fields`: string. `odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.

- `include_source`: boolean. When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.

- `include_debug_ids`: boolean. When true, include internal/subgroup/opposing IDs useful for reconciliation.

- `include_unavailable`: boolean. When true, include unavailable or suspended rows where the endpoint supports them.

- `since`: string. Resume token from a previous snapshot or stream message. Pass it after reconnecting.

- `catchup`: boolean. When true, return available missed stream events after `since` before waiting for new events.

- `heartbeat_sec`: integer. Maximum seconds between messages: a `heartbeat` is sent whenever this long passes without another message. Valid range is 5-120.

- `max_batch`: integer. Maximum stream messages to read per batch. Default is 500; use smaller batches for low-latency clients.

[Open full response schema](https://odds-api.net/docs#/operations/odds_stream_events__event_id__odds_stream_get)

### GET Stream (WebSocket) `/v1/events/{event_id}/odds/ws`

WebSocket feed for odds changes on one event. Messages use the same `delta`, `heartbeat`, and `resync` payloads as the SSE stream. Reconnect with jittered exponential backoff and `since=<last_resume>`.

### Path parameters

- `event_id` (required): string. Canonical event or race identifier from an event list response.

### Query parameters

- `bookmakers`: string. Comma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys.

- `types`: string. Comma-separated market type allow-list for odds filters.

- `market_keys`: string. Comma-separated market key allow-list for odds filters.

- `periods`: string. Comma-separated period allow-list for odds filters. Each value matches a line's numeric `period` or its period label, for example `full time`, `1st half` or `after 15 minutes`. Numeric `0` also matches timed checkpoints such as `after 15 minutes`; use `full time` to select only full-time lines.

- `price_fields`: string. `odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.

- `include_source`: boolean. When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.

- `include_debug_ids`: boolean. When true, include internal/subgroup/opposing IDs useful for reconciliation.

- `include_unavailable`: boolean. When true, include unavailable or suspended rows where the endpoint supports them.

- `since`: string. Resume token from a previous snapshot or stream message. Pass it after reconnecting.

- `catchup`: boolean. When true, return available missed stream events after `since` before waiting for new events.

- `heartbeat_sec`: integer. Maximum seconds between messages: a `heartbeat` is sent whenever this long passes without another message. Valid range is 5-120.

- `max_batch`: integer. Maximum stream messages to read per batch. Default is 500; use smaller batches for low-latency clients.

[Open full response schema](https://odds-api.net/docs#/operations/websocket_events_event_id_odds_ws)

### GET Snapshot `/v1/events/{event_id}/odds/history`

Returns line movement for a single selection across bookmakers and a time range. Use `selection_key` from an odds snapshot response, bound queries with `from_ts` and `to_ts`, and narrow by bookmaker or market when building charts or backtests.

### Path parameters

- `event_id` (required): string. Canonical event or race identifier from an event list response.

### Query parameters

- `selection_key` (required): string. Stable selection identifier from an odds snapshot line, used for history and line movement.

- `market_group_id`: string. Optional market grouping filter for history queries.

- `bookmakers`: string. Comma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys. Requested keys this API key cannot access, or that are not bookmakers, are reported in the `X-Excluded-Bookmakers` header.

- `from_ts`: string. ISO8601 UTC start timestamp for a bounded history query.

- `to_ts`: string. ISO8601 UTC end timestamp for a bounded history query.

- `price_type`: string. History price type to return, for example odds.

- `price_fields`: string. `odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.

- `include_source`: boolean. When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.

- `include_debug_ids`: boolean. When true, include internal/subgroup/opposing IDs useful for reconciliation.

- `include_unavailable`: boolean. When true, include unavailable or suspended rows where the endpoint supports them.

- `limit_points_per_bookmaker`: integer. Maximum history points per bookmaker. Use this to keep chart/backtest payloads bounded.

[Open full response schema](https://odds-api.net/docs#/operations/odds_history_events__event_id__odds_history_get)

### GET Stream (SSE) `/v1/events/{event_id}/odds/history/stream`

Server-Sent Events feed for line movement on one selection. This is useful for charts that should update while an event market is moving.

### Path parameters

- `event_id` (required): string. Canonical event or race identifier from an event list response.

### Query parameters

- `selection_key` (required): string. Stable selection identifier from an odds snapshot line, used for history and line movement.

- `market_group_id`: string. Optional market grouping filter for history queries.

- `bookmakers`: string. Comma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys. Requested keys this API key cannot access, or that are not bookmakers, are reported in the `X-Excluded-Bookmakers` header.

- `price_type`: string. History price type to return, for example odds.

- `price_fields`: string. `odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.

- `include_source`: boolean. When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept.

- `include_debug_ids`: boolean. When true, include internal/subgroup/opposing IDs useful for reconciliation.

- `include_unavailable`: boolean. When true, include unavailable or suspended rows where the endpoint supports them.

- `since`: string. Resume token from a previous snapshot or stream message. Pass it after reconnecting.

- `catchup`: boolean. When true, return available missed stream events after `since` before waiting for new events.

- `heartbeat_sec`: integer. Maximum seconds between messages: a `heartbeat` is sent whenever this long passes without another message. Valid range is 5-120.

- `max_batch`: integer. Maximum stream messages to read per batch. Default is 500; use smaller batches for low-latency clients.

[Open full response schema](https://odds-api.net/docs#/operations/odds_history_stream_events__event_id__odds_history_stream_get)

For a narrower build, start with the [A-League product overview](https://odds-api.net/sports/a-league) or the [player market key list](https://odds-api.net/betting-markets#player-props).
