API documentation

Every endpoint is a GET request to https://api.oddsrelay.io with your API key in a header. A call costs tokens, set by the product and the filters you send, never by the size of the reply.

API version 2026-07-08 · Changelog

On this page

Quickstart

  1. Get a key. Sign up free. Your first API key is shown once when you finish, so copy it then. Save it as the environment variable ODDSRELAY_KEY.

  2. Make your first call. Football match odds (sports=soccer, markets=h2h) in UK & Ireland, on the standard product:

    # --compressed asks for gzip and decompresses it for you.
    BASE=https://api.oddsrelay.io
    curl --compressed -sS -D - \
      -H "Authorization: Bearer $ODDSRELAY_KEY" \
      "$BASE/v2/odds/standard?region=uk&sports=soccer&markets=h2h"

    This call costs 1,107 tokens (pricing). Try it in the request tool

  3. Check the price first. Add quote=true to any odds request to get its cost without running it. Quotes are free.

    Quote

    BASE=https://api.oddsrelay.io
    curl --compressed -s -H "Authorization: Bearer $ODDSRELAY_KEY" \
      "$BASE/v2/odds/standard?region=uk&sports=soccer&quote=true"
    # {"cost": 5400, "remaining": …, "rate_card": "…"}
  4. Poll without paying twice. Send the last ETag back as If-None-Match. If nothing has changed you get a 304 with no body, and it costs no tokens.

    Poll with ETag

    # Send the last ETag back exactly as you got it, quotes included:
    # ETAG='"c1f97f865-4517-gz"'
    BASE=https://api.oddsrelay.io
    curl --compressed -s -o /dev/null -w "%{http_code}\n" \
      -H "Authorization: Bearer $ODDSRELAY_KEY" \
      -H "If-None-Match: $ETAG" \
      "$BASE/v2/odds/standard?region=uk&sports=soccer&markets=h2h"

For a complete client with retries and polling, see Example client.

Authentication

Send your key in the Authorization header as Bearer <key>, or in x-api-key. Never put it in the query string.

Server keys start or_live_: keep them on your server and out of code repositories. Browser keys start or_pub_, come with the Enterprise plan, and only work on the websites listed on the key.

No key is needed for /healthz, /v2/status, /v2/openapi.json, or the public view of /v2/coverage.

Regions

Odds endpoints take one region code. Available today: uk (UK & Ireland), which is also the default. Send one lowercase code; a list, a repeat or an uppercase code returns 400. Discovery and account endpoints take no region. GET /v2/regions lists the regions your key covers.

Endpoints

All endpoints are GET requests to https://api.oddsrelay.io. Try opens the call in your dashboard's request tool, which shows its cost before it runs. You need to be signed in.

EndpointReturnsCost
/v2/odds/{type}Matched odds for one product. TryTokens
/v2/odds/rawEvery bookmaker's and exchange's prices, a page at a time. TryTokens
/v2/odds/event/{id}Raw prices for one eventTokens
/v2/sportsSports and competitions, with their marketsFree
/v2/eventsEvents for a sportFree
/v2/bookmakersBookmakers and exchangesFree
/v2/regionsRegions your key coversFree
/v2/coverageWhat each bookmaker and exchange covers, by sportFree
/v2/statusService status per product (no key needed)Free
/v2/healthHow recently each product was updatedFree
/v2/usageYour plan, token balance and rate limitsFree
/v2/pricingThe token price tableFree
/v2/openapi.jsonThe OpenAPI 3.1 contract (no key needed)Free
/healthzReturns 200 when the API is up (no key needed)Free

Products

GET /v2/odds/{type} returns matched odds for one product. Each event lists its markets, each market its outcomes, and each outcome the best back prices from bookmakers beside the best lay prices from exchanges.

ProductWhat it holdsTry
standardEvery bookmaker's back price beside the best exchange lay price, event by event.Try
2upFootball prices from bookmakers' 2Up markets, beside the lay for the same team.Try
dutchingBack prices covering every outcome of a market, grouped into ready-made dutches.Try
each-wayHorse racing prices with each bookmaker's each-way terms, and both lays.Try
extra-placeOnly the races where a bookmaker pays more places than the exchange.Try
bogWin prices from bookmakers offering best odds guaranteed on a race.Try
rawEvery bookmaker's and exchange's prices, unmatched, 250 events a page.Try

On each-way and extra-place, lay is split into win and place. dutching has no lay prices; its back legs share a dutch_id. Prices don't include exchange commission: apply your own rate when you work out ratings and lay stakes.

Event ids are the same on every endpoint, but an id can change once, when a higher-ranked exchange lists the fixture. Don't use it as a permanent key.

Raw odds

GET /v2/odds/rawreturns every bookmaker's and exchange's prices per event, up to 250 events a page, ordered by kick-off. Follow meta.next_cursor for the next page. A cursor expires after 300 seconds; if it does, start again from the first page. Deduplicate by event id. Add updatedSince to get only what changed. See Paging through raw odds.

Discovery

/v2/sports, /v2/events, /v2/bookmakers and /v2/regions are free and list the values the filters accept.

What a reply carries

FieldDescription
meta.processed_atWhen the reply was built, also sent as X-Processed-At.
meta.last_seenWhen each bookmaker and sport was last read. The time since then is the most a price can have aged.
meta.countEvents in data.
X-OddsRelay-VersionA response header, not a field: which version of the data you were served, the same on every endpoint for one publish.

Prices carry no timestamp of their own; use meta.last_seen. A bookmaker not read within its own window, at most two minutes, is left out until it is read again. See How fresh a price is.

Filters

ParameterMax valuesReturns only
sports20Events in these sports or competitions (a sport key or a prefix, such as soccer)
bookmakers40Back prices from these bookmakers
exchanges4Lay prices from these exchanges
markets20These markets, such as h2h
commenceTimeFrom, commenceTimeTo–Events kicking off in this window (ISO 8601, UTC)

Filtering never changes a price: you get the full product with the rest removed. An outcome left with no back or no lay price is dropped, then any empty market or event. On dutching, a dutch is kept only when every leg is at a bookmaker you named. An unknown value returns 400 with the valid values listed. A valid value with nothing on today returns an empty reply, which is free. For one event, use /v2/odds/event/{id}.

Tokens

Each account has one pool of tokens that resets every month. A call's price depends on the product and the filters, never on the size of the reply. GET /v2/pricing returns the price table, Pricing explains it with examples, and quote=true prices any request for free.

Free: discovery endpoints, /v2/usage, /v2/pricing, quotes, 304 replies, empty replies and every error. A call that costs more than your balance returns 402 insufficient_tokens and nothing is charged. The body includes cost, plan, quote, rate_card, remaining, resets_at and upgrade_url, so you can narrow the filters or wait for the reset. On a custom Enterprise plan there is no upgrade_url.

HeaderDescription
X-Tokens-CostWhat this reply cost; 0 when it was free. Sent on every reply to a valid key.
X-Tokens-UsedTokens used this period.
X-Tokens-RemainingTokens left this period, or unlimited.
X-Tokens-LimitYour allowance for the period, or unlimited.
X-Tokens-ResetWhen the allowance resets (ISO 8601, UTC). On a trial, the trial's end if that is sooner.

The last four are sent whenever the balance is known, so treat them as optional. See Tokens, quotes and rate limits.

Rate limits

Each account is limited per 10 seconds, per minute, and in concurrent requests; GET /v2/usage shows your limits. Going over returns 429 too_many_requests with a Retry-After header. Every authenticated request counts, including 304s. Before a key is checked, each IP address has its own limit (429 too_many_from_address).

HeaderDescription
X-RateLimit-LimitYour limit for the window closest to running out: 10 seconds or 1 minute.
X-RateLimit-RemainingRequests left in that window.
X-RateLimit-ResetSeconds until that window resets.
Retry-AfterOn every 429 and 503: seconds to wait. Never sent on a 402.

Our edge also limits each IP address and the service as a whole. Its 429 and 502 replies have an empty body and no error code; honour Retry-After and back off.

Compression and caching

Send Accept-Encoding: gzip (the quickstart's --compressed does this) to get bodies over 1 kB compressed. Without it you get plain bodies. standard, dutching and raw refuse a request that rules gzip out (Accept-Encoding: identity) with a 400. Python's urllib sends that by default, so set Accept-Encoding: gzip yourself. /healthz, /v2/status, /v2/health, /v2/usage, quotes and errors are always plain.

Odds replies, /v2/sports, /v2/events and /v2/bookmakers carry an ETag. Send it back exactly as you received it, quotes included, in If-None-Match. If nothing has changed you get 304with no body: it costs no tokens but still counts towards your rate limit. A call your balance can't cover returns 402 even with If-None-Match.

Errors

Errors return JSON: {"error": {"code", "message", "request_id"}}, plus any extra fields listed below. Branch on code, not message, which may change.

StatusCodeWhen
400bad_requestUnknown parameter; Gzip refused on standard, dutching or raw; Expired cursor
400missing_paramRequired parameter missing
400region_requiredNo region and the key lacks uk
400unknown_bookmakerUnknown bookmaker (valid trimmed)Also returns: valid
400unknown_exchangeA bookmaker named in exchangesAlso returns: valid
400unknown_marketUnknown market (valid trimmed)Also returns: valid
400unknown_sportUnknown sport (valid trimmed)Also returns: valid
401expired_api_keyExpired key
401invalid_api_keyKey not recognised
401missing_api_keyNo key
401paused_api_keyPaused key
401revoked_api_keyRevoked key
402insufficient_tokensA public plan but business; Plan business; A custom planAlso returns: cost, plan, quote, rate_card, remaining, resets_at, upgrade_url
403account_suspendedSuspended accountAlso returns: dashboard_url, reason
403bookmaker_not_in_planVenue outside the plan's bookmakersAlso returns: valid
403bookmaker_not_in_regionVenue outside the regionAlso returns: type
403not_in_key_scopeProduct outside the keyAlso returns: type
403origin_not_allowedBrowser key off its website
403product_not_in_regionProduct the region does not serveAlso returns: type
403region_not_in_scopeRegion outside the keyAlso returns: type
403schema_not_in_scopeKey pinned to another API versionAlso returns: type
404not_foundNo such route
404unknown_eventEvent not on the raw board
404unknown_regionUnknown region
404unknown_typeUnknown board
405method_not_allowedA method other than GET
429too_many_from_addressThe per-address shield
429too_many_requestsThe account's flood cap
500internal_errorInternal error
503raw_not_enabledRaw board not held
503temporarily_unavailableBoard or registry not ready

For help with an error, contact us and include the request_id from the reply.

Versioning

This is API version 2026-07-08, returned in the X-OddsRelay-Api-Version header. Within /v2we only add things: endpoints, optional fields, enum values and error codes, so ignore fields you don't recognise. A breaking change ships as /v3, announced by email and in the changelog at least 6 months ahead, and /v2 keeps running for at least 12 months after that.

More

Guides

  • Example client: A complete client in cURL, Python and JavaScript: quote, call, read the token headers and poll with ETags.
  • Tokens, quotes and rate limits: How a call is priced, how filters lower the price, and how to handle a 402 or a 429.
  • Filtering matched odds: The sports, bookmakers, exchanges and markets filters, the kick-off window, and what a filtered reply leaves out.
  • How fresh a price is: The timestamps each reply carries and how to work out a price's age.
  • Reading coverage: What /v2/coverage returns with and without a key, and what live and interrupted mean.
  • Paging through raw odds: Page through raw odds with a cursor, handle expiry, and fetch only what changed.
  • Polling with ETags: Send If-None-Match to get a free 304 when nothing has changed.
  • Converting to American odds: Which products return American odds, and a function to convert the rest.

Use with an AI assistant

Copy this prompt into your coding assistant and replace the last line with what you want to build. It covers keys, requests, tokens, caching and every error code. Tools that read docs directly can use llms.txt and llms-full.txt.

Show the prompt

Prompt

You are integrating the OddsRelay odds API (version 2026-07-08). Follow this contract exactly.

BASE URL
  https://api.oddsrelay.io   (every route is GET; the full contract is https://api.oddsrelay.io/v2/openapi.json)

KEYS
  Send the key as Authorization: Bearer <key> or x-api-key: <key>, never in a query string.
  Server keys start or_live_. Read the key from an environment variable; never ship it to a browser.

THE CALL
  GET /v2/odds/{type}?region=uk   products: standard, 2up, dutching, each-way, extra-place, bog
  GET /v2/odds/raw?region=uk      every bookmaker's prices, paged (meta.next_cursor; a cursor expires after 300 s)
  On data routes region is one lowercase code; missing means uk (400 region_required if your key lacks uk).
  Discovery routes like /v2/bookmakers take no region. Any parameter a route doesn't read is 400.
  Filters on the matched products: sports (<= 20), bookmakers (<= 40), exchanges (<= 4), markets (<= 20),
  commenceTimeFrom / commenceTimeTo. Discovery lists the values: GET /v2/sports, /v2/bookmakers.

TOKENS
  Every data call costs tokens, priced from the request (product and filters), never the reply's size.
  Add quote=true to any data request to get {cost, remaining, rate_card} for free, before a big call.
  Read X-Tokens-Cost on replies to a valid key; X-Tokens-Remaining and X-Tokens-Reset come when the balance is
  known, so treat them as optional.
  402 insufficient_tokens means the price is above the balance: narrow the filters or wait for resets_at (a trial's
  end, on a trial). upgrade_url is absent on a custom plan.

DELIVERY
  Accept gzip and check Content-Encoding: standard, dutching and raw answer a request that refuses gzip
  (Accept-Encoding: identity) with 400. Status, health, usage, quotes and errors are always plain.
  Store each ETag exactly as sent (quotes included) and send it back as If-None-Match: 304 means unchanged,
  no body, no tokens. Quotes carry no ETag.
  Rate limits are per account (X-RateLimit-*). On 429 honour Retry-After; our edge's own 429 and 502
  have an empty body.

RESPONSE SHAPE (matched products)
  { "meta": { "feed_type", "region", "odds_format", "processed_at", "last_seen", "count",
              "version", "next_cursor" },
    "data": [ { "event_id", "sport_key", "sport_title", "commence_time", "home_team", "away_team",
                "markets": [ { "key", "outcomes": [ { "name", "back": [...], "lay": [...] } ] } ] } ] }
  lay is an array on standard, 2up and bog; an object {win: [...], place: [...]} on each-way and
  extra-place; absent on dutching, where back legs carry dutch_id (group by it; a dutch rates 100 / sum(1/price)).
  Event ids are stable across routes but re-key once when a higher-ranked exchange lists the fixture: don't
  store them as permanent keys. Every level can be empty; walk it defensively.

ERRORS
  Every JSON error is { "error": { "code", "message", "request_id" } }, plus the extra fields listed.
    400  bad_request, missing_param, region_required, unknown_bookmaker (valid), unknown_exchange (valid), unknown_market (valid), unknown_sport (valid)
         -> fix the request; do not retry.
    401  expired_api_key, invalid_api_key, missing_api_key, paused_api_key, revoked_api_key
         -> stop and surface it; do not retry.
    402  insufficient_tokens (cost, plan, quote, rate_card, remaining, resets_at, upgrade_url)
         -> out of tokens: add filters or wait for X-Tokens-Reset; price a call first with quote=true.
    403  account_suspended (dashboard_url, reason), bookmaker_not_in_plan (valid), bookmaker_not_in_region (type), not_in_key_scope (type), origin_not_allowed, product_not_in_region (type), region_not_in_scope (type), schema_not_in_scope (type)
         -> refused for this account, key, region or plan; do not retry.
    404  not_found, unknown_event, unknown_region, unknown_type
         -> a bad path or value.
    405  method_not_allowed
         -> use GET.
    429  too_many_from_address, too_many_requests
         -> back off and honour Retry-After.
    500  internal_error
         -> transient; retry with backoff.
    503  temporarily_unavailable
         -> transient; retry with backoff, honouring Retry-After.
    503  raw_not_enabled
         -> raw odds are not served here: stop and report it; do not retry.
  Branch on code, never on message. Log request_id.

WHAT TO BUILD
  [Describe what you want here.]

Widgets

The oddsmatcher widget shows live odds on your own site with one script tag. Live odds need a browser key (or_pub_), available on the Enterprise plan; the calculators need no key. See Widgets.

Reference and contract