OddTrace API
Live and pre-match odds from every bookmaker we track, surebets and live scores, as JSON — the same data as the website, in real time.
Quick start
Get your key on your account page (Pro plan and above), then:
curl -H "x-api-key: YOUR_KEY" "https://oddtrace.com/v1/odds?sport=soccer&limit=20"
The key can also be passed as a query parameter ?apiKey=YOUR_KEY (handy for EventSource). Keep it secret; regenerate it from your account if it leaks. All times are UTC (ISO 8601). Prices are decimal odds.
Machine-readable description (OpenAPI 3): /openapi.json — generate a client in your language.
Plans and limits
| Plan | Requests per month | Per minute | Real-time stream (WebSocket, SSE) | Odds history (90 days) |
|---|---|---|---|---|
| Pro | 10,000 | 60 | No | No |
| Developer | 500,000 | 600 | Yes | Yes |
| Business | 5,000,000 | 3,000 | Yes | Yes |
Every request counts as one, including opening a stream (messages received on an open stream are not counted). List responses return at most 500 items; page with offset. The quota resets on the 1st of each month (UTC). You get an email at 80% and 100%.
| Response header | Meaning |
|---|---|
x-requests-used | requests used this month |
x-requests-remaining | requests left this month |
x-quota-reset | when the monthly quota resets |
x-ratelimit-remaining | requests left in the current minute |
Endpoints
| Route | What it returns |
|---|---|
| GET /v1/sports | Sports with matches right now: number of matches, competitions, bookmakers. |
| GET /v1/bookmakers | Bookmakers we track: key, display name, feed status (up, stale, down), number of priced matches, logo URL. |
| GET /v1/markets | The market catalogue: keys, outcomes, whether a line (point) is required. |
| GET /v1/events | Matches by kick-off time, with live score when in play. |
| GET /v1/odds GET /v1/sports/{sport}/odds | Main markets (1x2, moneyline, spreads, totals) of many matches at once. |
| GET /v1/events/{id}/odds | Every market of one match. |
| GET /v1/events/{id}/history | Every price change of a match over the last 90 days (Developer and Business). |
| GET /v1/scores | Live matches: score, period, clock. |
| GET /v1/arbs | Surebets right now, best profit first. |
| GET /v1/status | Feed health and the status of each bookmaker. |
| GET /v1/stream/sse WS /v1/stream | Real-time stream: snapshot, then every change (Developer and Business). |
Common filters
sport (e.g. soccer, tennis, icehockey — see /v1/sports), competition, live (true / false), limit, offset. Odds routes also take markets and bookmakers (comma-separated keys).
GET /v1/events
[{"id": "ev_67d7b6ab23f8", "sport": "soccer", "competition": "en_premier_league",
"commence_time": "2026-11-08T15:00:00+00:00", "home_team": "Arsenal", "away_team": "Chelsea",
"live": true, "bookmakers": ["pinnacle", "betclic", …],
"score": {"home": 1, "away": 0}, "period": "2H", "clock": "67'", "state_at": "2026-11-08T16:22:10.512Z"}]
score, period and clock appear only for live matches whose state we know. Periods: 1H, HT, 2H, ET, PEN, Q1–Q4, P1–P3, S1–S5 (sets), OT.
GET /v1/odds
[
{
"id": "ev_67d7b6ab23f8", "sport": "soccer", "competition": "en_premier_league",
"commence_time": "2026-11-08T15:00:00+00:00", "home_team": "Arsenal", "away_team": "Chelsea",
"live": false, "reliable": true,
"best": [{"market": "1x2", "outcome": "home", "price": 2.18, "bookmaker": "pinnacle"},
{"market": "totals", "point": 2.5, "outcome": "over", "price": 1.95, "bookmaker": "betclic"}, …],
"bookmakers": [
{"key": "pinnacle", "status": "up", "delayed": false, "last_update": "2026-11-02T01:49:51.338Z", "markets": [
{"key": "1x2", "outcomes": [
{"name": "home", "price": 2.18, "suspended": false, "updated_at": "2026-11-02T01:49:51.338Z"},
{"name": "draw", "price": 3.55, "suspended": false, "updated_at": "2026-11-02T01:49:51.338Z"},
{"name": "away", "price": 3.62, "suspended": false, "updated_at": "2026-11-02T01:49:51.338Z"}]},
{"key": "totals", "point": 2.5, "outcomes": [{"name": "over", "price": 1.91, …}, {"name": "under", "price": 1.99, …}]}
]}
]
}
]
best = best price per outcome across bookmakers. delayed = true on a bookmaker while its live feed is interrupted: its prices may be several minutes old and are left out of best and of surebets. reliable = false while our feed resynchronises (a few seconds). A bookmaker status other than up means its prices may be stale. A suspended price is shown but cannot be bet.
GET /v1/events/{id}/odds
Same shape as one item of /v1/odds, with every market (all lines of totals and handicaps, halves and periods, corners, cards, player props when offered). Asking for a match triggers a fresh full read at each bookmaker; the response says which were refreshed in refresh.
GET /v1/events/{id}/history
Price changes of one match, oldest first, kept 90 days. Filters: market, bookmaker, outcome, point, since and until (ISO 8601), limit (at most 5,000 rows per call; continue with since = last ts). Developer and Business plans.
curl -H "x-api-key: YOUR_KEY" "https://oddtrace.com/v1/events/ev_67d7b6ab23f8/history?market=1x2&bookmaker=pinnacle"
[{"ts": "2026-11-02T12:00:01.000Z", "market": "1x2", "outcome": "home", "point": null, "bookmaker": "pinnacle",
"price": 2.01, "suspended": false, "removed": false}, …]
removed = the bookmaker stopped offering that price at that moment. A row is written only when a price changes.
GET /v1/arbs
Filters: min_profit (0.01 = 1%), sport, live, bookmakers (only surebets whose every leg is at one of these).
[{"id": "arb_3f2a9c1d0b7e", "event_id": "ev_67d7b6ab23f8", "sport": "soccer", "competition": "us_mls",
"home_team": "Los Angeles FC", "away_team": "Minnesota United", "commence_time": "2026-11-03T03:30:00+00:00",
"live": false, "market": "1x2", "point": null, "profit": 0.0529,
"legs": [{"market": "1x2", "outcome": "home", "bookmaker": "coral", "price": 1.98, "net_price": 1.98, "age_s": 3.1},
{"market": "1x2", "outcome": "draw", "bookmaker": "leode", "price": 4.4, "net_price": 4.4, "age_s": 8.0},
{"market": "1x2", "outcome": "away", "bookmaker": "leode", "price": 4.6, "net_price": 4.6, "age_s": 8.0}],
"found_at": "2026-11-02T01:40:12.004Z", "updated_at": "2026-11-02T01:41:02.551Z"}]
profit is the calculated return on the total stake at the listed prices. Stake each leg i as bankroll × (1/net_price_i) / Σ(1/net_price). net_price is the price after exchange commission. age_s = seconds since the bookmaker last confirmed that price. Only fresh prices are used (10 s live, 120 s pre-match), and legs far from the market are filtered out as likely data errors.
GET /v1/bookmakers · GET /v1/scores
[{"key": "pinnacle", "name": "Pinnacle", "status": "up", "delayed": false, "since": "2026-11-02T01:20:03.112Z", "events": 1520, "logo": "/static/books/pinnacle.com.png"}]
[{"id": "ev_…", "sport": "basketball", "competition": "us_nba", "commence_time": "…", "home_team": "…", "away_team": "…",
"score": {"home": 78, "away": 74}, "period": "Q3", "clock": "4:12", "state_source": "betclic", "state_at": "…"}]
Real-time streams (Developer and Business)
Two transports, same content. Pick SSE for simplicity (plain HTTP, works with curl, EventSource, proxies), WebSocket if your stack prefers it. Filters: sport, events (comma-separated match ids). A Pro key is refused (HTTP 403 for SSE, close code 4403 for WebSocket).
curl -N -H "x-api-key: YOUR_KEY" "https://oddtrace.com/v1/stream/sse?sport=soccer" wss://oddtrace.com/v1/stream?apiKey=YOUR_KEY&sport=soccer
Messages
| type | Content |
|---|---|
snapshot | First message: seq, reliable, bookmakers (status), events, odds (every price: event_id, market, outcome, point, bookmaker, price, suspended, updated_at). Build your state from it. |
price | One price changed: same fields as a snapshot price, plus removed (true = the bookmaker no longer offers it). |
event | A match appeared or changed (kick-off, live, score). |
event_removed | A match ended or was removed: drop its prices. |
bookmaker_status | A bookmaker went up, stale or down. |
Every message carries seq, increasing by one. In SSE, seq is also the event id, the message type is the event name, and a : keep-alive comment is sent every 15 s without traffic.
Reconnecting
If your client falls too far behind, the server ends the stream (SSE event: close, WebSocket close code 1013). Reconnect: you get a fresh snapshot — replace your state with it. Also reconnect if you see a gap in seq, or after our feed resynchronises (you receive a new snapshot).
Markets
Market key = base[_period]. Bases include: 1x2, btts, btts_both_halves, btts_totals, clean_sheet_away, clean_sheet_home, correct_score, double_chance, double_chance_btts, double_chance_totals, draw_no_bet, exact_goals, exact_goals_away, exact_goals_home, first_team_to_score, goal_both_halves, goals_range, half_time_full_time, highest_scoring_half, last_team_to_score, moneyline, odd_even, odd_even_away, odd_even_home, race_to, result_btts, result_totals, score_both_halves_away, score_both_halves_home, spreads, spreads_3way, team_to_score_away, team_to_score_home, team_totals_away, team_totals_home, to_qualify, totals, win_both_halves_away, win_both_halves_home, win_either_half_away, win_either_half_home, win_from_behind_away, win_from_behind_home, win_to_nil_away, win_to_nil_home, winning_margin. Periods: f5, h1, h2, ot, p1, p2, p3, q1, q2, q3, q4, reg, set1, set2, set3, set4, set5 (for example totals_h1 = first-half total). In ice hockey and other sports with overtime, totals, spreads and moneyline include overtime; the regular-time versions end with _reg (1x2 is always regular time). In tennis, totals/spreads count games; set lines are totals_sets/spreads_sets. A handicap point is the home team's. Outcomes: home, draw, away, over, under, yes, no, home_draw… — full list on /v1/markets.
Code examples
Python — best prices of today's soccer, all pages
import requests
API, KEY = "https://oddtrace.com", "YOUR_KEY"
s = requests.Session()
s.headers["x-api-key"] = KEY
offset, matches = 0, []
while True:
r = s.get(f"{API}/v1/odds", params={"sport": "soccer", "limit": 500, "offset": offset})
r.raise_for_status()
page = r.json()
matches += page
if len(page) < 500:
break
offset += 500
print(len(matches), "matches, requests left:", r.headers["x-requests-remaining"])
for m in matches[:5]:
print(m["home_team"], "-", m["away_team"], [(b["outcome"], b["price"], b["bookmaker"]) for b in m["best"]])
Python — surebets above 1%
for a in s.get(f"{API}/v1/arbs", params={"min_profit": 0.01}).json():
print(f"+{a['profit']:.2%}", a["home_team"], "-", a["away_team"], a["market"],
[(l["outcome"], l["price"], l["bookmaker"]) for l in a["legs"]])
Python — SSE stream
import json, requests
with requests.get(f"{API}/v1/stream/sse", params={"sport": "tennis"}, headers={"x-api-key": KEY}, stream=True) as r:
event, prices = None, {}
for line in r.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
msg = json.loads(line[6:])
if event == "snapshot":
prices = {(o["event_id"], o["market"], o["outcome"], o["point"], o["bookmaker"]): o["price"] for o in msg["odds"]}
elif event == "price":
key = (msg["event_id"], msg["market"], msg["outcome"], msg["point"], msg["bookmaker"])
if msg["removed"]:
prices.pop(key, None)
else:
prices[key] = msg["price"]
JavaScript (browser or Node 18+) — SSE with EventSource
const es = new EventSource("https://oddtrace.com/v1/stream/sse?apiKey=YOUR_KEY&sport=soccer");
const prices = new Map();
const key = (o) => `${o.event_id}|${o.market}|${o.outcome}|${o.point}|${o.bookmaker}`;
es.addEventListener("snapshot", (e) => {
prices.clear();
for (const o of JSON.parse(e.data).odds) prices.set(key(o), o.price);
});
es.addEventListener("price", (e) => {
const o = JSON.parse(e.data);
if (o.removed) prices.delete(key(o)); else prices.set(key(o), o.price);
});
es.addEventListener("close", () => es.close()); // then reconnect after a short delay
Only put apiKey in a URL from your own servers or private tools: a key in a public web page can be copied.
JavaScript — fetch
const r = await fetch("https://oddtrace.com/v1/scores?sport=basketball", { headers: { "x-api-key": "YOUR_KEY" } });
console.log(r.headers.get("x-requests-remaining"), await r.json());
Errors
Errors are JSON: {"error": "message"}.
| Status | Meaning |
|---|---|
| 401 | Missing or invalid key. |
| 403 | Your plan does not include this (no API on Free; streams from Developer). |
| 404 | Unknown match. |
| 503 | Odds history temporarily unavailable (live data is not affected). |
| 422 | Invalid parameter (for example a non-main market on a list route). |
| 429 | Too many requests this minute, or monthly quota used up. Wait for Retry-After seconds. |
Changelog
- 2 Nov 2026 — API keys for subscribers, monthly quotas and plans. New:
/v1/stream/sse,/v1/arbs,/v1/bookmakers,/v1/scores,/v1/events/{id}/history(90 days). Error messages in English. Ice hockey regular-time lines moved to_reg; tennis set lines to_sets.
OddTrace compares odds published by bookmakers; we are not a bookmaker. 18+.