# 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](https://oddsrelay.io/contact) 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](https://oddsrelay.io/contact) 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

Source: https://oddsrelay.io/docs/guides/historical-odds · the API contract: https://api.oddsrelay.io/v2/openapi.json
