Public beta · v1
Docs
One key, plain JSON over HTTPS. Every figure carries its source count and price age.
Football only · 1X2, over/under goals, both teams to score, Asian handicap · derived figures · no bookmaker names
Base URL
https://data.oddsintel.app
OpenAPI spec (OpenAPI 3.1, JSON)
On this page
Quickstart
Key in a minute, first call in 60 seconds
Free key by email, no card. Data on the free key is 2 h delayed.
Get a free key
Name, work email, one line on your use. The Sample key is emailed in a minute, with no review.
Copy it
Keys start with oid_. Keep yours on your server.
Shell
export ODDSINTEL_KEY=oid_YOUR_KEYCall today’s matches
JSON back. On a Sample key,
meta.delay_minutesis 120.First call
curl -G \ https://data.oddsintel.app/v1/fair \ -H "X-Api-Key: $ODDSINTEL_KEY" \ -d date=$(date -u +%F)
The same call in your language
# the key from your emailexport ODDSINTEL_KEY=oid_YOUR_KEY# today's matches (UTC), 1X2curl -G \ https://data.oddsintel.app/v1/fair \ -H "X-Api-Key: $ODDSINTEL_KEY" \ -d date=$(date -u +%F) -d market=1x2200Response, Sample key
{ "data": [ { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-18T14:00:00Z", "competition": "Premier League (England)", "home": "Arsenal", "away": "Chelsea", "market": "1x2", "line": null, "selection": "home", "fair_p": 0.512, "fair_odds": 1.953, "consensus_close": null, "close_ts": null, "close_is_final": false, "n_sources": 4, "price_age_s": 84, "status": "provisional", "quality_flag": "ok", "computed_at": "2026-10-18T13:52:10Z", "checked_at": "2026-10-18T13:52:10Z", "read_age_s": 84, "oldest_input_age_s": 695, "dispersion_pp": 1.8, "n_sources_band": "4+", "close_hash": null, "kickoff_source": "own" } ], "meta": { "date": "2026-10-18", "count": 1, "data_as_of": "2026-10-18T13:52:10Z", "contract_version": "v1", "plan": "sample", "delay_minutes": 120 }}Every response, before you have a key
{ "data": [ { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-18T14:00:00Z", "competition": "Premier League (England)", "home": "Arsenal", "away": "Chelsea", "market": "1x2", "line": null, "selection": "home", "fair_p": 0.512, "fair_odds": 1.953, "consensus_close": null, "close_ts": null, "close_is_final": false, "n_sources": 4, "price_age_s": 84, "status": "provisional", "quality_flag": "ok", "computed_at": "2026-10-18T13:52:10Z", "checked_at": "2026-10-18T13:52:10Z", "read_age_s": 84, "oldest_input_age_s": 695, "dispersion_pp": 1.8, "n_sources_band": "4+", "close_hash": null, "kickoff_source": "own" }, { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-18T14:00:00Z", "competition": "Premier League (England)", "home": "Arsenal", "away": "Chelsea", "market": "ou", "line": 2.5, "selection": "over", "fair_p": 0.548, "fair_odds": 1.825, "consensus_close": null, "close_ts": null, "close_is_final": false, "n_sources": 4, "price_age_s": 3912, "status": "provisional", "quality_flag": "ok", "computed_at": "2026-10-18T13:52:10Z", "checked_at": "2026-10-18T13:52:10Z", "read_age_s": 97, "oldest_input_age_s": 708, "dispersion_pp": 1.8, "n_sources_band": "4+", "close_hash": null, "kickoff_source": "own" }, { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-18T14:00:00Z", "competition": "Premier League (England)", "home": "Arsenal", "away": "Chelsea", "market": "btts", "line": null, "selection": "yes", "fair_p": null, "fair_odds": null, "consensus_close": null, "close_ts": null, "close_is_final": false, "n_sources": 2, "price_age_s": null, "status": "withheld", "quality_flag": "thin", "computed_at": "2026-10-18T13:52:10Z", "checked_at": null, "read_age_s": null, "oldest_input_age_s": null, "dispersion_pp": null, "n_sources_band": "2", "close_hash": null, "kickoff_source": "own" } ], "meta": { "date": "2026-10-18", "count": 3, "data_as_of": "2026-10-18T13:52:10Z", "contract_version": "v1", "plan": "starter" }}Examples from the OpenAPI spec, as returned to a Starter key. A Sample key’s meta also carries delay_minutes.
Plans
What your key reads
Sample
- Past dates
- Bulk history
- Today + 2 days
- 2 h delayed
- Limits
- 200/day · 5/min
Starter
- Past dates
- History since 10 Oct 2026, capped at 90 days
- Today + 2 days
- No added delay
- Limits
- 5,000/day · 60/min
Pro
- Past dates
- Full history since 10 Oct 2026
- Today + 2 days
- No added delay
- Limits
- 50,000/day · 120/min
Business
- Past dates
- Bulk history since 10 Oct 2026
- Today + 2 days
- No added delay
- Limits
- Custom limits
A key asking outside its plan’s history depth gets 403 plan_scope. Every plan returns the same fields. Full plans: pricing.
Authentication
One header
Send your key in the X-Api-Key header. Keys start with oid_. A free Sample key is emailed in a minute; paid keys after a review within 24 hours: get a key here.
A missing or unknown key gets 401; a revoked key gets 403. /v1/status needs no key.
Shell
# the key from your emailexport ODDSINTEL_KEY=oid_YOUR_KEYcurl -H "X-Api-Key: $ODDSINTEL_KEY" ...Keep the key on your server. Do not put it in a web page or an app people can download.
Endpoints
The endpoints
/v1/fair and /v1/close return the same shape: a data array with one row per match, market and selection, and a meta object. Responses are cached for up to 60 seconds. One date per call, no paging. Unknown parameters are ignored.
/v1/fair
X-Api-KeyLatest fair prices for matches kicking off on that UTC date.
Parameters
daterequiredYYYY-MM-DDKick-off date, UTC. Within your plan's history depth: Sample yesterday to 2 days ahead, Starter the last 90 days and any upcoming day, Pro and Business no cap (else 403 plan_scope).marketoptional1x2 · ou · btts · ahOnly this market. Omit for all markets. Anything else: 400 bad_request.competitionoptionaltext, ≤ 80 charactersOnly matches whose competition contains this text, case-insensitive: premier league, england.max_ageoptionalseconds, 1–86400Leave out rows before kick-off whose oldest input is older than this (oldest_input_age_s; read_age_s where it is not recorded). Closes and withheld rows stay.as_ofoptionalUTC time, ISO 8601, e.g. 2026-10-18T13:00:00ZThe rows as they stood at that time, from our history, to test a comparison at a fixed time before kick-off. Rounded down to 5 minutes; ages are measured at that time. Sample keys: never later than their delay.
History depth by plan: Sample yesterday only, Starter 90 days back, Pro and Business no cap. We hold figures since 10 Oct 2026 (/v1/status history_from), so no plan reaches before that. Sample keys also read today and the next 2 days, as the figures stood 2 hours ago (meta.delay_minutes: 120); paid keys have no added delay.
Errors: 400 bad_request · 401 missing_key / invalid_key · 403 revoked / plan_scope · 429 rate_limited · 503 data_stale / busy / not_open_yet (all errors)
curl -G \ https://data.oddsintel.app/v1/fair \ -H "X-Api-Key: $ODDSINTEL_KEY" \ -d date=$(date -u +%F) -d market=1x2Example response (Starter key)
{ "data": [ { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-18T14:00:00Z", "competition": "Premier League (England)", "home": "Arsenal", "away": "Chelsea", "market": "1x2", "line": null, "selection": "home", "fair_p": 0.512, "fair_odds": 1.953, "consensus_close": null, "close_ts": null, "close_is_final": false, "n_sources": 4, "price_age_s": 84, "status": "provisional", "quality_flag": "ok", "computed_at": "2026-10-18T13:52:10Z", "checked_at": "2026-10-18T13:52:10Z", "read_age_s": 84, "oldest_input_age_s": 695, "dispersion_pp": 1.8, "n_sources_band": "4+", "close_hash": null, "kickoff_source": "own" }, { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-18T14:00:00Z", "competition": "Premier League (England)", "home": "Arsenal", "away": "Chelsea", "market": "ou", "line": 2.5, "selection": "over", "fair_p": 0.548, "fair_odds": 1.825, "consensus_close": null, "close_ts": null, "close_is_final": false, "n_sources": 4, "price_age_s": 3912, "status": "provisional", "quality_flag": "ok", "computed_at": "2026-10-18T13:52:10Z", "checked_at": "2026-10-18T13:52:10Z", "read_age_s": 97, "oldest_input_age_s": 708, "dispersion_pp": 1.8, "n_sources_band": "4+", "close_hash": null, "kickoff_source": "own" }, { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-18T14:00:00Z", "competition": "Premier League (England)", "home": "Arsenal", "away": "Chelsea", "market": "btts", "line": null, "selection": "yes", "fair_p": null, "fair_odds": null, "consensus_close": null, "close_ts": null, "close_is_final": false, "n_sources": 2, "price_age_s": null, "status": "withheld", "quality_flag": "thin", "computed_at": "2026-10-18T13:52:10Z", "checked_at": null, "read_age_s": null, "oldest_input_age_s": null, "dispersion_pp": null, "n_sources_band": "2", "close_hash": null, "kickoff_source": "own" } ], "meta": { "date": "2026-10-18", "count": 3, "data_as_of": "2026-10-18T13:52:10Z", "contract_version": "v1", "plan": "starter" }}/v1/close
X-Api-KeyConsensus closing prices for matches on that date.
Parameters
daterequiredYYYY-MM-DDKick-off date, UTC. Within your plan's history depth: Sample yesterday to 2 days ahead, Starter the last 90 days and any upcoming day, Pro and Business no cap (else 403 plan_scope).marketoptional1x2 · ou · btts · ahOnly this market. Omit for all markets. Anything else: 400 bad_request.max_ageoptionalseconds, 1–86400Leave out rows before kick-off whose oldest input is older than this (oldest_input_age_s; read_age_s where it is not recorded). Closes and withheld rows stay.
Same rows as /v1/fair plus consensus_close. A close is final (close_is_final: true) once the match has kicked off; before that consensus_close is null.
Final closes: ask for a past date (the example below asks for yesterday). Today's matches have consensus_close null until they kick off.
No competition filter here: /v1/close ignores it (and any unknown parameter) and returns every competition for the date. Filter on the competition field.
Errors: 400 bad_request · 401 missing_key / invalid_key · 403 revoked / plan_scope · 429 rate_limited · 503 data_stale / busy / not_open_yet (all errors)
# yesterday, UTC (macOS: date -u -v-1d +%F)curl -G \ https://data.oddsintel.app/v1/close \ -H "X-Api-Key: $ODDSINTEL_KEY" \ -d date=$(date -u -d yesterday +%F) -d market=1x2Example response (Starter key)
{ "data": [ { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-18T14:00:00Z", "competition": "Premier League (England)", "home": "Arsenal", "away": "Chelsea", "market": "1x2", "line": null, "selection": "home", "fair_p": 0.515, "fair_odds": 1.942, "consensus_close": 1.942, "close_ts": "2026-10-18T14:00:00Z", "close_is_final": true, "n_sources": 4, "price_age_s": 140, "status": "confirmed_before_ko", "quality_flag": "ok", "computed_at": "2026-10-18T14:00:30Z", "checked_at": "2026-10-18T14:00:30Z", "read_age_s": 140, "oldest_input_age_s": 680, "dispersion_pp": 1.8, "n_sources_band": "4+", "close_hash": "45aff601d259b3723a1f8410f4695ce06ccf2a38efba6c9ac5499248816f9288", "kickoff_source": "own" } ], "meta": { "date": "2026-10-18", "count": 1, "data_as_of": "2026-10-18T14:00:30Z", "contract_version": "v1", "plan": "starter" }}/v1/status
no keyWhether the data is fresh. No key needed.
Parameters
None.
status is "stale" when the newest data is older than max_age_s (seconds, given in the response). The data endpoints then answer 503 data_stale.
history_from is the first kick-off date (UTC) we hold figures for. No plan reaches further back than this.
Errors: None: it answers 200 even while the keyed endpoints are closed. (all errors)
curl https://data.oddsintel.app/v1/statusExample response
{ "status": "ok", "data_as_of": "2026-10-18T13:52:10Z", "max_age_s": 600, "n_matches": 752, "history_from": "2026-10-17"}/v1/close.csv
X-Api-KeyThe /v1/close rows for one kick-off day (UTC) as a CSV file. Same parameters (date, optional market), same key, limits and history depth. One file counts as one request.
One header row, then one row per match, market and selection. The columns are the row fields, in the same order. An empty cell is null; booleans are true / false. Errors are the same JSON as /v1/close. The answer carries X-Data-As-Of (and X-Delay-Minutes on a delayed Sample day).
Download a day
curl
curl -G https://data.oddsintel.app/v1/close.csv -H "X-Api-Key: $ODDSINTEL_KEY" -d date=$(date -u -d yesterday +%F) -o close.csvCSV header row
fixture_id,kickoff,competition,home,away,market,line,selection,fair_p,fair_odds,consensus_close,close_ts,close_is_final,n_sources,price_age_s,status,quality_flag,computed_at,checked_at,read_age_s,oldest_input_age_s,dispersion_pp,n_sources_band,close_hash,kickoff_source/v1/close/bulk
X-Api-KeyStarter, Pro and Business. The /v1/close.csv rows for every kick-off day from from to to (UTC, up to 31 days), optional market, as one gzip-compressed CSV file: one header row, then each day in order. Same columns as /v1/close.csv. One file counts as one request; its rows count as rows served. Your plan’s history depth applies to from.
The file is built day by day. If something fails part-way, the file is cut and gzip reports it as damaged: download it again. At most two files are built at a time; otherwise 503 busy with Retry-After. A free Sample key answers 403 plan_scope: use /v1/close.csv, one day at a time.
curl
curl -G https://data.oddsintel.app/v1/close/bulk \ -H "X-Api-Key: $ODDSINTEL_KEY" \ -d from=2026-10-01 -d to=2026-10-31 -o closes.csv.gzgunzip closes.csv.gz/v1/close/changelog
X-Api-KeyCloses do not change once we publish them. Every published close carries close_hash, and we record it when we first serve it, then check it again every 2 minutes (older days once a day). Any close whose hash later differs is listed here, with the old and new hash, the reason and the time. An empty list since meta.ledger_from means no published close has changed. Every plan can read it; no figures, so fetch the new figure from /v1/close. Optional since (default 7 days ago), fixture_id, and after for the next page (meta.next_after, 1,000 rows a page).
Reasons: revised (another closing price at the same kick-off) · kickoff_moved (the close was taken again at the new kick-off) · withheld (the match was not played, its date is disputed, or it started early) · reopened (the kick-off moved later) · restored · removed.
Check a close you stored
Python
import hashlibdef close_hash(r): """r = a close row from /v1/close (close_is_final true, consensus_close set).""" line = "" if r["line"] is None else f"{r['line']:.2f}".replace("-0.00", "0.00") s = "|".join(["oddsintel-close-v1", r["fixture_id"], r["kickoff"] or "", r["market"], line, r["selection"], f"{r['consensus_close']:.3f}", r["close_ts"] or ""]) return hashlib.sha256(s.encode()).hexdigest()assert close_hash(row) == row["close_hash"]curl
curl -G https://data.oddsintel.app/v1/close/changelog -H "X-Api-Key: $ODDSINTEL_KEY" \ --data-urlencode since=2026-10-20T00:00:00Z/v1/webhook
X-Api-KeyPro and Business. Register one https URL (PUT with {"url": "https://..."}) and we POST a small JSON notice when a fixture’s closes become final (close.ready: fixture_id, kickoff, markets, fixture_hash) and when a published close changes (close.changed). The PUT answer shows your signing secret once: store it. GET shows the webhook and its last 20 deliveries, DELETE removes it, POST /v1/webhook/test sends a ping.
Every notice has OddsIntel-Signature: t=…,v1=…: HMAC-SHA256 of <t>.<raw body> with your secret. Check it and refuse a t over 5 minutes old. Answer 2xx within 5 seconds. Anything else is retried after 1 min, 5 min, 30 min, 2 h and 6 h, then given up; after 20 failed attempts in a row the webhook is switched off (PUT again to switch it on, with a new secret). The same notice can arrive twice: de-duplicate on its id. We never follow redirects; the URL must be a public host name.
curl -X PUT https://data.oddsintel.app/v1/webhook -H "X-Api-Key: $ODDSINTEL_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/oddsintel"}'# → data.secret = whsec_... (shown once)/v1/clv
X-Api-KeyGrade prices you took or published against the consensus close. Send a JSON body {"bets": [...]}; each bet comes back, in the order sent, with clv = odds × (1 / consensus_close) − 1, the same close /v1/close serves. For ahsend the home side’s handicap as line for both selections; on a whole or quarter line the clv is per unit of stake that was not refunded (same sign as the expected value).
One call counts as one request, whatever the batch size. Batch size: up to 10 bets on Sample, 50 on Starter, 500 on Pro and Business. Each bet needs fixture_id, market, selection and odds, plus line for ou, and taken_at (when the price was taken). Optional: ref (echoed). A price taken less than 60 seconds before kick-off, or later, is never graded, and neither is a price more than 25 % above the margin-free closing price (implausible_price). A grade is a measurement against our close, not a verification of your record.
One bad bet never fails the call. Each row has a status: graded · not_closed_yet (before kick-off) · no_consensus (under 3 independent sources at the close: no figure, never a guess) · unknown_fixture · unknown_selection (no figure for that market, line and selection) · taken_after_kickoff · plan_scope (outside your plan's window) · invalid (see message: e.g. taken_at missing) · implausible_price (more than 25 % above the margin-free closing price: no real offer sits there, not graded).
Takeable grading (on Pro: whether each price was seen at our sources when it was taken) is being finalised; until then every graded price says takeable: "unchecked". Every graded bet carries our fair price at that time, fair_at_taken_at and edge_vs_fair_at_taken_at_pct. The method, the tolerance and what unknown means: how CLV grading works.
Errors: 400 bad_request (no bets, or more than your plan’s batch size) · 401 missing_key / invalid_key · 403 revoked · 429 rate_limited · 503 data_stale / busy / not_open_yet (all errors)
curl https://data.oddsintel.app/v1/clv -H "X-Api-Key: $ODDSINTEL_KEY" -H "Content-Type: application/json" -d '{"bets":[{"ref":"tip-101","fixture_id":"8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47","market":"1x2","selection":"home","odds":2.1,"taken_at":"2026-10-18T09:30:00Z"},{"ref":"tip-102","fixture_id":"8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47","market":"1x2","selection":"draw","odds":3.6,"taken_at":"2026-10-18T09:30:00Z"},{"ref":"tip-103","fixture_id":"8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47","market":"ou","line":2.5,"selection":"over","odds":1.95,"taken_at":"2026-10-18T14:05:00Z"}]}'Example response (Starter key)
{ "data": [ { "ref": "tip-101", "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "market": "1x2", "line": null, "selection": "home", "odds": 2.1, "taken_at": "2026-10-18T09:30:00Z", "status": "graded", "clv": 0.0814, "consensus_close": 1.942, "close_fair_p": 0.5149, "n_sources": 4, "close_ts": "2026-10-18T14:00:00Z", "kickoff": "2026-10-18T14:00:00Z", "message": null, "takeable": "unchecked", "takeable_basis": null, "fair_at_taken_at": 2.05, "edge_vs_fair_at_taken_at_pct": 2.44, "takeable_reason": null, "event_id": "evt_01M5EB1T6CXE9EC1DNKW54Q9A7" }, { "ref": "tip-102", "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "market": "1x2", "line": null, "selection": "draw", "odds": 3.6, "taken_at": "2026-10-18T09:30:00Z", "status": "no_consensus", "clv": null, "consensus_close": null, "close_fair_p": null, "n_sources": 2, "close_ts": "2026-10-18T14:00:00Z", "kickoff": "2026-10-18T14:00:00Z", "message": "Fewer than 3 independent sources at the close: no figure, never a guess.", "takeable": null, "takeable_basis": null, "fair_at_taken_at": null, "edge_vs_fair_at_taken_at_pct": null, "takeable_reason": null, "event_id": "evt_01M5EB1T6CXE9EC1DNKW54Q9A7" }, { "ref": "tip-103", "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "market": "ou", "line": 2.5, "selection": "over", "odds": 1.95, "taken_at": "2026-10-18T14:05:00Z", "status": "taken_after_kickoff", "clv": null, "consensus_close": null, "close_fair_p": null, "n_sources": null, "close_ts": null, "kickoff": "2026-10-18T14:00:00Z", "message": "A price taken less than 60 seconds before kick-off, or later, is not graded against the close.", "takeable": null, "takeable_basis": null, "fair_at_taken_at": null, "edge_vs_fair_at_taken_at_pct": null, "takeable_reason": null, "event_id": "evt_01M5EB1T6CXE9EC1DNKW54Q9A7" } ], "meta": { "count": 3, "graded": 1, "data_as_of": "2026-10-19T08:00:30Z", "contract_version": "v1", "plan": "starter", "max_bets": 50, "day_bets_limit": null, "day_bets_remaining": null }}/v1/fixtures
X-Api-KeyPut our figures next to the odds feed you already use. Send the kick-off time (kickoff, ISO 8601 with a timezone) and the two team names as your feed spells them (home, away). You get up to 5 of our fixtures kicking off within 15 minutes either side, best first, each with a confidence from 0 to 1. One call counts as one request.
Join when confidence is 0.9 or more; check by eye from 0.6 to 0.9; below that, treat it as not found. If teams_reversedis true, your home team is our away team: swap home and away before you compare 1X2 prices. A women’s, U21 or reserve team never matches the first team. The kick-off day must be inside your plan’s history window.
Errors: 400 bad_request (kickoff, home or away missing or malformed) · 401 missing_key / invalid_key · 403 revoked / plan_scope · 429 rate_limited · 503 busy / not_open_yet (all errors)
curl -G https://data.oddsintel.app/v1/fixtures \ -H "X-Api-Key: $ODDSINTEL_KEY" \ --data-urlencode kickoff=2026-10-20T19:00:00Z \ --data-urlencode home="Man Utd" --data-urlencode away=ChelseaExample response (Starter key)
{ "data": [ { "fixture_id": "8f0c6a2e-4b1d-4c7a-9e3f-2d5b8a1c0e47", "kickoff": "2026-10-20T19:00:00Z", "competition": "Premier League (England)", "home": "Manchester United", "away": "Chelsea", "confidence": 1, "kickoff_delta_s": 0, "teams_reversed": false, "kickoff_source": "own", "event_id": "evt_01M5EB1T6CXE9EC1DNKW54Q9A7" } ], "meta": { "count": 1, "kickoff": "2026-10-20T19:00:00Z", "window_s": 900, "data_as_of": "2026-10-20T08:14:00Z", "contract_version": "v1", "plan": "starter", "lookup": "teams" }}Client snippets
Fair, close and status in one client
Copy one block, set ODDSINTEL_KEY, run it. It uses today’s and yesterday’s UTC dates, so it stays inside a free Sample key’s window. No SDK to install.
# pip install requestsimport os, time, datetime, requestsBASE = "https://data.oddsintel.app/v1"KEY = os.environ["ODDSINTEL_KEY"]def get(path, **params): for attempt in range(2): r = requests.get(BASE + path, params=params, headers={"X-Api-Key": KEY}, timeout=10) wait = int(r.headers.get("Retry-After", 0)) if r.status_code in (429, 503) and 0 < wait <= 60 and attempt == 0: time.sleep(wait) continue r.raise_for_status() return r.json()def day(offset=0): # UTC date, offset in days now = datetime.datetime.now(datetime.timezone.utc) return (now + datetime.timedelta(days=offset)).date().isoformat()status = get("/status") # /v1/status, no key neededfair = get("/fair", date=day(), market="1x2") # /v1/fair, todayclose = get("/close", date=day(-1), market="ou") # /v1/close, yesterdayprint(status["status"], fair["meta"]["count"], close["meta"]["count"])Fields
Every field in a row
fixture_idstring (uuid)- Stable match id.
kickoffstring (UTC time)- Kick-off time.
competitionstring- Competition and country, e.g. Premier League (England).
home / awaystring- Team names.
market1x2 · ou · btts · ah- Match result, over/under goals, both teams to score, Asian handicap (every line at least 3 independent sources quote).
linenumber | null- Goal line for ou (e.g. 2.5). For ah the home side's handicap on both selections, quarter lines as published: line -0.25 with selection away is the away side at +0.25. Null for 1x2 and btts.
selectionstring- 1x2: home / draw / away. ou: over / under. btts: yes / no. ah: home / away.
fair_pnumber | null- Consensus probability (0–1) with the bookmaker margin removed (Shin method for 1x2, power method for ou, btts and ah: standard margin-removal models). On a whole or quarter line it is the break-even probability with the push or half refund removed: 1 / fair_p is the fair price, not the chance of winning the bet outright.
fair_oddsnumber | null- Fair price in decimal odds: 1 / fair_p.
consensus_closenumber | null- The closing price: the fair price (1 / fair_p) as it stood at kick-off, in decimal odds, from at least 3 independent sources. null before kick-off.
close_tsstring | null- When the close was taken (UTC); null before.
close_is_finalboolean- true once the match has kicked off.
n_sourcesinteger- Independent sources behind the figure, capped at 4: 4 means 4 or more. Brands sharing one trading desk count once. Under 3 sources we publish no figure: the row is status "withheld" with nulls.
price_age_sinteger | null- Value age: seconds since the figure last changed, from its newest input price at that change. It keeps growing while the market does not move, even though we keep checking; for freshness use read_age_s. On a close: the newest input's age at kick-off.
statusstring- provisional (may still move) · confirmed_before_ko (taken at kick-off) · withheld (under 3 sources, inputs not read within 45 minutes, or not re-checked within 10 minutes; figures null).
quality_flagok · stale · thin- ok on every published row. On a withheld row, why: stale (3+ sources quote it, but too few prices were read in the last 45 minutes, or we have not re-checked it in the last 10 minutes) or thin (under 3 sources).
computed_atstring (UTC time)- When this row was computed.
checked_atstring (UTC time) | null- The last time we re-computed this figure from our latest reads. The figure is rewritten only when it moves, so this can be newer than computed_at. On a close: when the close was computed. null when withheld.
read_age_sinteger | null- Read age: seconds since the newest input price was last confirmed by a read, as of checked_at. Before kick-off every input was read within the last 45 minutes at checked_at; if we stop re-checking a match, it grows, and once the newest input is over 45 minutes old the row is served withheld (quality_flag stale). On a close: equals price_age_s. null when withheld.
oldest_input_age_sinteger | null- Age of the oldest input price behind the figure, in seconds (read_age_s is the newest's). It can be up to 45 minutes more than read_age_s; with 3 sources, one slow input moves the figure by a third of its lag. On a close: the oldest input's age at kick-off. null when withheld or not recorded (before 19 October).
dispersion_ppnumber | null- How much the independent sources disagree on this selection: highest minus lowest fair probability among them, in percentage points. No source is named. A gap smaller than this is noise. null when withheld or not recorded (before 19 October).
n_sources_bandstring- n_sources as a band: "3" or "4+" on a published row.
close_hashstring | null- A published close only: sha256 of the close's own fields, so you can prove later that a close you stored is the one we still serve. Recompute it from the row (see Close hash and changelog). Closes do not change; if one ever does, /v1/close/changelog lists it. null on every other row.
meta
date- The date you asked for.
count- Number of rows in data.
data_as_of- Time of the newest data behind the response (UTC).
contract_version- Always v1 on this version.
plan- The plan of the key you used: sample, starter, pro or business.
delay_minutes- Only on free Sample keys, for today and upcoming days: the figures are as they stood this many minutes ago (120). Absent otherwise.
as_of- Only when you sent as_of: the time the rows are shown as of (rounded down to 5 minutes).
max_age- Only when you sent max_age: the value applied.
Method
How each figure is made
- We read each bookmaker's own prices for the match. Bookmakers sharing a trading desk count once, as one independent source.
- Each source's margin is removed, outliers are set aside, and the rest are averaged with equal weight, one vote per independent source, into one fair probability (
fair_p).fair_odds= 1 /fair_p. - A source counts only if its full set of prices was read within the last 45 minutes. Under 3 counting sources we publish no figure: the row is withheld, with
quality_flagstaleorthin. - Before kick-off a row is provisional and is recomputed when prices move. The close (
consensus_close) is the same fair price as it stood at kick-off (1 /fair_p), withclose_tsandclose_is_final:true. - Two ages on every row.
read_age_s: how long ago the newest input price was confirmed by a read, as ofchecked_at, our last re-check.price_age_s: how long since the figure last changed. On a close both are the newest input's age at kick-off. - Before kick-off a changed figure is published at most once every 5 minutes,
fair_phas 3 decimal places andn_sourcesstops at 4 (4 or more), so no single source's price can be worked out from our figures.
Glossary
Terms in plain words
- Independent source
- One bookmaker's own trading. Bookmakers sharing a trading desk count once.
- Fair price
- The consensus of at least 3 independent sources with every bookmaker margin taken out, as a probability (fair_p) and as decimal odds (fair_odds).
- Closing price
- The fair price as it stood at kick-off (consensus_close = 1 / fair_p then). The usual yardstick for whether a price was good.
- Price age
- How long since the figure last changed (price_age_s). It is not a freshness measure: use read_age_s. A figure whose inputs we have not read within 45 minutes, or that we have not re-checked within 10 minutes, is withheld.
- Read age
- How long since the newest input price was last confirmed by a read (read_age_s), as of our last re-check (checked_at).
Errors & limits
When a call fails
Every error is JSON with a stable code and a readable message:
Error body
{ "error": "missing_key", "message": "Send your key in the X-Api-Key header."}- 400
bad_request - A parameter is missing or malformed, e.g. date not YYYY-MM-DD.
- 401
missing_key - No X-Api-Key header.
- 401
invalid_key - The key is not one we issued.
- 403
revoked - The key has been revoked.
- 403
plan_scope - The date is outside your plan's history depth (Sample: yesterday to 2 days ahead; Starter: the last 90 days and any upcoming day). The message names the window.
- 429
rate_limited - Your plan's limit is reached. Wait the Retry-After seconds.
- 503
data_stale - Our data is older than 10 minutes: we refuse rather than serve stale data.
- 503
busy - The beta is at its request limit right now. Wait the Retry-After seconds and try again.
- 503
not_open_yet - The API has not opened yet. Try again later.
Headers
X-Api-Key- Request. Your key, on every /v1/fair, /v1/close, /v1/close.csv, /v1/clv and /v1/fixtures call.
X-RateLimit-Limit- Response. Your per-minute limit.
X-RateLimit-Remaining- Response. Calls left in this minute.
Retry-After- Response, on 429 and 503 busy. Seconds to wait.
Limits
Limits count per key: per minute, and per UTC day (the daily count resets at midnight UTC; there is no daily header, a daily 429’s Retry-After counts down to midnight). Over the limit you get 429 with a Retry-After header in seconds. Responses are cached for up to 60 seconds, so polling faster than that returns the same data.
Sample
- 200 requests a day
- 5 requests a minute
- Up to 300 rows per answer
- 1 key · internal testing and evaluation only
Starter
- 5,000 requests a day
- 60 requests a minute
- Internal business use
Pro
- 50,000 requests a day
- 120 requests a minute
- Show individual figures to your users inside your own product (no resale or bulk redistribution)
Business
- Custom limits
Plans and planned prices: pricing.
Changelog
Versions and changes
The version is in the path (/v1) and in meta.contract_version. We add fields rather than change them where we can. Before any breaking change we email key holders at least 14 days ahead.
v1 — 17 October 2026: public beta
- /v1/fair, /v1/close and /v1/status; markets 1x2, ou and btts.
- Free Sample keys sent by email in a minute: past days plus today and the next 2 days on a 2-hour delay (meta.delay_minutes). Since 19 October the past part is yesterday only, and an answer holds at most 300 rows (meta.truncated).
v1 — October 2026: additions
- /v1/close.csv: one kick-off day of closing prices as a CSV file, one request.
- History depth by plan (Starter 90 days back, Pro and Business no cap); /v1/status adds history_from.
- POST /v1/clv: CLV grading of a batch of prices against the consensus close, one call = one request.
- 19 October: POST /v1/clv needs taken_at on every bet; prices taken less than 60 seconds before kick-off, or more than 25 % above the closing price (implausible_price), are not graded; every graded bet carries takeable: "unchecked". /v1/status adds stale_reason, engine and inputs.
- 19 October: two fields at the end of every row, checked_at and read_age_s (when we last re-checked a figure, and how old its newest read was then). price_age_s is unchanged and now documented as the value age: time since the figure last changed. The CSV gains the same two columns at the end.
- 19 October: three more fields at the end of every row (and CSV column): oldest_input_age_s (age of the oldest input behind a figure), dispersion_pp (how much the sources disagree, no source named) and n_sources_band. n_sources now stops at 4 (4 or more), fair_p has 3 decimal places, and a figure before kick-off changes at most once every 5 minutes. New parameters: max_age (leave out figures with an old input) and, on /v1/fair, as_of (the figures as they stood at a past time).
- 20 October: four fields at the end of every POST /v1/clv row: takeable_basis, fair_at_taken_at, edge_vs_fair_at_taken_at_pct (our fair price at taken_at, and your price against it) and takeable_reason. Takeable grading (on Pro: whether each price was seen at our sources when it was taken) is being finalised; until then every graded price says takeable: "unchecked". How CLV grading works.
- 20 October: Asian handicap (market ah) on /v1/fair, /v1/close, /v1/close.csv and POST /v1/clv: every line at least 3 independent sources quote, the home side’s handicap on both selections. Takeable grading does not cover ah (takeable_reason market).
- 20 October: GET /v1/close/bulk (up to 31 kick-off days of closes as one gzip CSV file, Starter and up); close_hash at the end of every row and CSV (sha256 of a published close’s own fields) with GET /v1/close/changelog (every published close that changed later, with old and new hash); the close-ready webhook for Pro and Business (PUT /v1/webhook, signed notices).
- 19 October: GET /v1/fixtures finds our fixture id from your feed’s kick-off time and team names, with a confidence score. A figure we have not re-checked for 10 minutes is now withheld too (stale), like one whose inputs are over 45 minutes old.
Support
Ask a person
Email [email protected]: we aim to answer within 2 business days on Starter and Pro, 1 on Business (service levels). Include your key’s first characters (never the whole key) and the request you sent.
Generating a client? Import the OpenAPI 3.1 spec into Postman, Insomnia or any OpenAPI code generator.
Using an AI coding assistant? Point it at llms.txt: the whole API on one plain-text page.