Skip to main content

Overview

The Games endpoints give you a single call for everything happening in one match: every market type (moneyline, spread, totals, halftime, props) × every bookmaker, laid out in a matrix with Pinnacle as the reference column. This is the fastest way to build a comparison view — no joins, no multiple calls.

How game IDs work

Every game has a stable game_id UUID derived from (competition, match_name):
game_id = md5(competition + '::' + lower(trim(match_name)))::uuid
The same game always gets the same UUID — store it in your DB, use it in WebSocket filters, or share it in URLs. It never changes while the game is live.

GET /api/games

List all games currently in the odds database.

Query Parameters

ParameterTypeDefaultDescription
sportstringFilter by sport name (e.g. Football, Basketball)
competitionstringFilter by competition code (e.g. EPL, NBA)
bookmakerstringOnly games where this bookmaker has odds
is_livebooleantrue = in-play only, false = pre-match only
limitinteger200Max games to return, up to 500

Response

{
  "data": [
    {
      "game_id": "a3f1d2e4-b5c6-7890-abcd-ef1234567890",
      "match_name": "PSG - Lyon",
      "competition": "LIG1",
      "sport": "Football",
      "match_date": "2026-04-20T20:00:00Z",
      "is_live": false,
      "bookmaker_count": 5,
      "last_scraped_at": "2026-04-20T14:30:00Z"
    }
  ],
  "meta": {
    "count": 47,
    "rate_limit_remaining": 199
  }
}

Response Fields

FieldTypeDescription
game_idUUID stringStable deterministic ID for this game
match_namestringMatch name as stored in odds database
competitionstringCompetition code (e.g. EPL, NBA, LIG1)
sportstringSport name derived from competition code
match_dateISO-8601 | nullScheduled kick-off time (UTC)
is_livebooleanWhether any bookmaker has this as live right now
bookmaker_countintegerNumber of bookmakers with active odds
last_scraped_atISO-8601 | nullMost recent odds update for this game

Code Example

# All live Football games
curl "https://api.oddsstream.io/api/games?sport=Football&is_live=true" \
  -H "X-Api-Key: os_live_YOUR_KEY"
import requests

games = requests.get(
    "https://api.oddsstream.io/api/games",
    params={"sport": "Football", "competition": "EPL"},
    headers={"X-Api-Key": "os_live_YOUR_KEY"},
).json()["data"]

for g in games:
    print(g["game_id"], g["match_name"], g["bookmaker_count"], "books")

GET /api/games/{game_id}

All odds for one game — every market type × every bookmaker in a single matrix response. Markets are grouped by (market_type, period). Within each market, selections are rows and bookmakers are columns, with Pinnacle always shown separately.

Path Parameter

ParameterDescription
game_idUUID from GET /api/games

Query Parameters

ParameterTypeDefaultDescription
market_typestringFilter to one market type (e.g. moneyline, total)
periodintegerFilter to one period (0=full-time, 1=first-half)
bookmakerstringInclude only one bookmaker’s prices

Response

{
  "data": {
    "game_id": "a3f1d2e4-b5c6-7890-abcd-ef1234567890",
    "match_name": "PSG - Lyon",
    "competition": "LIG1",
    "sport": "Football",
    "match_date": "2026-04-20T20:00:00Z",
    "is_live": false,
    "all_bookmakers": ["Betsson", "Unibet.fr", "Winamax"],
    "markets": [
      {
        "market_type": "moneyline",
        "period": 0,
        "period_label": "Full Time",
        "bookmakers": ["Betsson", "Unibet.fr", "Winamax"],
        "pinnacle_scraped_at": "2026-04-20T14:29:45Z",
        "selections": [
          {
            "selection": "PSG",
            "pinnacle": { "odds": 1.72, "liquidity": 4200, "scraped_at": "2026-04-20T14:29:45Z" },
            "books": {
              "Betsson":   { "odds": 1.78, "scraped_at": "2026-04-20T14:28:00Z", "url": null },
              "Unibet.fr": { "odds": 1.75, "scraped_at": "2026-04-20T14:27:30Z", "url": null },
              "Winamax":   { "odds": 1.80, "scraped_at": "2026-04-20T14:29:10Z", "url": null }
            }
          },
          {
            "selection": "Draw",
            "pinnacle": { "odds": 3.85, "liquidity": 1800, "scraped_at": "2026-04-20T14:29:45Z" },
            "books": {
              "Betsson":   { "odds": 3.90, "scraped_at": "2026-04-20T14:28:00Z", "url": null },
              "Unibet.fr": { "odds": 3.80, "scraped_at": "2026-04-20T14:27:30Z", "url": null },
              "Winamax":   { "odds": 3.95, "scraped_at": "2026-04-20T14:29:10Z", "url": null }
            }
          },
          {
            "selection": "Lyon",
            "pinnacle": { "odds": 4.50, "liquidity": 1200, "scraped_at": "2026-04-20T14:29:45Z" },
            "books": {
              "Betsson":   { "odds": 4.60, "scraped_at": "2026-04-20T14:28:00Z", "url": null },
              "Unibet.fr": null,
              "Winamax":   { "odds": 4.55, "scraped_at": "2026-04-20T14:29:10Z", "url": null }
            }
          }
        ]
      },
      {
        "market_type": "total",
        "period": 0,
        "period_label": "Full Time",
        "bookmakers": ["Betsson", "Winamax"],
        "pinnacle_scraped_at": "2026-04-20T14:29:45Z",
        "selections": [
          {
            "selection": "Over 2.5",
            "pinnacle": { "odds": 2.05, "liquidity": 3500, "scraped_at": "2026-04-20T14:29:45Z" },
            "books": {
              "Betsson": { "odds": 2.10, "scraped_at": "2026-04-20T14:28:00Z", "url": null },
              "Winamax": { "odds": 2.08, "scraped_at": "2026-04-20T14:29:10Z", "url": null }
            }
          }
        ]
      }
    ]
  },
  "meta": {
    "rate_limit_remaining": 198
  }
}

Response Fields

Top-level data:
FieldTypeDescription
game_idUUID stringStable game identifier
match_namestringMatch name
competitionstringCompetition code
sportstringSport name
match_dateISO-8601 | nullKick-off time (UTC)
is_livebooleanWhether the game is currently live
all_bookmakersstring[]All bookmakers with odds for this game
marketsMarketMatrix[]All markets in matrix format
MarketMatrix object:
FieldTypeDescription
market_typestringMarket type (moneyline, spread, total, player_prop, etc.)
periodintegerPeriod (0=full-time, 1=first-half, 2=second-half)
period_labelstringHuman-readable period (e.g. "Full Time", "1st Half")
bookmakersstring[]Bookmakers with odds for this specific market
pinnacle_scraped_atISO-8601 | nullWhen Pinnacle’s prices were last updated for this market
selectionsMatrixRow[]One row per outcome
MatrixRow object:
FieldTypeDescription
selectionstringOutcome name (e.g. "PSG", "Over 2.5", "Draw")
pinnaclePinnacleCell | nullPinnacle’s reference price
booksRecord<bookmaker, BookOddsCell | null>Each bookmaker’s price (null = no odds available)
PinnacleCell:
FieldTypeDescription
oddsfloat | nullDecimal odds
liquidityfloat | nullPinnacle max bet — proxy for line confidence
scraped_atISO-8601 | nullWhen this price was scraped
BookOddsCell:
FieldTypeDescription
oddsfloat | nullDecimal odds
scraped_atISO-8601 | nullWhen this price was scraped
urlstring | nullDirect link to the market (if available)

Code Examples

# All markets for a game
curl "https://api.oddsstream.io/api/games/a3f1d2e4-b5c6-7890-abcd-ef1234567890" \
  -H "X-Api-Key: os_live_YOUR_KEY"

# Only moneyline full-time
curl "https://api.oddsstream.io/api/games/a3f1d2e4-b5c6-7890-abcd-ef1234567890?market_type=moneyline&period=0" \
  -H "X-Api-Key: os_live_YOUR_KEY"
import requests

def get_game(game_id: str, api_key: str) -> dict:
    return requests.get(
        f"https://api.oddsstream.io/api/games/{game_id}",
        headers={"X-Api-Key": api_key},
    ).json()["data"]

game = get_game("a3f1d2e4-b5c6-7890-abcd-ef1234567890", "os_live_YOUR_KEY")

for market in game["markets"]:
    print(f"\n{market['market_type']}{market['period_label']}")
    for row in market["selections"]:
        pin = row["pinnacle"]
        pin_str = f"Pinnacle: {pin['odds']}" if pin else "Pinnacle: —"
        book_strs = ", ".join(
            f"{bk}: {cell['odds']}" if cell else f"{bk}: —"
            for bk, cell in row["books"].items()
        )
        print(f"  {row['selection']:20s}  {pin_str}  |  {book_strs}")
const game = await fetch(
  `https://api.oddsstream.io/api/games/${gameId}`,
  { headers: { "X-Api-Key": "os_live_YOUR_KEY" } }
).then(r => r.json()).then(r => r.data);

for (const market of game.markets) {
  console.log(`\n${market.market_type}${market.period_label}`);
  for (const row of market.selections) {
    const pin = row.pinnacle?.odds ?? "—";
    const books = Object.entries(row.books)
      .map(([bk, cell]) => `${bk}: ${cell?.odds ?? "—"}`)
      .join(" | ");
    console.log(`  ${row.selection.padEnd(20)} Pinnacle: ${pin} | ${books}`);
  }
}

Using game_id with WebSocket

Subscribe to live updates for a single game by passing game_id as a query parameter when connecting:
// Step 1: get a short-lived token (5-min TTL)
const { token } = await fetch("https://oddsstream.io/api/stream/token", {
  method: "POST",
  headers: { "X-Api-Key": "os_live_YOUR_KEY" },
}).then(r => r.json());

// Step 2: connect with token + game_id filter
const ws = new WebSocket(
  `wss://api.oddsstream.io/api/stream?token=${token}&game_id=a3f1d2e4-b5c6-7890-abcd-ef1234567890`
);

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  // msg.bookmaker is "Pinnacle" for Pinnacle changes (includes msg.liquidity)
  // msg.bookmaker is the bookmaker name for comparison book changes
  console.log(`${msg.bookmaker} changed ${msg.market_type} ${msg.selection}${msg.odds}`);
};
Combine GET /api/games/{id} on page load (full snapshot) with a game_id-filtered WebSocket connection for real-time updates — you get a complete live odds comparison view with a single REST call and one WebSocket.

WebSocket Guide

Full tutorial for building real-time odds feeds.

Value Bets

Pre-calculated +EV opportunities against Pinnacle.