DocsGuidesView as Markdown

Historical odds

A past board, one event's price changes, closing lines and day files: the five calls, how they are priced, the archive's window and the errors.

Before you start

Historical odds hold every bookmaker price change the UK & Ireland raw feed has served since 17 October 2026. They answer at the live feed's base URL, https://api.oddsrelay.io, on a key that includes the Historical odds product. A key without it is refused with 403 not_in_key_scope.

Every route is GET. region is uk or left out; any other region is 404 unknown_region. Instants are ISO-8601 to the millisecond, and one without a zone is read as UTC.

Historical odds carry bookmaker prices only. Naming an exchange in exchanges, or sending exchangesOnly=true, is 403 exchange_history_unavailable.

On a self-serve plan, history is for your own use: your models, your research, your team. Publishing it, reselling it and pulls bigger than a plan's pool go through an Enterprise contract.

The archive's window

The archive starts on 17 October 2026 and runs to the end of the previous UTC day: each day is added overnight, once it has ended. An instant or a day after the newest one the archive holds, or before its floor, is 400 date_out_of_range, and the error's earliest and latest give the window you can ask for. An instant with no board recorded at or before it is 404 no_history.

The board at a past instant

GET /v2/historical/odds/raw returns the raw board as it stood at date: one page of up to 250 events, the same event objects as /v2/odds/raw, with limit and cursor as there. date and sports are required (400 missing_param without them), and bookmakers, markets and eventIds narrow it. It is served gzip only, so send Accept-Encoding: gzip, as the examples' --compressed does.

meta adds historical: true, timestamp (the board version at or before date), previous_timestamp, next_timestamp (the next change, or where a gap in the recording begins; null at the archive's end) and complete, false inside a gap. competition_key is always null in history: the competition register is not recorded.

A free quote for a past football board

curl --compressed -s -H "x-api-key: $ODDSRELAY_KEY" \
  "https://api.oddsrelay.io/v2/historical/odds/raw?region=uk&sports=soccer&date=2026-10-17T19:40:00Z&quote=true"
# {"cost": …, "remaining": …, "rate_card": "…"}

One event's price changes

GET /v2/historical/events/{id}/odds returns one event between from (required) and to. to defaults to 24 hours after from, or the newest instant if that is sooner, and a window spans at most 24 hours. An id merged into another event answers as the event it became.

First come the prices at from, then one row for each outcome every time its price moved. A row is timestamp, event_id, bookmaker, market, outcome, price and last_update. removed: true means the outcome left the board, and index appears only on the second and later outcome sharing a name in one market, counting from 1. meta.gaps lists any gap in the recording inside the window, each with its timestamp and why.

limit (at most 5,000 rows, the default) and cursor page through the rows. A page never splits one version of the board, so it can run a few rows past limit.

An event's afternoon

curl --compressed -s -H "x-api-key: $ODDSRELAY_KEY" \
  "https://api.oddsrelay.io/v2/historical/events/$EVENT_ID/odds?region=uk&from=2026-10-17T13:00:00Z&to=2026-10-17T19:45:00Z"

Closing lines

GET /v2/historical/closing returns the events that kicked off on one UTC day, date as YYYY-MM-DD, for the sports you name (required), nested as event, bookmakers, markets and outcomes. bookmakers and markets narrow it, and limit and cursor page through the events, up to 50 a page, by sport (the file's sport, alphabetically), then kick-off, then id.

Each outcome carries the fields the closing file holds, named as the file names them:

  • name, and ord, which numbers outcomes sharing a name in one market from 0
  • open_at and open_price: when the outcome was first seen in the window its file was cut from, and its price then
  • close_at and close_price: its last price at or before kick-off, and the instant of its last change at or before kick-off (00:00 UTC, when the file opens, if it did not change that day)
  • high, low and moves: the highest and lowest price before kick-off, and how many times the price moved
  • first_seen and last_seen: the first and last time the outcome was seen on its kick-off day (first_seen is the same instant as open_at)
  • inplay_seen: whether it was still on the board after kick-off
  • open_back_price, open_lay_price, open_available and the same three after close_: an exchange's back, lay and amount available, null while historical odds carry bookmakers only

One day's football closing lines, match odds only

curl --compressed -s -H "x-api-key: $ODDSRELAY_KEY" \
  "https://api.oddsrelay.io/v2/historical/closing?region=uk&sports=soccer&date=2026-10-17&markets=h2h"

Day files

GET /v2/historical/files lists the files, free. Filter it with tier (closing, 60s and changes, cut every night; 5s and 1s, cut by arrangement and listed only where a file exists), sports, and from and to as UTC days. A file's sport is the sport key's base: soccer, horse_racing, ice_hockey. A day file holds every bookmaker and market of its sport, so an account whose scope narrows Historical odds, or whose plan limits its bookmakers, is refused on both files routes with 403 files_need_whole_scope: contact us to arrange files.

Download a file at its url, GET /v2/historical/files/{tier}/{sport}/{day}.csv.gz. It comes as application/gzip, with an ETag: send it back as If-None-Match and a second download of the same file is a free 304. A day file never changes once cut, so it comes with Cache-Control: private, max-age=86400, immutable; a re-cut changes its ETag. Downloads are sent at a capped rate, one download at a time per account (and only a few across all accounts); one more is refused at once with 429 too_many_downloads and Retry-After: wait that many seconds and retry. A download is charged in full when it starts: one you cut off keeps its charge.

A 60s or changes file holds one row per outcome change, sorted by ts, with the columns ts, ts_ms, event_id, sport_key, sport_title, commence_time, home_team, away_team, bookmaker, bookmaker_title, bookmaker_region, is_exchange, market, outcome, ord, price, back_price, lay_price, available, last_update and op. op is image for the outcome as the file opens (each file starts with the whole board at 00:00 UTC, so one day stands alone), set when its price moved or it appeared, and remove when it left the board. In 60s, an outcome has at most one row a minute, as it stood at that minute's end. A closing file holds the closing call's fields, one row per outcome.

List a day's files, then download one

# 1. The files for one day: each entry's name, tier, sport, day, size in bytes, url and cost in tokens (free)
curl --compressed -s -H "x-api-key: $ODDSRELAY_KEY" \
  "https://api.oddsrelay.io/v2/historical/files?region=uk&tier=closing&sports=soccer&from=2026-10-17&to=2026-10-17"

# 2. Download one by its url: gzipped CSV, saved as it comes
curl --compressed -s -H "x-api-key: $ODDSRELAY_KEY" -o closing_soccer_2026-10-17.csv.gz \
  "https://api.oddsrelay.io/v2/historical/files/closing/soccer/2026-10-17.csv.gz"

What history costs

History spends the account's one token pool, the live feed's. A page of the board, event or closing call that holds data costs that call's price per page, and a file costs its price for each started megabyte of its size. Filters narrow what a page holds, not its price. /pricing publishes the four prices from GET /v2/pricing.

Add quote=true to any of the four priced calls and it returns {cost, remaining, rate_card} instead, free. Listing the files, 304 replies, replies with nothing in them and every refusal are free too.

Errors

The live feed's errors apply as they do there: 401 for the key, 402 insufficient_tokens, 429 for the rate limit. History adds or leans on these:

  • 400 date_out_of_range: the instant or day is outside the archive; earliest and latest give the window
  • 400 missing_param: date, sports or from is missing
  • 400 bad_request: an unknown or repeated parameter, a malformed instant, a tier that is not one of the list, or a board call without gzip
  • 403 exchange_history_unavailable: an exchange was asked for, and historical odds carry bookmakers only
  • 403 files_need_whole_scope: on either files route, your account's scope narrows Historical odds or your plan limits its bookmakers. A day file holds every bookmaker and market of its sport, so contact us to arrange files
  • 403 not_in_key_scope: the key does not include Historical odds
  • 403 sport_not_in_plan, market_not_in_plan or bookmaker_not_in_plan: a value outside your account's scope; valid lists the ones it allows
  • 404 no_history: no board was recorded at or before the instant
  • 404 unknown_event: no event by that id in the window
  • 404 not_found: no file by that name; list them first
  • 429 too_many_downloads: a file download past the download cap; wait the Retry-After seconds

Every parameter: API reference.