Skip to content

API reference

Integrate in an afternoon.

The OddsRelay feed is one keyed, versioned REST API: get a key, send a GET, receive matched opportunities from api.oddsrelay.io.

Quickstart

1. Get a key. Start a free trial. Your or_test_* key serves the full UK product (all 6 feed types) for 14 days. Live keys are or_live_*.

2. Make one call. Ask for matched standard opportunities in the UK:

curl -s --compressed "https://api.oddsrelay.io/v2/odds/standard?region=uk" \
  -H "Authorization: Bearer <YOUR_API_KEY>"

3. Poll efficiently. Prices re-poll on a ~3s pre-match cycle and the board re-bakes continuously, so never re-download what hasn't changed: always send Accept-Encoding: gzip (the payload is large) and If-None-Match with the last ETag; together they collapse most polls to a tiny 304 Not Modified.

Poll every 15 to 30 seconds. Responses carry Cache-Control: private, max-age=15, and a given board typically changes every 15 to 25 seconds, so polling faster returns the same ETag repeatedly while still spending quota: every request counts against your cap, including a 304.

Conditional polling (304)
# 1. First poll: capture the ETag
ETAG=$(curl -sD - -o /dev/null --compressed \
  "https://api.oddsrelay.io/v2/odds/standard?region=uk" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r')

# 2. Subsequent polls: send it back. Until the board rolls you get a
#    bodyless 304 Not Modified (fast + free), otherwise a fresh 200 + new ETag.
curl -s -o /dev/null -w "%{http_code}\n" --compressed \
  "https://api.oddsrelay.io/v2/odds/standard?region=uk" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "If-None-Match: $ETAG"

Coding with AI?

Paste this into Claude, or any coding agent, along with your key and what you want built. It carries the whole contract: the endpoint, both auth forms, the exact response shape, the polling rules and the error codes. The last line is where you say what to build.

Prompt · copy the whole thing
You are integrating the OddsRelay odds API. Follow this contract exactly.

BASE URL
  https://api.oddsrelay.io/v2

AUTH
  Send the key as a header, never in a query string:
    Authorization: Bearer <YOUR_API_KEY>
  (x-api-key: <YOUR_API_KEY> is equivalent.)
  Keys look like or_live_* (production) or or_test_* (14-day trial).
  Read the key from an environment variable. Never commit it, never ship it to a browser
  bundle: browser embeds need a separate origin-locked widget key.

THE CALL
  GET /v2/odds/standard?region=uk
  Query params:
    region      one region code, or a CSV like uk,ire (max 8). Your key's scope gates which.
    format      envelope (default) or bare. bare returns the data array alone and moves the
                metadata into X-Count / X-Processed-At / X-Stale-Hidden headers.
    oddsFormat  decimal (default) or american.
    dateFormat  iso (default) or unix.
  Feed types are the path segment: standard, 2up, bog, each-way, extra-place, dutching.

RESPONSE SHAPE (event-grouped; this is the part to model carefully)
  { "meta": { "feed_type", "region", "odds_format", "processed_at", "count",
              "stale_hidden", "unreliable_links", "version", "next_cursor" },
    "data": [ {
      "event_id":      "stable across polls and across every /v2 endpoint",
      "sport_key":     "soccer_premier_league",
      "sport_title":   "Premier League",
      "home_team":     "Arsenal",          // team sports
      "away_team":     "Chelsea",          // racing sends "venue" instead;
                                            // "event_name" is the fallback when neither splits
      "commence_time": "2030-07-08T14:00:00Z",
      "markets": [ {
        "key": "h2h",                      // also: totals, win, each_way, btts
        "outcomes": [ {
          "name":  "Arsenal",
          "point": 2.5,                    // present on totals only
          "back":  [ { "bookmaker": "sky_bet", "price": 2.10,
                       "link": "https://...", "last_update": "..." } ],
          "lay":   [ { "exchange": "smarkets", "price": 2.05, "available": 1840.0,
                       "link": "https://...", "last_update": "..." } ]
        } ]
      } ]
    } ] }

  Rules that matter:
  - back[] is sorted best (highest) price first; lay[] is sorted best (lowest) price first,
    deduped to one offer per exchange. Do not re-sort before pairing.
  - An outcome may legally have no lay offers (dutching feed types omit the lay key entirely),
    an empty back array, or neither. Every level can be empty. Walk defensively.
  - "link" is a deep link to that event at that bookmaker or exchange. It can be null.
  - each-way and extra-place replace lay[] with a { "win": [...], "place": [...] } pair, and
    their back offers add "places" and "place_fraction".
  - To build a matched-betting board, fan each outcome out to one row per (back offer x lay
    offer). Pairing only against the best lay hides real opportunities.

MONEY MATHS IS YOURS, NOT THE WIRE'S
  The response carries source facts only. There is no rating, profit, qualifying-loss or
  commission field, by design: commission is the viewer's own exchange account rate.
  Compute derived figures client-side, for example:
    rating        = (backOdds / layOdds) * 100          // 0% commission; >100 is an arb
    layStake      = (backOdds * backStake) / (layOdds - commission/100 * (layOdds - 1))
    liability     = layStake * (layOdds - 1)
  Let the user set their own commission per exchange; do not hardcode one.

POLLING
  Poll every 15 to 30 seconds. Store the ETag from each response and send it back as
  If-None-Match; a 304 means the board is unchanged and you should keep the rows you have.
  Always send Accept-Encoding: gzip — the full board is large.
  Every request counts against the rate limit, including a 304, so do not poll faster than
  the board changes. Read your budget from X-RateLimit-Remaining and X-RateLimit-Remaining-Hour.
  On 429, honour the Retry-After header.

ERRORS
  Every error is { "error": { "code", "message", "type"?, "request_id" } }.
    401  invalid_api_key / key_revoked / key_expired   -> stop and surface it; do not retry.
    403  type_not_in_scope / region_not_in_scope / raw_not_in_scope   -> a scope problem;
         "type" names the failing axis. Do not retry.
    404  unknown_region / unknown_sport                -> a bad request value.
    429  rate_limited                                  -> back off, honour Retry-After.
    500 / 503                                          -> transient; retry with backoff.
  Log request_id on failure; it identifies the request in support.

WHAT TO BUILD
  [Describe what you want here: for example "a matched-betting board sorted by rating, with a
  search box and a per-exchange commission setting", or "a service that pulls the board every
  20 seconds and stores the best back price per outcome".]

Two things models get wrong here, so the prompt states both: commission is never on the wire (it is the viewer's own exchange rate), and a matched board needs one row per back-and-lay pairing rather than per best lay. If your agent produces ratings that look low, or a board that feels thin, check those first.

Authentication

Every request needs a key, sent as a Bearer token or an x-api-key header. Keys are 256-bit, shown once at issue, and stored hashed. A key is never accepted in a query parameter, where it would leak into logs.

Headers
# Preferred: Bearer
Authorization: Bearer <YOUR_API_KEY>

# Equivalent: x-api-key header
x-api-key: <YOUR_API_KEY>

# NEVER in a query string (keys leak into logs). This is rejected:
# https://api.oddsrelay.io/v2/odds/standard?api_key=...   ✗

A key carries a scope across four axes: feed types, regions, form (processed/raw) and add-ons. Requests outside scope return a 403 (see Errors).

The region filter

One endpoint set, one filter: ?region=uk (the default), or a comma-separated list like ?region=uk,ire. There is no endpoint-per-region. Your key's scope gates which regions it may request; call GET /v2/regions to discover them.

regionMarketStatus
ukUnited KingdomRequestable, served today.
ireIrelandRequestable, served today.
saSouth AfricaRequestable by arrangement; code reserved and key-scopable.
ngNigeriaRequestable by arrangement; code reserved and key-scopable.
keKenyaRequestable by arrangement; code reserved and key-scopable.
ghGhanaRequestable by arrangement; code reserved and key-scopable.
tzTanzaniaRequestable by arrangement; code reserved and key-scopable.
ugUgandaRequestable by arrangement; code reserved and key-scopable.
zmZambiaRequestable by arrangement; code reserved and key-scopable.
cmCameroonRequestable by arrangement; code reserved and key-scopable.
ciCôte d'IvoireRequestable by arrangement; code reserved and key-scopable.
snSenegalRequestable by arrangement; code reserved and key-scopable.
cdDemocratic Republic of the CongoRequestable by arrangement; code reserved and key-scopable.
mzMozambiqueRequestable by arrangement; code reserved and key-scopable.
mwMalawiRequestable by arrangement; code reserved and key-scopable.
auAustraliaRequestable by arrangement; code reserved and key-scopable.
caCanadaRequestable by arrangement; code reserved and key-scopable.
usUnited StatesRequestable by arrangement; code reserved and key-scopable.
rwRwandaCode reserved, not yet requestable.
nzNew ZealandCode reserved, not yet requestable.
  • A known region your key isn't scoped for → 403 region_not_in_scope.
  • A region not in the enum → 404 unknown_region.

Endpoints

Base URL https://api.oddsrelay.io/v2. All endpoints require auth. The legacy /v1 surface is frozen and deprecated (see Versioning).

MethodPathPurpose
GET/v2/odds/{type}The matched board for a feed type + region (v2, event-grouped).
GET/v2/regionsRegions this key may request + their coverage (v2).
GET/v2/coverageBookmakers covered + per-board update times, no odds (v2).
GET/v2/healthLiveness + per-board processed_at / age (v2).
GET/v2/sportsSports + leagues currently covered (LIVE, free).
GET/v2/eventsUpcoming events for a sport (LIVE, free).
GET/v2/bookmakersThe venue catalog: every bookmaker/exchange slug the feed emits (LIVE, free).
GET/v2/odds/rawRaw per-bookmaker odds per event (beta; enablement required).
GET/v2/rawAlias of GET /v2/odds/raw (beta; enablement required).
GET/v2/odds/event/{eventId}Raw per-bookmaker odds for one event (beta; enablement required).

Query parameters for GET /v2/odds/{type}. All are optional; unlisted parameters are ignored:

ParamValuesDefaultNotes
regionuk · a CSV like uk,ireukThe region filter (see above). Key scope gates it.
formatenvelope · bareenvelopebare returns the data array alone; meta moves to X-* headers.
oddsFormatdecimal · americandecimalPure presentation: american returns signed moneyline integers (still numeric).
dateFormatiso · unixisoiso → ISO-8601 UTC …Z; unix → epoch seconds (integers).
includeSeqtrue · falsefalseAdds an X-OddsRelay-Seq header (board sequence) for a clean REST→stream handoff later.

Roadmap · not yet live

  • GET /v2/stream (SSE): Live push (snapshot + delta + seq). Returns 404 today.
  • WSS /v2/ws: Sub-second bidirectional channel. Returns 404 today.
  • Full cross-book arbitrage & EV: Arbitrage/EV computed across the full roster (derived arbitrage & EV surfaces exist in the platform but are not part of the current public catalogue). Coming.
  • Historical odds: Point-in-time odds/opportunity history. Coming; not live.
  • Middles, low-hold & dropping-odds signals: Derived middle, low-hold and steam/dropping-odds feeds. Coming; not live.
  • OddsRelay-Version date-pinning: Optional Stripe-style per-key version header for fine-grained backward-compatible evolution.

The v2 surface

Beyond the matched boards above, /v2 carries a free discovery family and an on-request raw family. Everything is purely additive; nothing in the frozen /v1 changes. Base URL https://api.oddsrelay.io/v2. Same server-to-server auth (Bearer / x-api-key). The list families share one envelope, { meta, data }, with meta.next_cursor for pagination (?cursor=). Two endpoints answer differently and are marked where they appear: /v2/odds/event/{eventId} returns the bare event object, and /v2/bookmakers returns a catalogue with no cursor. Add ?format=bare for the data array alone, ?oddsFormat=american (default decimal) and ?dateFormat=unix (default ISO-8601 Z). Strong ETag/304 and gzip work exactly as on the matched boards.

MethodPathPurpose
GET/v2/sportsSports + leagues currently covered (LIVE, free).
GET/v2/eventsUpcoming events for a sport (LIVE, free).
GET/v2/bookmakersThe venue catalog: every bookmaker/exchange slug the feed emits (LIVE, free).
GET/v2/odds/rawRaw per-bookmaker odds per event (beta; enablement required).
GET/v2/rawAlias of GET /v2/odds/raw (beta; enablement required).
GET/v2/odds/event/{eventId}Raw per-bookmaker odds for one event (beta; enablement required).

Sports, events & bookmakersLive · free

GET /v2/sports lists the sports and leagues currently covered; GET /v2/events lists upcoming fixtures, each with a has_coverage[] array naming which boards carry it (e.g. standard, dutching). ?sport= is required on /v2/events: a precise key (soccer_premier_league) or a coarse prefix (soccer); narrow further with ?eventIds= or a commenceTimeFrom/commenceTimeTo window. All three discovery endpoints are live and free (each response carries X-Requests-Cost: 0), though they still count toward your key's fair-use rate limit like every endpoint.

200 · /v2/sports
{
  "meta": {
    "feed_type": "sports",
    "region": "uk",
    "odds_format": "decimal",
    "processed_at": "2026-08-15T12:03:12.000Z",
    "count": 2,
    "version": "v2",
    "next_cursor": null
  },
  "data": [
    {
      "key": "soccer_intl_friendly",
      "group": "Soccer",
      "title": "Intl Friendly",
      "description": "Intl Friendly (Soccer)",
      "active": true,
      "has_outrights": false,
      "regions": [
        "ire",
        "uk"
      ]
    },
    {
      "key": "soccer_premier_league",
      "group": "Soccer",
      "title": "Premier League",
      "description": "Premier League (Soccer)",
      "active": true,
      "has_outrights": false,
      "regions": [
        "ire",
        "uk"
      ]
    }
  ]
}
200 · /v2/events?sport=soccer
{
  "meta": {
    "feed_type": "events",
    "region": "uk",
    "odds_format": "decimal",
    "processed_at": "2026-08-15T12:03:12.000Z",
    "count": 3,
    "version": "v2",
    "next_cursor": null
  },
  "data": [
    {
      "id": "or_evt_2a7a8855d318",
      "sport_key": "soccer_premier_league",
      "sport_title": "Premier League",
      "commence_time": "2026-08-15T14:00:00Z",
      "home_team": "Arsenal",
      "away_team": "Chelsea",
      "status": "upcoming",
      "has_coverage": [
        "standard"
      ]
    },
    {
      "id": "or_evt_d584fd3d39ef",
      "sport_key": "soccer_premier_league",
      "sport_title": "Premier League",
      "commence_time": "2026-08-15T15:00:00Z",
      "home_team": "Spurs",
      "away_team": "Everton",
      "status": "upcoming",
      "has_coverage": [
        "standard"
      ]
    },
    {
      "id": "or_evt_7b52c2b9681b",
      "sport_key": "soccer_intl_friendly",
      "sport_title": "Intl Friendly",
      "commence_time": "2026-08-15T18:00:00Z",
      "home_team": "Spain",
      "away_team": "Brazil",
      "status": "upcoming",
      "has_coverage": [
        "dutching"
      ]
    }
  ]
}

GET /v2/bookmakersis the venue catalog: it resolves every slug the feed emits (back offers' bookmaker, lay offers' exchange, raw book keys) to its display name, regions and whether it is an exchange. Offers carry a lean slug instead of repeating that metadata thousands of times per response; the catalog is static config, so cache it and refresh occasionally.

200 · /v2/bookmakers
{
  "meta": {
    "feed_type": "bookmakers",
    "count": 67,
    "version": "v2"
  },
  "data": [
    {
      "key": "betfair_exchange",
      "name": "Betfair Exchange",
      "regions": [
        "ire",
        "uk"
      ],
      "is_exchange": true
    },
    {
      "key": "sky_bet",
      "name": "Sky Bet",
      "regions": [
        "ire",
        "uk"
      ],
      "is_exchange": false
    }
  ]
}

Raw oddsBeta · enablement required

GET /v2/odds/raw (alias /v2/raw) returns per-bookmaker odds exactly as read from each book: each bookmaker's own market and native selection strings, not cross-book-normalised per selection. Each event also carries the paired exchange lay price + liquidity alongside the back prices. Raw is available on request (beta): a key without the raw form in its scope returns 403 raw_not_in_scope, and a raw-scoped key returns 503 raw_not_enabled until raw serving is enabled for it. Ask us to enable raw on your key. The raw board carries prices only; the per-offer deep links live on the matched boards. Fetch one event with GET /v2/odds/event/{eventId}.

200 · /v2/odds/raw
{
  "meta": {
    "feed_type": "raw",
    "region": "uk",
    "odds_format": "decimal",
    "processed_at": "2026-08-15T12:03:12.000Z",
    "count": 1,
    "version": "v2",
    "next_cursor": null
  },
  "data": [
    {
      "id": "or_evt_2a7a8855d318",
      "sport_key": "soccer_premier_league",
      "sport_title": "Premier League",
      "commence_time": "2026-08-15T14:00:00Z",
      "home_team": "Arsenal",
      "away_team": "Chelsea",
      "bookmakers": [
        {
          "key": "bet365",
          "title": "bet365",
          "region": "uk",
          "last_update": "2026-08-15T12:03:11Z",
          "markets": [
            {
              "key": "h2h",
              "last_update": "2026-08-15T12:03:11Z",
              "outcomes": [
                {
                  "name": "Arsenal",
                  "price": 2.1
                },
                {
                  "name": "Chelsea",
                  "price": 3.6
                },
                {
                  "name": "Draw",
                  "price": 3.4
                }
              ]
            }
          ]
        },
        {
          "key": "betfair_exchange",
          "title": "Betfair Exchange",
          "region": "uk",
          "last_update": "2026-08-15T12:03:09Z",
          "markets": [
            {
              "key": "h2h_lay",
              "last_update": "2026-08-15T12:03:09Z",
              "outcomes": [
                {
                  "name": "Arsenal",
                  "back_price": 2.1,
                  "lay_price": 2.12,
                  "available": 1450
                }
              ]
            }
          ],
          "currency": "GBP"
        }
      ]
    }
  ]
}
200 · /v2/odds/event/{eventId}
{
  "id": "or_evt_2a7a8855d318",
  "sport_key": "soccer_premier_league",
  "sport_title": "Premier League",
  "commence_time": "2026-08-15T14:00:00Z",
  "home_team": "Arsenal",
  "away_team": "Chelsea",
  "bookmakers": [
    {
      "key": "bet365",
      "title": "bet365",
      "region": "uk",
      "last_update": "2026-08-15T12:03:11Z",
      "markets": [
        {
          "key": "h2h",
          "last_update": "2026-08-15T12:03:11Z",
          "outcomes": [
            {
              "name": "Arsenal",
              "price": 2.1
            },
            {
              "name": "Chelsea",
              "price": 3.6
            },
            {
              "name": "Draw",
              "price": 3.4
            }
          ]
        }
      ]
    },
    {
      "key": "betfair_exchange",
      "title": "Betfair Exchange",
      "region": "uk",
      "last_update": "2026-08-15T12:03:09Z",
      "markets": [
        {
          "key": "h2h_lay",
          "last_update": "2026-08-15T12:03:09Z",
          "outcomes": [
            {
              "name": "Arsenal",
              "back_price": 2.1,
              "lay_price": 2.12,
              "available": 1450
            }
          ]
        }
      ],
      "currency": "GBP"
    }
  ]
}

On the roadmapComing · not live

Coming next (not live yet): full cross-book arbitrage & +EV across the full roster, SSE/WebSocket streaming, historical odds, and middles / low-hold / dropping-odds signals. These are listed under x-roadmap in the spec and in the roadmap card above, described as planned, never as shipped.

The envelope & types

GET /v2/odds/{type} returns { meta, data }, where data is event-grouped: each event appears once, then markets[] outcomes[]carry every venue's offers side by side (back[] best price first; lay[] lowest first). Every odds and money value is a number (e.g. 2.1, 1840.0); every timestamp is ISO-8601 UTC (…Z). Switch presentation with ?oddsFormat=american (signed moneylines) or ?dateFormat=unix (epoch seconds). Ignore unknown fields.

FieldTypeNotes
dataarrayEvent objects, each appearing once: events → markets → outcomes → back/lay offers.
meta.feed_typestringThe feed-type slug served.
meta.regionstringThe region(s) served (echoes the request).
meta.odds_formatstringdecimal (default) or american; echoes ?oddsFormat.
meta.processed_atstring (ISO Z)Matcher cycle stamp; epoch seconds with ?dateFormat=unix.
meta.countintegerEvents in data (this page). On paginated families X-Count reports the full filtered set instead, so the two differ when a result spans pages.
meta.stale_hiddenbooleantrue when the whole board is older than its update-time SLA. Rows are never dropped for age; treat it as an age warning on the data you were served.
meta.unreliable_linksobject[]{ bookmaker, sport } slug pairs for books whose deeplink is flagged unreliable.
meta.versionstring"v2".
meta.next_cursorstring | nullPagination cursor (discovery/raw); null on the matched boards (served whole).

Per-type shapes. One grammar; each {type} fixes which offer sections an outcome carries:

typeType-specific fields
standardoutcomes carry back[] + lay[]. Back offer: { bookmaker, price, link, last_update }; lay offer: { exchange, price, available, link, last_update }.
2upSame back/lay grammar as standard (h2h markets; the promo context is the feed type itself).
bogRace events (venue, runners as outcomes), market win, same back/lay offers. Every book on the board pays best odds guaranteed.
each-wayRace events, market each_way. Back offers add places + place_fraction (the book's terms); lay splits into { win: [...], place: [...] }; each place offer carries the places its exchange market pays.
extra-placeSame shape as each-way; the edge is a back offer whose places exceeds the place market's places.
dutchingBack only: a fully-priced market with no lay key. Take the best price per outcome to reconstruct the dutch.

Each event appears once, carrying event_id (stable across polls AND across every /v2 family), sport_key + sport_title, home_team/away_team for team sports (race events carry venue; if a team fixture cannot be split, both are null and event_name carries the original string), and commence_time. Venues are snake_case slugs (sky_bet) resolved by GET /v2/bookmakers; every offer carries its own last_update. All odds/money values are numbers; all timestamps are ISO-8601 UTC (…Z), or epoch seconds with ?dateFormat=unix.

Derive it client-side

The wire carries source facts only: prices, liquidity, place terms, links, timestamps. Commission is not a wire field; it is your own exchange account's rate. Ratings, qualifying loss, profit and stake splits are pure functions of the carried prices, so you compute them in one line, with your stake and your commission rate, and no fixed assumptions baked into the feed:

  • rating = back ÷ (lay − (lay − 1) × commission) × 100 (commission = your own account's rate; at 0: 100 × back ÷ lay)
  • lay stake = stake × back ÷ (lay − commission × (lay − 1))
  • dutching overround = Σ 1/price; stake per outcome ∝ 1/price; profit = stake × (1/overround − 1)

Each-way / extra-place use the standard EW qualifying maths over (back price, terms) × (win lay, place lay); the extra-place edge is back places minus the place market's places.

A trimmed sample response:

200 · /v2/odds/standard?region=uk
{
  "meta": {
    "feed_type": "standard",
    "region": "uk",
    "odds_format": "decimal",
    "processed_at": "2026-07-08T12:34:56.123Z",
    "count": 412,
    "stale_hidden": false,
    "unreliable_links": [],
    "version": "v2",
    "next_cursor": null
  },
  "data": [
    {
      "event_id": "or_evt_8092b51efde0",
      "sport_key": "soccer_premier_league",
      "sport_title": "Premier League",
      "commence_time": "2026-07-08T14:00:00Z",
      "home_team": "Arsenal",
      "away_team": "Chelsea",
      "markets": [
        {
          "key": "h2h",
          "outcomes": [
            {
              "name": "Arsenal",
              "back": [
                {
                  "bookmaker": "888sport",
                  "price": 2.1,
                  "link": null,
                  "last_update": "2026-07-08T12:34:48Z"
                }
              ],
              "lay": [
                {
                  "exchange": "smarkets",
                  "price": 2.05,
                  "available": 1840,
                  "link": null,
                  "last_update": "2026-07-08T12:34:48Z"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Prefer ?format=bare to receive the data array alone; the metadata moves to X-Processed-At / X-Count / X-Stale-Hidden headers.

Errors

Errors use one stable JSON envelope. Branch on error.code (the message may change). Every error carries a request_id in the body, echoed in the X-OddsRelay-Request-Id header; quote it in support. On scope errors (403) a type field names the axis that failed (type / region / raw / bet365).

Error envelope
{
  "error": {
    "code": "region_not_in_scope",
    "message": "Key not scoped for region 'sa'.",
    "type": "region",
    "request_id": "or_req_…"
  }
}
StatuscodeWhen
400bad_requestMalformed query (bad region syntax, or an unsupported oddsFormat/dateFormat).
400missing_paramA required query parameter was omitted (e.g. /v2/events without ?sport=).
401missing_api_keyNo key presented.
401invalid_api_keyKey not recognised, or the account for the key is not active.
401revoked_api_keyKey has been revoked.
401expired_api_keyKey is past its expiry.
403type_not_in_scopeValid key, not scoped for this feed type.
403region_not_in_scopeValid key, not scoped for this (known) region.
403raw_not_in_scopeValid key, not scoped for the raw form.
404unknown_typeFeed type not in the enum.
404unknown_regionRegion not in the enum.
404unknown_sportThe `sport` filter is not a known sport key (v2 events/raw).
404unknown_eventThe `{eventId}` is not a known event id (GET /v2/odds/event/{eventId}).
404not_foundNo such endpoint/path.
405method_not_allowedA non-GET method. The API is read-only: only GET (and CORS preflight OPTIONS for widget keys) is supported.
429rate_limitedPer-key fair-use or per-IP flood cap (see Retry-After).
500internal_errorServer fault (no upstream leakage).
503internal_errorFeed momentarily unavailable (board warming or a load burst). Safe to retry after Retry-After seconds.
503raw_not_enabledThe key is scoped for the raw form, but raw serving is not yet enabled for it (available on request, beta).

Versioning & deprecation

  • Path-versioned. /v2 is current; /v1 is frozen legacy. The effective dated version is echoed in X-OddsRelay-Api-Version.
  • Additive-only within a major. New endpoints, new optional fields, new enum values and new error codes can land any time, so always ignore unknown fields.
  • Breaking changes never happen in place. A removal/rename/semantic change ships as a new major (this is exactly why /v2 is a new path, not a mutation of /v1).
  • v1 is deprecated in favour of v2. Every /v1 response carries Deprecation + Link headers pointing here. No hard Sunset date is set: existing integrations keep working, and a live client is never sunset without consent (≥ 12 months notice).
DateVersionChange
2026-07-21v2commission removed from matched lay offers (including the each-way win/place lay pair) and raw exchange outcomes. It was a static per-venue constant, not a price fact; your exchange account's own commission rate is the true input to rating/qualifying-loss maths, applied client-side in the documented formulas, never a wire field. /v1 is byte-identical and unchanged.
2026-07-20v2v2 matched schema redesigned in place (pre-customer): responses are now EVENT-GROUPED (data[] = events → markets[] → outcomes[] → back[]/lay[] offers) with formal home_team/away_team (venue for racing) and comparator-standard naming (price, link, available, last_update, snake_case venue slugs). Derived values removed from the wire: rating, qualifying_loss, potential_profit, roi, implied_*, stake_pct and overround are computed client-side from the carried prices; meta.count now counts events and meta.board_count is gone (bare-format header is X-Count). Raw board outcomes drop the redundant side flag and currency moves to the exchange's bookmaker node. NEW: GET /v2/bookmakers, the free venue catalog resolving every slug. /v1 is byte-identical and unchanged.
2026-07-08v2Added the normalized /v2 schema (numeric odds/money, ISO-8601 Z timestamps, stable event_id + selection_key, meta/data envelope, unreliable_links typed as objects, distinct count vs board_count, ?oddsFormat and ?dateFormat). /v2 is the new default; /v1 is FROZEN legacy and now DEPRECATED (Deprecation + Link headers; no sunset date set).
2026-06-30v1Initial public feed: /v1/odds/{type} (the processed feed types), /v1/regions, /v1/coverage, /v1/health; or_live_*/or_test_* 4-axis scope; ETag/304, gzip, rate-limit headers, the stable error envelope.

Rate limits

Two layers: a per-IP flood throttle, and a per-key fair-use cap (tier-scaled). Every response carries your budget:

Response headers
X-RateLimit-Limit:            <your per-minute cap>
X-RateLimit-Remaining:        <left this minute>
X-RateLimit-Limit-Hour:       <your per-hour cap>
X-RateLimit-Remaining-Hour:   <left this hour>
# on 429:
Retry-After:                  <seconds>

These four ride on every odds response. The discovery, raw and health endpoints answer without them, and so do error responses other than a per-key 429, so read your budget from a successful odds call rather than from an error.

The discovery and raw families also carry X-Requests-Remaining / X-Requests-Used / X-Requests-Cost, a convenience view over the same hourly budget. Every request counts as one, including a 304: revalidating saves you the payload, not the quota. Budget your polling on request count, not bytes.

Embedding widgets

The same feed powers embeddable widgets you can drop into your own product: calculators (no key) and a live oddsmatcher (your key). The oddsmatcher calls this API directly from your visitors' browsers, so odds bytes never route through us twice. Theme and preview them in the widget library.

One host element + one script
<div data-or-widget="oddsmatcher"
     data-or-region="uk"
     data-or-key="or_live_YOUR_WIDGET_KEY"></div>
<script src="https://oddsrelay.io/widgets/v1/oddsrelay-widgets.js" crossorigin="anonymous" async></script>

A widget key is a browser key locked to an origin allowlist: the API reflects Access-Control-Allow-Origin only for the exact origins you register (never a wildcard), so a copied snippet is inert on any other domain. Widget keys are display-scoped and carry their own rate cap; everything else (auth, the region filter, ETag/304 and the rate-limit headers) works exactly as above. Ask us to issue one locked to your domains.

The full embed contract is at /docs/widgets: the version-pinned script URL and its integrity hash, the exact CSP directives, consent-manager categorisation, what the widget stores on a visitor's device, per-platform install guides and the versioning policy.

OpenAPI spec

The full machine-readable contract is published as OpenAPI 3.1. Import it into Postman, Insomnia, Scalar, Redoc or any code generator.