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 stablegame_id UUID derived from (competition, match_name):
game_id = md5(competition + '::' + lower(trim(match_name)))::uuid
GET /api/games
List all games currently in the odds database.Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
sport | string | — | Filter by sport name (e.g. Football, Basketball) |
competition | string | — | Filter by competition code (e.g. EPL, NBA) |
bookmaker | string | — | Only games where this bookmaker has odds |
is_live | boolean | — | true = in-play only, false = pre-match only |
limit | integer | 200 | Max 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
| Field | Type | Description |
|---|---|---|
game_id | UUID string | Stable deterministic ID for this game |
match_name | string | Match name as stored in odds database |
competition | string | Competition code (e.g. EPL, NBA, LIG1) |
sport | string | Sport name derived from competition code |
match_date | ISO-8601 | null | Scheduled kick-off time (UTC) |
is_live | boolean | Whether any bookmaker has this as live right now |
bookmaker_count | integer | Number of bookmakers with active odds |
last_scraped_at | ISO-8601 | null | Most 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
| Parameter | Description |
|---|---|
game_id | UUID from GET /api/games |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
market_type | string | — | Filter to one market type (e.g. moneyline, total) |
period | integer | — | Filter to one period (0=full-time, 1=first-half) |
bookmaker | string | — | Include 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-leveldata:
| Field | Type | Description |
|---|---|---|
game_id | UUID string | Stable game identifier |
match_name | string | Match name |
competition | string | Competition code |
sport | string | Sport name |
match_date | ISO-8601 | null | Kick-off time (UTC) |
is_live | boolean | Whether the game is currently live |
all_bookmakers | string[] | All bookmakers with odds for this game |
markets | MarketMatrix[] | All markets in matrix format |
MarketMatrix object:
| Field | Type | Description |
|---|---|---|
market_type | string | Market type (moneyline, spread, total, player_prop, etc.) |
period | integer | Period (0=full-time, 1=first-half, 2=second-half) |
period_label | string | Human-readable period (e.g. "Full Time", "1st Half") |
bookmakers | string[] | Bookmakers with odds for this specific market |
pinnacle_scraped_at | ISO-8601 | null | When Pinnacle’s prices were last updated for this market |
selections | MatrixRow[] | One row per outcome |
MatrixRow object:
| Field | Type | Description |
|---|---|---|
selection | string | Outcome name (e.g. "PSG", "Over 2.5", "Draw") |
pinnacle | PinnacleCell | null | Pinnacle’s reference price |
books | Record<bookmaker, BookOddsCell | null> | Each bookmaker’s price (null = no odds available) |
PinnacleCell:
| Field | Type | Description |
|---|---|---|
odds | float | null | Decimal odds |
liquidity | float | null | Pinnacle max bet — proxy for line confidence |
scraped_at | ISO-8601 | null | When this price was scraped |
BookOddsCell:
| Field | Type | Description |
|---|---|---|
odds | float | null | Decimal odds |
scraped_at | ISO-8601 | null | When this price was scraped |
url | string | null | Direct 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 passinggame_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.