Skip to content
OddsRelay

API reference

Integrate in an afternoon.

The OddsRelay feed is one keyed, versioned REST API: get a key, send one authenticated GET, receive matched opportunities. Deep UK bookmaker coverage with bet365 included, exchange/lay-ready — served from api.oddsrelay.io/v1.

Quickstart

1. Get a key. Start a free trial — your or_test_* key serves the full UK product (all 7 feed types, bet365 included) 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/v1/odds/standard?region=uk" \
  -H "Authorization: Bearer <YOUR_API_KEY>"

3. Poll efficiently. The board rolls roughly every 15s. 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.

Conditional polling (304)
# 1. First poll — capture the ETag
ETAG=$(curl -sD - -o /dev/null --compressed \
  "https://api.oddsrelay.io/v1/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/v1/odds/standard?region=uk" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "If-None-Match: $ETAG"

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 — 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/v1/odds/standard?api_key=...   ✗

A key carries a scope across four axes — feed types, regions, form (processed/raw) and add-ons (e.g. bet365). 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,ie. There is no endpoint-per-region. Your key's scope gates which regions it may request; call GET /v1/regions to discover them.

  • uk is live today; other regions are reserved and 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/v1. All endpoints require auth.

MethodPathPurpose
GET/odds/{type}Processed (matched) opportunities for a feed type + region.
GET/regionsRegions this key may request + their coverage.
GET/coverageBookmakers covered + per-board freshness (no odds).
GET/healthLiveness + per-board processed_at / age for the key's regions.

Roadmap — not yet live

  • GET /v1/raw/oddsNormalised, unmatched odds per selection (raw tier). Returns 404 today.
  • GET /v1/stream (SSE)Live push — snapshot + delta + seq. Returns 404 today.
  • WSS /v1/wsSub-second bidirectional channel (arb tier). Returns 404 today.
  • OddsRelay-Version date-pinningOptional Stripe-style per-key version header for fine-grained backward-compatible evolution.

The envelope & types

GET /v1/odds/{type} returns a JSON envelope. Odds are decimal strings (e.g. "1.6"); money is a string, usually £-prefixed and occasionally negative; rating, ROI and liquidity are numbers. Parse defensively and ignore unknown fields.

FieldTypeNotes
opportunitiesarrayThe matched opportunities. Shape depends on {type} (see below).
opportunity_countintegerHow many opportunities were served.
processed_atstring (ISO)Matcher cycle stamp, millisecond precision.
stale_hiddenbooleantrue if rows were hidden past the freshness cutoff.
served_totalintegerTotal served (equals opportunity_count today).
unreliable_linksstring[]Selections whose deeplink is flagged unreliable.
regionstringThe region served (echoes the request).

Per-type opportunity fields. The concrete shape is chosen by {type}:

typeType-specific fields
standardBack-vs-lay legs: outcome, bookmaker (+_link), exchange (+_link), back_odds, lay_odds, lay_liquidity, rating.
2upSame back-vs-lay shape as standard (2Up offer market).
bogBack-vs-lay + ROI (best-odds-guaranteed, racing).
each-wayWin + place exchange legs: horse, bookmaker_each_way_odds, bookmaker_fraction, bookmaker_places, win_exchange (+_link), exchange_win_odds/_liquidity, place_exchange (+_link), exchange_place_odds/_liquidity, exchange_places, rating, ROI.
extra-placeextra_places, implied_odds (no ROI).
dutchingoutcomes (leg count) + per-leg first_/second_/… : *_bookmaker, *_outcome, *_odds, *_link; ROI.
price-boost-matcherBack-vs-lay + original_odds (the pre-boost price).

Every opportunity also carries: _id, sport, market_type, fixture, league, commence_time, qualifying_loss, potential_profit, region, plus denormalised time fields (date_and_time, game_day, game_time, datetime_string, odds_read_at) and horse_race_distance / horse_race_runners on racing rows.

A trimmed sample response:

200 — /v1/odds/standard?region=uk
{
  "opportunities": [
    {
      "_id": "mu5y0PCqeQufUv3K0soTnCvxhiYk5nJx",
      "sport": "Football",
      "market_type": "BTTS",
      "fixture": "Spain U19 (W) v Iceland U19 (W)",
      "league": "UEFA Women's U19 Euro Championship",
      "outcome": "No",
      "bookmaker": "Paddy Power",
      "bookmaker_link": "https://www.paddypower.com/...",
      "exchange": "Betfair Exchange",
      "exchange_link": "https://www.betfair.com/exchange/...",
      "back_odds": "1.6",
      "lay_odds": "1.51",
      "lay_liquidity": 21.39,
      "rating": 105.96,
      "qualifying_loss": "£0.60",
      "potential_profit": "£11.92",
      "commence_time": "2026-07-01T15:00:00Z",
      "date_and_time": "01/07/26 16:00",
      "game_day": "01/07/26",
      "game_time": "16:00",
      "datetime_string": "20260630201025",
      "odds_read_at": "2026-06-30T20:10:21.746441+00:00",
      "horse_race_distance": null,
      "horse_race_runners": null,
      "region": "uk"
    }
  ],
  "opportunity_count": 21114,
  "processed_at": "2026-06-30T20:10:29.000Z",
  "stale_hidden": false,
  "served_total": 21114,
  "unreliable_links": [],
  "region": "uk"
}

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

Errors

Errors use one stable JSON envelope — branch on error.code (the message may change). Every response carries an X-OddsRelay-Request-Id (echoed in the body) — quote it in support.

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 (e.g. bad region syntax).
401missing_api_keyNo key presented.
401invalid_api_keyKey not recognised.
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.
403bet365_not_in_scopeReserved — bet365 is enforced by FILTERING (a key without the add-on receives 0 bet365 rows), never a 403.
404unknown_typeFeed type not in the enum.
404unknown_regionRegion not in the enum.
404not_foundNo such endpoint/path.
429rate_limitedPer-key fair-use or per-IP flood cap (see Retry-After).
500internal_errorServer fault (no upstream leakage).

Versioning & deprecation

  • Path-versioned /v1. The effective dated version is echoed in X-OddsRelay-Api-Version.
  • Additive-only within v1. New endpoints, new optional fields, new enum values and new error codes can land any time — always ignore unknown fields.
  • Breaking changes never happen in place. A removal/rename/semantic change only ships in a future /v2 with a sunset window.
  • Anything being retired is signalled with Deprecation + Sunset + Link headers, with ≥ 12 months notice — and a live client is never sunset without consent.

Rate limits

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

Response headers
X-RateLimit-Limit:            120
X-RateLimit-Remaining:        118
X-RateLimit-Limit-Hour:       2000
X-RateLimit-Remaining-Hour:   1998
# on 429:
Retry-After:                  <seconds>

Conditional polling (gzip + If-None-Match → 304) keeps you well inside any cap — a 304 is cheap and still returns fresh rate-limit headers.

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 playground.

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/oddsrelay-widgets.js" 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.

OpenAPI spec

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

Ready to build?

Get a key and pull live matched opportunities in minutes.