---
title: "Match predictions (Elo win/draw/loss + value bet)"
method: GET
path: "/v1/predictions"
tags: ["nerd"]
---

# Match predictions (Elo win/draw/loss + value bet)

`GET /v1/predictions`

## Request

- Base URL: `https://api.bigballsdata.com`
- URL: `https://api.bigballsdata.com/v1/predictions`
- Auth: one of:
  - API key in header `x-api-key`
  - HTTP bearer

**COVERAGE, stated plainly: this is not a per-sport prediction feed.**
Re-measured 2026-09-28 against production: 276 rows in total, and every
row is tied to one match, so the sport of a row is the sport of its
match. By sport, against that denominator of 276; football 176 (72
`match_result` for **FIFA World Cup 2026** plus 104 `value_prop`),
american_football 67, ice_hockey 29, baseball 2, basketball 2. Those
five sports are exactly what `meta.covered_sports` names. A plan scoped
to any OTHER sport receives 403 with the coverage named; a plan scoped
to one of these five is admitted and sees only its own sports' rows.
Predictions are written per batch against matches that already carry
moneyline odds, not as a continuous per-sport feed, so which of the
five sports has rows on a given day follows the odds board. (The
2026-09-02 figure of 129 rows, and its "there is no live writer" note,
are superseded: the value_prop writer resumed and added 147 rows.)

**FRESHNESS, AND WHY IT IS ON THE RESPONSE.** 129 of those 276 rows
(46.7%) are past their own `valid_until`; all 72 `match_result` rows
and 57 of the 204 `value_prop` rows, the oldest by 92 days. They are
**still returned**; nothing is filtered. Each row carries `valid_until`,
`expired` (true / false / **null** when the row states no expiry, which
is not the same as fresh) and `expired_days`, and `meta` carries
`returned`, `expired`, `expiry_unknown`, `as_of` and; only when at
least one row is expired; `stale: true` and a human `message`. An
expired row describes a match already played and must not be read as a
forecast: check `valid_until` per row before using it.

Pre-computed Elo-based win / draw / loss probabilities per match, with
value-bet analysis (Elo-implied vs de-vigged market probability) when
moneyline odds exist. Filter by `tournament` (e.g. wc2026), `match_id`,
or `type`. Requires Solo plan or higher (Free receives 403); results are
additionally scoped to the sports your key is entitled to.
Response array item keys: match_id, prediction_type, home_team,
away_team, home_win_probability, draw_probability, away_win_probability,
home_elo, away_elo, model_version, valid_until, expired, expired_days,
generated_at, kickoff_utc, plus value_bet / best_side / elo_prob /
market_prob / edge (null until moneyline odds land).

**UPDATE 2026-10-09 (correcting forward; the 276-row and 46.7% figures
above are a 2026-09-28 snapshot and are left as the record of that
date).** Re-measured against the whole table, 642 rows: football 259
(72 `match_result` plus 187 `value_prop`), american_football 258,
ice_hockey 85, baseball 24, basketball 16. 414 of 642 (64.5%) are past
their own `valid_until`.

Two corrections to the paragraphs above. First, "null until moneyline
odds land" was the wrong explanation for `value_bet` / `best_side` /
`elo_prob` / `market_prob` / `edge` being null on a `value_prop` row: a
`value_prop` row is only written for a match that ALREADY carries
moneyline odds, and the row stores its own model probability, de-vigged
market probability and edge. 570 of 642 rows (88.8%) were returning
those five fields null, plus `home_team`, `away_team` and the three
win/draw/loss probabilities, because the read path extracted only the
key names a `match_result` row uses. `value_prop` rows now serve
`best_side`, `elo_prob`, `market_prob`, `edge` and `value_bet` from
their own stored analysis, and `home_team` / `away_team` from the match.
`home_win_probability`, `draw_probability` and `away_win_probability`
remain null on a `value_prop` row deliberately: that model is two-way
and states no draw, so the number is reported only where it is
side-scoped, in `elo_prob` against `best_side`.

Second, ORDERING. The list was ordered by kickoff ascending, i.e.
oldest first, so `?limit=5` returned five expired rows, the first 124
days past expiry, while 228 live rows sat further down. Live rows (and
rows whose expiry is unknown) now sort ahead of expired ones; within
the live group soonest kickoff first, within the expired group most
recent kickoff first. This is an ordering change and not a filter:
nothing is withheld, `meta.total` is unchanged, and every expired row
is still returned on a later page.

## Query parameters

- `tournament` 'wc2026'
- `match_id` string, uuid
- `type` string
- `sport` 'football' | 'basketball' | 'baseball' | 'ice_hockey' | 'cricket' | 'mma' | 'boxing' | 'formula1' | 'american_football'
- `limit` integer
- `offset` integer

## Response `200`

ok. When `?sport=` named a sport with no predictions, `data` is empty
and `meta` carries `coverage`, `covered_sports` and `note`.

- object

## Other responses

- `400`: invalid query, or a ?sport= value that is not a known sport
- `403`: plan_required (Solo or higher)

---

[API](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api.md) · [All operations](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api/llms.txt) · [OpenAPI document](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api/revisions/9191fc13fcdb?raw)
