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.
# 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.
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.
# 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.
| region | Market | Status |
|---|---|---|
| uk | United Kingdom | Requestable, served today. |
| ire | Ireland | Requestable, served today. |
| sa | South Africa | Requestable by arrangement; code reserved and key-scopable. |
| ng | Nigeria | Requestable by arrangement; code reserved and key-scopable. |
| ke | Kenya | Requestable by arrangement; code reserved and key-scopable. |
| gh | Ghana | Requestable by arrangement; code reserved and key-scopable. |
| tz | Tanzania | Requestable by arrangement; code reserved and key-scopable. |
| ug | Uganda | Requestable by arrangement; code reserved and key-scopable. |
| zm | Zambia | Requestable by arrangement; code reserved and key-scopable. |
| cm | Cameroon | Requestable by arrangement; code reserved and key-scopable. |
| ci | Côte d'Ivoire | Requestable by arrangement; code reserved and key-scopable. |
| sn | Senegal | Requestable by arrangement; code reserved and key-scopable. |
| cd | Democratic Republic of the Congo | Requestable by arrangement; code reserved and key-scopable. |
| mz | Mozambique | Requestable by arrangement; code reserved and key-scopable. |
| mw | Malawi | Requestable by arrangement; code reserved and key-scopable. |
| au | Australia | Requestable by arrangement; code reserved and key-scopable. |
| ca | Canada | Requestable by arrangement; code reserved and key-scopable. |
| us | United States | Requestable by arrangement; code reserved and key-scopable. |
| rw | Rwanda | Code reserved, not yet requestable. |
| nz | New Zealand | Code 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).
| Method | Path | Purpose |
|---|---|---|
| GET | /v2/odds/{type} | The matched board for a feed type + region (v2, event-grouped). |
| GET | /v2/regions | Regions this key may request + their coverage (v2). |
| GET | /v2/coverage | Bookmakers covered + per-board update times, no odds (v2). |
| GET | /v2/health | Liveness + per-board processed_at / age (v2). |
| GET | /v2/sports | Sports + leagues currently covered (LIVE, free). |
| GET | /v2/events | Upcoming events for a sport (LIVE, free). |
| GET | /v2/bookmakers | The venue catalog: every bookmaker/exchange slug the feed emits (LIVE, free). |
| GET | /v2/odds/raw | Raw per-bookmaker odds per event (beta; enablement required). |
| GET | /v2/raw | Alias 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:
| Param | Values | Default | Notes |
|---|---|---|---|
| region | uk · a CSV like uk,ire | uk | The region filter (see above). Key scope gates it. |
| format | envelope · bare | envelope | bare returns the data array alone; meta moves to X-* headers. |
| oddsFormat | decimal · american | decimal | Pure presentation: american returns signed moneyline integers (still numeric). |
| dateFormat | iso · unix | iso | iso → ISO-8601 UTC …Z; unix → epoch seconds (integers). |
| includeSeq | true · false | false | Adds 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.
| Method | Path | Purpose |
|---|---|---|
| GET | /v2/sports | Sports + leagues currently covered (LIVE, free). |
| GET | /v2/events | Upcoming events for a sport (LIVE, free). |
| GET | /v2/bookmakers | The venue catalog: every bookmaker/exchange slug the feed emits (LIVE, free). |
| GET | /v2/odds/raw | Raw per-bookmaker odds per event (beta; enablement required). |
| GET | /v2/raw | Alias 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.
{
"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"
]
}
]
}{
"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.
{
"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}.
{
"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"
}
]
}
]
}{
"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.
| Field | Type | Notes |
|---|---|---|
| data | array | Event objects, each appearing once: events → markets → outcomes → back/lay offers. |
| meta.feed_type | string | The feed-type slug served. |
| meta.region | string | The region(s) served (echoes the request). |
| meta.odds_format | string | decimal (default) or american; echoes ?oddsFormat. |
| meta.processed_at | string (ISO Z) | Matcher cycle stamp; epoch seconds with ?dateFormat=unix. |
| meta.count | integer | Events 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_hidden | boolean | true 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_links | object[] | { bookmaker, sport } slug pairs for books whose deeplink is flagged unreliable. |
| meta.version | string | "v2". |
| meta.next_cursor | string | null | Pagination cursor (discovery/raw); null on the matched boards (served whole). |
Per-type shapes. One grammar; each {type} fixes which offer sections an outcome carries:
| type | Type-specific fields |
|---|---|
| standard | outcomes carry back[] + lay[]. Back offer: { bookmaker, price, link, last_update }; lay offer: { exchange, price, available, link, last_update }. |
| 2up | Same back/lay grammar as standard (h2h markets; the promo context is the feed type itself). |
| bog | Race events (venue, runners as outcomes), market win, same back/lay offers. Every book on the board pays best odds guaranteed. |
| each-way | Race 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-place | Same shape as each-way; the edge is a back offer whose places exceeds the place market's places. |
| dutching | Back 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:
{
"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": {
"code": "region_not_in_scope",
"message": "Key not scoped for region 'sa'.",
"type": "region",
"request_id": "or_req_…"
}
}| Status | code | When |
|---|---|---|
| 400 | bad_request | Malformed query (bad region syntax, or an unsupported oddsFormat/dateFormat). |
| 400 | missing_param | A required query parameter was omitted (e.g. /v2/events without ?sport=). |
| 401 | missing_api_key | No key presented. |
| 401 | invalid_api_key | Key not recognised, or the account for the key is not active. |
| 401 | revoked_api_key | Key has been revoked. |
| 401 | expired_api_key | Key is past its expiry. |
| 403 | type_not_in_scope | Valid key, not scoped for this feed type. |
| 403 | region_not_in_scope | Valid key, not scoped for this (known) region. |
| 403 | raw_not_in_scope | Valid key, not scoped for the raw form. |
| 404 | unknown_type | Feed type not in the enum. |
| 404 | unknown_region | Region not in the enum. |
| 404 | unknown_sport | The `sport` filter is not a known sport key (v2 events/raw). |
| 404 | unknown_event | The `{eventId}` is not a known event id (GET /v2/odds/event/{eventId}). |
| 404 | not_found | No such endpoint/path. |
| 405 | method_not_allowed | A non-GET method. The API is read-only: only GET (and CORS preflight OPTIONS for widget keys) is supported. |
| 429 | rate_limited | Per-key fair-use or per-IP flood cap (see Retry-After). |
| 500 | internal_error | Server fault (no upstream leakage). |
| 503 | internal_error | Feed momentarily unavailable (board warming or a load burst). Safe to retry after Retry-After seconds. |
| 503 | raw_not_enabled | The 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).
| Date | Version | Change |
|---|---|---|
| 2026-07-21 | v2 | commission 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-20 | v2 | v2 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-08 | v2 | Added 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-30 | v1 | Initial 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:
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.
<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.