Skip to content

Public beta · v1

Advanced endpoints

Bulk files, the close changelog, webhooks, CLV grading and fixture matching. The quickstart, plans, fields and errors are on the main docs page.

GET

/v1/close/bulk

X-Api-Key

Starter, 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. In pandas, pd.read_csv("closes.csv.gz") reads the file directly.

curl

curl -G https://data.oddsintel.app/v1/close/bulk \  -H "X-Api-Key: $ODDSINTEL_KEY" \  -d from=2026-10-10 -d to=2026-10-31 -o closes.csv.gzgunzip closes.csv.gz
GET

/v1/close/changelog

X-Api-Key

Closes 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
PUT · GET · DELETE

/v1/webhook

X-Api-Key

Pro 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)
POST

/v1/clv

X-Api-Key

Grade 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  }}
GET

/v1/fixtures

X-Api-Key

Put 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=Chelsea

Example 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"  }}