openapi: 3.2.0 info: title: OddsRelay Account API version: '2026-07-08' x-verified: '2026-10-08' description: 'The OddsRelay odds API: matched boards, the raw board, discovery, coverage and status, as served at `https://api.oddsrelay.io`.' servers: - url: https://api.oddsrelay.io security: - bearerAuth: [] - apiKeyHeader: [] tags: - name: Account description: Usage and prices. Free. paths: /v2/usage: get: operationId: getUsage summary: Your plan, balance, key, flood cap and scope description: Free, flood-capped, and answered for a SUSPENDED account too (it shows the reason). Plain JSON. `scope` and `scope_version` are your account's scope (both null when it has none); `key.products` is your key's products within it. tags: - Account parameters: [] responses: '200': description: Usage. headers: X-OddsRelay-Api-Version: description: 'Always sent: #/components/headers/X-OddsRelay-Api-Version' required: true schema: type: string Cache-Control: description: 'Always sent: #/components/headers/Cache-Control' required: true schema: type: string Vary: description: 'Always sent: #/components/headers/Vary' required: true schema: type: string X-RateLimit-Limit: description: 'Always sent: #/components/headers/X-RateLimit-Limit' required: true schema: type: string pattern: ^[0-9]+$ X-RateLimit-Remaining: description: 'Always sent: #/components/headers/X-RateLimit-Remaining' required: true schema: type: string pattern: ^[0-9]+$ X-RateLimit-Reset: description: 'Always sent: #/components/headers/X-RateLimit-Reset' required: true schema: type: string pattern: ^[0-9]+$ X-Tokens-Cost: description: 'Always sent: #/components/headers/X-Tokens-Cost' required: true schema: type: string pattern: ^[0-9]+$ X-Tokens-Used: $ref: '#/components/headers/X-Tokens-Used' X-Tokens-Remaining: $ref: '#/components/headers/X-Tokens-Remaining' X-Tokens-Limit: $ref: '#/components/headers/X-Tokens-Limit' X-Tokens-Reset: $ref: '#/components/headers/X-Tokens-Reset' X-OddsRelay-Request-Id: description: 'Always sent: #/components/headers/X-OddsRelay-Request-Id' required: true schema: type: string content: application/json: schema: $ref: '#/components/schemas/Usage' examples: usage: summary: GET /v2/usage value: plan: free rate_card: 2026-09 account: status: active suspension_reason: null test: true tokens: used: 0 remaining: unlimited limit: unlimited resets_at: '2026-10-16T21:02:31Z' regions: - uk key: id: key_XXXXXXXXXXXX kind: server products: - standard - 2up - dutching - each-way - extra-place - bog - raw regions: - uk token_cap: null tokens_used: null flood_cap: per_10s: 0 per_min: 0 scope: null scope_version: null description: A live reply; the key id, token count and flood-cap numbers are placeholders. '400': $ref: '#/components/responses/BadRequest' description: Unknown or repeated parameter. '401': $ref: '#/components/responses/Unauthorized' description: '`missing_api_key`, `invalid_api_key` (also a closed account), `revoked_api_key`, `expired_api_key`, `paused_api_key`.' '403': $ref: '#/components/responses/Forbidden' description: '`origin_not_allowed`.' '429': $ref: '#/components/responses/TooManyRequests' description: '`too_many_from_address`, then after authentication `too_many_requests`; the front door''s own caps answer an empty-bodied 429.' '500': $ref: '#/components/responses/InternalError' description: '`internal_error`.' '503': $ref: '#/components/responses/ServiceUnavailable' description: '`temporarily_unavailable`.' '502': $ref: '#/components/responses/FrontDoorError' /v2/pricing: get: operationId: getPricing summary: The price table description: The price table every call is priced from, byte for byte. Free. `ETag` is its SHA-256 (`-gz` on the gzip copy); 304 on a match. Gzip unless refused. tags: - Account parameters: [] responses: '200': description: The table. headers: X-OddsRelay-Api-Version: description: 'Always sent: #/components/headers/X-OddsRelay-Api-Version' required: true schema: type: string Cache-Control: description: 'Always sent: #/components/headers/Cache-Control' required: true schema: type: string Vary: description: 'Always sent: #/components/headers/Vary' required: true schema: type: string X-RateLimit-Limit: description: 'Always sent: #/components/headers/X-RateLimit-Limit' required: true schema: type: string pattern: ^[0-9]+$ X-RateLimit-Remaining: description: 'Always sent: #/components/headers/X-RateLimit-Remaining' required: true schema: type: string pattern: ^[0-9]+$ X-RateLimit-Reset: description: 'Always sent: #/components/headers/X-RateLimit-Reset' required: true schema: type: string pattern: ^[0-9]+$ X-Tokens-Cost: description: 'Always sent: #/components/headers/X-Tokens-Cost' required: true schema: type: string pattern: ^[0-9]+$ X-Tokens-Used: $ref: '#/components/headers/X-Tokens-Used' X-Tokens-Remaining: $ref: '#/components/headers/X-Tokens-Remaining' X-Tokens-Limit: $ref: '#/components/headers/X-Tokens-Limit' X-Tokens-Reset: $ref: '#/components/headers/X-Tokens-Reset' ETag: description: 'Always sent: #/components/headers/ETag' required: true schema: type: string X-OddsRelay-Request-Id: description: 'Always sent: #/components/headers/X-OddsRelay-Request-Id' required: true schema: type: string Content-Encoding: $ref: '#/components/headers/Content-Encoding' content: application/json: schema: $ref: '#/components/schemas/PriceTable' examples: table: summary: GET /v2/pricing value: rate_card: 2026-09 provisional: true captured_at: '2026-09-16T12:01:45Z' recent: max_age_s: 0 factor: 0 regions: uk: products: standard: whole: 0 dims: - sports - bookmakers - exchanges - markets gz_bytes: 0 points: standard: sports: soccer: 0 tennis: 0 _other: 0 description: Trimmed from a live reply. Numbers are placeholders. '304': description: 'Not modified: no body.' headers: X-OddsRelay-Api-Version: description: 'Always sent: #/components/headers/X-OddsRelay-Api-Version' required: true schema: type: string Cache-Control: description: 'Always sent: #/components/headers/Cache-Control' required: true schema: type: string Vary: description: 'Always sent: #/components/headers/Vary' required: true schema: type: string X-RateLimit-Limit: description: 'Always sent: #/components/headers/X-RateLimit-Limit' required: true schema: type: string pattern: ^[0-9]+$ X-RateLimit-Remaining: description: 'Always sent: #/components/headers/X-RateLimit-Remaining' required: true schema: type: string pattern: ^[0-9]+$ X-RateLimit-Reset: description: 'Always sent: #/components/headers/X-RateLimit-Reset' required: true schema: type: string pattern: ^[0-9]+$ X-Tokens-Cost: description: 'Always sent: #/components/headers/X-Tokens-Cost' required: true schema: type: string pattern: ^[0-9]+$ X-Tokens-Used: $ref: '#/components/headers/X-Tokens-Used' X-Tokens-Remaining: $ref: '#/components/headers/X-Tokens-Remaining' X-Tokens-Limit: $ref: '#/components/headers/X-Tokens-Limit' X-Tokens-Reset: $ref: '#/components/headers/X-Tokens-Reset' ETag: description: 'Always sent: #/components/headers/ETag' required: true schema: type: string '400': $ref: '#/components/responses/BadRequest' description: Unknown or repeated parameter. '401': $ref: '#/components/responses/Unauthorized' description: '`missing_api_key`, `invalid_api_key` (also a closed account), `revoked_api_key`, `expired_api_key`, `paused_api_key`.' '403': $ref: '#/components/responses/Forbidden' description: '`account_suspended`, `origin_not_allowed`.' '429': $ref: '#/components/responses/TooManyRequests' description: '`too_many_from_address`, then after authentication `too_many_requests`; the front door''s own caps answer an empty-bodied 429.' '500': $ref: '#/components/responses/InternalError' description: '`internal_error`.' '503': $ref: '#/components/responses/ServiceUnavailable' description: '`temporarily_unavailable`.' '502': $ref: '#/components/responses/FrontDoorError' components: examples: temporarily_unavailable: summary: Board or registry not ready value: error: code: temporarily_unavailable message: Feed temporarily unavailable. request_id: or_req_00000000000000000000000000000000 too_many_from_address: summary: The per-address shield value: error: code: too_many_from_address message: Too many requests from this address. request_id: or_req_00000000000000000000000000000000 expired_api_key: summary: Expired key value: error: code: expired_api_key message: This API key has expired. request_id: or_req_00000000000000000000000000000000 too_many_requests: summary: The account's flood cap value: error: code: too_many_requests message: Too many requests for this account. request_id: or_req_00000000000000000000000000000000 revoked_api_key: summary: Revoked key value: error: code: revoked_api_key message: This API key has been revoked. request_id: or_req_00000000000000000000000000000000 raw_not_enabled: summary: Raw board not held value: error: code: raw_not_enabled message: The raw feed is not enabled on this deployment. request_id: or_req_00000000000000000000000000000000 bad_request_cursor: summary: Expired cursor value: error: code: bad_request message: 'cursor expired: start again from the first page' request_id: or_req_00000000000000000000000000000000 exchange_not_in_plan: summary: Exchange outside the account's scope value: error: code: exchange_not_in_plan message: '''smarkets'' is not in your plan''s exchanges for standard: ''valid'' lists the ones it serves in this region. GET /v2/usage shows your account''s scope.' request_id: or_req_00000000000000000000000000000000 valid: - betfair_exchange market_not_in_plan: summary: Market outside the account's scope value: error: code: market_not_in_plan message: '''double_chance'' is not in your plan''s markets for standard: ''valid'' lists them. GET /v2/usage shows your account''s scope.' request_id: or_req_00000000000000000000000000000000 valid: - btts - h2h - totals account_suspended: summary: Suspended account value: error: code: account_suspended message: This account is suspended because of a payment problem. request_id: or_req_00000000000000000000000000000000 reason: payment dashboard_url: https://app.oddsrelay.io/billing region_required: summary: No region and the key lacks uk value: error: code: region_required message: This key does not cover uk, the region a call without ?region= reads. Add ?region=za (one of your key's regions). request_id: or_req_00000000000000000000000000000000 paused_api_key: summary: Paused key value: error: code: paused_api_key message: This API key is paused. request_id: or_req_00000000000000000000000000000000 product_not_in_region: summary: Product the region does not serve value: error: code: product_not_in_region message: Region 'za' does not serve 'standard'. request_id: or_req_00000000000000000000000000000000 type: product unknown_bookmaker: summary: Unknown bookmaker (valid trimmed) value: error: code: unknown_bookmaker message: Unknown bookmaker 'nope'. request_id: or_req_00000000000000000000000000000000 valid: - 7bet - 888sport - bet365 - bet600 - bet_target - betano unknown_sport: summary: Unknown sport (valid trimmed) value: error: code: unknown_sport message: 'Unknown sport ''curling_xyz''. A sport or a competition key: full list: /v2/sports.' request_id: or_req_00000000000000000000000000000000 valid: - american_football - aussie_rules - badminton - baseball - basketball - boxing missing_param: summary: Required parameter missing value: error: code: missing_param message: Required query parameter 'sport' is missing. request_id: or_req_00000000000000000000000000000000 bookmaker_not_in_region: summary: Venue outside the region value: error: code: bookmaker_not_in_region message: '''william_hill'' is not a venue in region ''za'': GET /v2/bookmakers lists each venue''s regions.' request_id: or_req_00000000000000000000000000000000 type: region unknown_market: summary: Unknown market on this product, carried by others (valid trimmed) value: error: code: unknown_market message: 'Unknown market ''each_way''. Carried by: /v2/odds/each-way, /v2/odds/extra-place.' request_id: or_req_00000000000000000000000000000000 valid: - 1st_half_away_team_total_goals - 1st_half_away_team_total_points - 1st_half_btts - 1st_half_correct_score - 1st_half_double_chance - 1st_half_handicap_goals products: - each-way - extra-place origin_not_allowed: summary: Browser key off its website value: error: code: origin_not_allowed message: This key may not be used from this website. request_id: or_req_00000000000000000000000000000000 bookmaker_not_in_plan: summary: Venue outside the plan's bookmakers value: error: code: bookmaker_not_in_plan message: '''coral'' is not in your plan''s bookmakers: ''valid'' lists the ones it serves in this region.' request_id: or_req_00000000000000000000000000000000 valid: - bet365 - william_hill missing_api_key: summary: No key value: error: code: missing_api_key message: 'No API key presented. Send Authorization: Bearer or_live_… or x-api-key.' request_id: or_req_00000000000000000000000000000000 not_in_key_scope: summary: Product outside the key value: error: code: not_in_key_scope message: This key does not include 'raw'. request_id: or_req_00000000000000000000000000000000 type: product unknown_exchange: summary: A bookmaker named in exchanges value: error: code: unknown_exchange message: 'Unknown exchange ''william_hill''. ''william_hill'' is a bookmaker: use bookmakers=william_hill.' request_id: or_req_00000000000000000000000000000000 valid: - betdaq - betfair_exchange - matchbook - smarkets schema_not_in_scope: summary: Key pinned to another API version value: error: code: schema_not_in_scope message: Key not scoped for the 'v2' API. Use the version your key is scoped to. request_id: or_req_00000000000000000000000000000000 type: schema internal_error: summary: Internal error value: error: code: internal_error message: Internal error. request_id: or_req_00000000000000000000000000000000 bad_request_param: summary: Unknown parameter value: error: code: bad_request message: 'Unknown parameter ''foo'': /v2/odds/{type} accepts bookmakers, commenceTimeFrom, commenceTimeTo, dateFormat, exchanges, format, markets, oddsFormat, quote, region, sports.' request_id: or_req_00000000000000000000000000000000 bad_request_gzip: summary: Gzip refused on standard, dutching or raw value: error: code: bad_request message: 'add Accept-Encoding: gzip; with curl use --compressed' request_id: or_req_00000000000000000000000000000000 region_not_in_scope: summary: Region outside the key value: error: code: region_not_in_scope message: Key not scoped for region 'za'. request_id: or_req_00000000000000000000000000000000 type: region invalid_api_key: summary: Key not recognised value: error: code: invalid_api_key message: API key not recognised. request_id: or_req_00000000000000000000000000000000 sport_not_in_plan: summary: Sport outside the account's scope value: error: code: sport_not_in_plan message: '''tennis'' is not in your plan''s sports for standard: ''valid'' lists them. GET /v2/usage shows your account''s scope.' request_id: or_req_00000000000000000000000000000000 valid: - horse_racing - soccer schemas: Error403: allOf: - $ref: '#/components/schemas/ErrorBody' - type: object properties: error: type: object properties: code: type: string enum: - account_suspended - not_in_key_scope - product_not_in_region - origin_not_allowed - region_not_in_scope - bookmaker_not_in_region - bookmaker_not_in_plan - sport_not_in_plan - market_not_in_plan - exchange_not_in_plan - schema_not_in_scope Error503: allOf: - $ref: '#/components/schemas/ErrorBody' - type: object properties: error: type: object properties: code: type: string enum: - temporarily_unavailable - raw_not_enabled Error400: allOf: - $ref: '#/components/schemas/ErrorBody' - type: object properties: error: type: object properties: code: type: string enum: - bad_request - missing_param - region_required - unknown_sport - unknown_market - unknown_bookmaker - unknown_exchange PriceTable: type: object required: - rate_card - recent - regions description: The price table the feed prices every call from, byte for byte. A call costs max(1, ceil(whole x each named filter's points / 100)). properties: rate_card: type: string provisional: type: boolean captured_at: type: string recent: type: object properties: max_age_s: type: number factor: type: number regions: type: object Error500: allOf: - $ref: '#/components/schemas/ErrorBody' - type: object properties: error: type: object properties: code: type: string enum: - internal_error ErrorBody: type: object required: - error description: Every JSON 4xx and 5xx this API writes. properties: error: type: object required: - code - message - request_id properties: code: type: string description: Stable machine code; branch on this, never on `message`. message: type: string description: Human text; may change. request_id: type: string description: '`or_req_` + 32 hex, as `X-OddsRelay-Request-Id`.' type: type: string enum: - product - region - schema description: 'Scope 403s only: which axis refused (`not_in_key_scope` and `product_not_in_region` product; `region_not_in_scope` and `bookmaker_not_in_region` region; `schema_not_in_scope` schema).' valid: type: array items: type: string description: '`unknown_sport` (sport key bases), `unknown_bookmaker`, `unknown_exchange`, `bookmaker_not_in_plan` (your plan''s bookmakers, or your account scope''s, in the call''s region), `sport_not_in_plan`, `market_not_in_plan` and `exchange_not_in_plan` (the scope''s list for that product): the values accepted, sorted. `unknown_market`: the market keys this product carries, sorted (keyless `/v2/coverage`''s `markets`); a key another product carries may also be accepted on this route, and matches nothing here.' products: type: array items: type: string description: '`unknown_market` only, when another product carries the market: those products, sorted, as the `{type}` of `/v2/odds/{type}` (or `raw`). Left out when no product carries it.' reason: type: string enum: - payment - terms description: '`account_suspended` only.' dashboard_url: type: string description: '`account_suspended` only.' Usage: type: object required: - plan - rate_card - account - tokens - regions - key - flood_cap - scope - scope_version properties: plan: type: - string - 'null' rate_card: type: string account: type: object required: - status - suspension_reason - test properties: status: type: string enum: - active - suspended suspension_reason: type: - string - 'null' test: type: - boolean - 'null' tokens: type: object required: - used - remaining - limit - resets_at properties: used: type: - integer - 'null' remaining: type: - integer - string - 'null' limit: type: - integer - string - 'null' resets_at: type: - string - 'null' description: When the pool resets; on a trial, the trial's end when that comes first. description: '`remaining` and `limit` read `unlimited` on an unlimited account; null while the balance is unknown.' regions: type: array items: type: string key: type: object required: - id - kind - products - regions - token_cap - tokens_used properties: id: type: string kind: type: string enum: - server - browser - dashboard products: type: array items: type: string regions: type: array items: type: string token_cap: type: 'null' tokens_used: type: 'null' flood_cap: type: object required: - per_10s - per_min properties: per_10s: type: integer per_min: type: integer scope: type: - object - 'null' description: 'Your account''s scope, or null when it has none: `{"products": {: {: [values] | null}}}`. A product not listed is not readable; within one, a null axis means every value. The axes are each product''s filters: `sports`, `markets`, `bookmakers`, `exchanges` on the six matched boards; `sports`, `markets`, `bookmakers` (venues of both kinds) on `raw`, which also answers `/v2/odds/event/{id}`. Products and axes are always listed in that order and each list sorted. A value a call names outside it is a 403 `*_not_in_plan` with `valid`; a filter left out is trimmed to it.' scope_version: type: - integer - 'null' description: The version of `scope`, raised by each change; null when the account has no scope. Error429: allOf: - $ref: '#/components/schemas/ErrorBody' - type: object properties: error: type: object properties: code: type: string enum: - too_many_requests - too_many_from_address Error401: allOf: - $ref: '#/components/schemas/ErrorBody' - type: object properties: error: type: object properties: code: type: string enum: - missing_api_key - invalid_api_key - revoked_api_key - expired_api_key - paused_api_key responses: Forbidden: description: '403: the account, the key''s scope, the region, the plan or the account''s scope refuses this call.' headers: X-OddsRelay-Api-Version: description: The API version label, equal to this document's `info.version`. On every reply but `/healthz`, a `HEAD`'s 405 and the empty-bodied 429s and 502. required: true schema: type: string X-OddsRelay-Request-Id: description: 'This request''s id (`or_req_` + 32 hex), the same value as an error body''s `request_id`: quote it when you contact us. On data and discovery 200s, quotes, `/v2/usage` and `/v2/pricing` 200s and every JSON error but a 405; not on 304s, `/v2/health`, `/v2/regions`, `/v2/coverage` 200s or `/v2/odds/event/{id}` 200s.' required: true schema: type: string Vary: $ref: '#/components/headers/Vary' X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' X-Tokens-Cost: $ref: '#/components/headers/X-Tokens-Cost' X-Tokens-Used: $ref: '#/components/headers/X-Tokens-Used' X-Tokens-Remaining: $ref: '#/components/headers/X-Tokens-Remaining' X-Tokens-Limit: $ref: '#/components/headers/X-Tokens-Limit' X-Tokens-Reset: $ref: '#/components/headers/X-Tokens-Reset' Access-Control-Allow-Origin: $ref: '#/components/headers/Access-Control-Allow-Origin' content: application/json: schema: $ref: '#/components/schemas/Error403' examples: account_suspended: $ref: '#/components/examples/account_suspended' not_in_key_scope: $ref: '#/components/examples/not_in_key_scope' product_not_in_region: $ref: '#/components/examples/product_not_in_region' origin_not_allowed: $ref: '#/components/examples/origin_not_allowed' region_not_in_scope: $ref: '#/components/examples/region_not_in_scope' bookmaker_not_in_region: $ref: '#/components/examples/bookmaker_not_in_region' bookmaker_not_in_plan: $ref: '#/components/examples/bookmaker_not_in_plan' sport_not_in_plan: $ref: '#/components/examples/sport_not_in_plan' market_not_in_plan: $ref: '#/components/examples/market_not_in_plan' exchange_not_in_plan: $ref: '#/components/examples/exchange_not_in_plan' schema_not_in_scope: $ref: '#/components/examples/schema_not_in_scope' FrontDoorError: description: '502 from the front door when the API behind it does not answer: an empty body, no `code`. Retry with backoff.' TooManyRequests: description: '429 with `Retry-After`. `too_many_requests`: the account''s flood cap or its in-flight cap (with `X-RateLimit-*`). `too_many_from_address`: the per-address shield, before authentication (no token or flood-cap headers). The front door''s own per-address and global caps also answer 429 with `Retry-After`, but with an EMPTY body and no `code`.' headers: X-OddsRelay-Api-Version: description: The API version label, equal to this document's `info.version`. On every reply but `/healthz`, a `HEAD`'s 405 and the empty-bodied 429s and 502. required: true schema: type: string X-OddsRelay-Request-Id: description: 'This request''s id (`or_req_` + 32 hex), the same value as an error body''s `request_id`: quote it when you contact us. On data and discovery 200s, quotes, `/v2/usage` and `/v2/pricing` 200s and every JSON error but a 405; not on 304s, `/v2/health`, `/v2/regions`, `/v2/coverage` 200s or `/v2/odds/event/{id}` 200s.' required: true schema: type: string Vary: $ref: '#/components/headers/Vary' Retry-After: description: 'Seconds to wait before retrying: on every 429 and 503. A 402 never carries it.' required: true schema: type: string X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' X-Tokens-Cost: $ref: '#/components/headers/X-Tokens-Cost' X-Tokens-Used: $ref: '#/components/headers/X-Tokens-Used' X-Tokens-Remaining: $ref: '#/components/headers/X-Tokens-Remaining' X-Tokens-Limit: $ref: '#/components/headers/X-Tokens-Limit' X-Tokens-Reset: $ref: '#/components/headers/X-Tokens-Reset' Access-Control-Allow-Origin: $ref: '#/components/headers/Access-Control-Allow-Origin' content: application/json: schema: $ref: '#/components/schemas/Error429' examples: too_many_requests: $ref: '#/components/examples/too_many_requests' too_many_from_address: $ref: '#/components/examples/too_many_from_address' InternalError: description: 500 `internal_error`. If it came after a spend, `X-Tokens-Cost` reads 0 and the spend stands. headers: X-OddsRelay-Api-Version: description: The API version label, equal to this document's `info.version`. On every reply but `/healthz`, a `HEAD`'s 405 and the empty-bodied 429s and 502. required: true schema: type: string X-OddsRelay-Request-Id: description: 'This request''s id (`or_req_` + 32 hex), the same value as an error body''s `request_id`: quote it when you contact us. On data and discovery 200s, quotes, `/v2/usage` and `/v2/pricing` 200s and every JSON error but a 405; not on 304s, `/v2/health`, `/v2/regions`, `/v2/coverage` 200s or `/v2/odds/event/{id}` 200s.' required: true schema: type: string Vary: $ref: '#/components/headers/Vary' X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' X-Tokens-Cost: $ref: '#/components/headers/X-Tokens-Cost' X-Tokens-Used: $ref: '#/components/headers/X-Tokens-Used' X-Tokens-Remaining: $ref: '#/components/headers/X-Tokens-Remaining' X-Tokens-Limit: $ref: '#/components/headers/X-Tokens-Limit' X-Tokens-Reset: $ref: '#/components/headers/X-Tokens-Reset' Access-Control-Allow-Origin: $ref: '#/components/headers/Access-Control-Allow-Origin' content: application/json: schema: $ref: '#/components/schemas/Error500' examples: internal_error: $ref: '#/components/examples/internal_error' Unauthorized: description: '401: no key, or a key that is not accepted. Carries `WWW-Authenticate`; no token or flood-cap headers.' headers: X-OddsRelay-Api-Version: description: The API version label, equal to this document's `info.version`. On every reply but `/healthz`, a `HEAD`'s 405 and the empty-bodied 429s and 502. required: true schema: type: string X-OddsRelay-Request-Id: description: 'This request''s id (`or_req_` + 32 hex), the same value as an error body''s `request_id`: quote it when you contact us. On data and discovery 200s, quotes, `/v2/usage` and `/v2/pricing` 200s and every JSON error but a 405; not on 304s, `/v2/health`, `/v2/regions`, `/v2/coverage` 200s or `/v2/odds/event/{id}` 200s.' required: true schema: type: string Vary: $ref: '#/components/headers/Vary' WWW-Authenticate: description: On every 401. required: true schema: type: string content: application/json: schema: $ref: '#/components/schemas/Error401' examples: missing_api_key: $ref: '#/components/examples/missing_api_key' invalid_api_key: $ref: '#/components/examples/invalid_api_key' revoked_api_key: $ref: '#/components/examples/revoked_api_key' expired_api_key: $ref: '#/components/examples/expired_api_key' paused_api_key: $ref: '#/components/examples/paused_api_key' ServiceUnavailable: description: '503 with `Retry-After`: `temporarily_unavailable` (a board, the key registry or a connection is not ready; retry) or `raw_not_enabled` (the key may use raw but the raw board is not held here).' headers: X-OddsRelay-Api-Version: description: The API version label, equal to this document's `info.version`. On every reply but `/healthz`, a `HEAD`'s 405 and the empty-bodied 429s and 502. required: true schema: type: string X-OddsRelay-Request-Id: description: 'This request''s id (`or_req_` + 32 hex), the same value as an error body''s `request_id`: quote it when you contact us. On data and discovery 200s, quotes, `/v2/usage` and `/v2/pricing` 200s and every JSON error but a 405; not on 304s, `/v2/health`, `/v2/regions`, `/v2/coverage` 200s or `/v2/odds/event/{id}` 200s.' required: true schema: type: string Vary: $ref: '#/components/headers/Vary' Retry-After: description: 'Seconds to wait before retrying: on every 429 and 503. A 402 never carries it.' required: true schema: type: string X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' X-Tokens-Cost: $ref: '#/components/headers/X-Tokens-Cost' X-Tokens-Used: $ref: '#/components/headers/X-Tokens-Used' X-Tokens-Remaining: $ref: '#/components/headers/X-Tokens-Remaining' X-Tokens-Limit: $ref: '#/components/headers/X-Tokens-Limit' X-Tokens-Reset: $ref: '#/components/headers/X-Tokens-Reset' Access-Control-Allow-Origin: $ref: '#/components/headers/Access-Control-Allow-Origin' content: application/json: schema: $ref: '#/components/schemas/Error503' examples: temporarily_unavailable: $ref: '#/components/examples/temporarily_unavailable' raw_not_enabled: $ref: '#/components/examples/raw_not_enabled' BadRequest: description: '400: a parameter this route does not read or sent twice, a malformed value, gzip refused on standard, dutching or raw, or a filter value the catalogue does not know.' headers: X-OddsRelay-Api-Version: description: The API version label, equal to this document's `info.version`. On every reply but `/healthz`, a `HEAD`'s 405 and the empty-bodied 429s and 502. required: true schema: type: string X-OddsRelay-Request-Id: description: 'This request''s id (`or_req_` + 32 hex), the same value as an error body''s `request_id`: quote it when you contact us. On data and discovery 200s, quotes, `/v2/usage` and `/v2/pricing` 200s and every JSON error but a 405; not on 304s, `/v2/health`, `/v2/regions`, `/v2/coverage` 200s or `/v2/odds/event/{id}` 200s.' required: true schema: type: string Vary: $ref: '#/components/headers/Vary' X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' X-Tokens-Cost: $ref: '#/components/headers/X-Tokens-Cost' X-Tokens-Used: $ref: '#/components/headers/X-Tokens-Used' X-Tokens-Remaining: $ref: '#/components/headers/X-Tokens-Remaining' X-Tokens-Limit: $ref: '#/components/headers/X-Tokens-Limit' X-Tokens-Reset: $ref: '#/components/headers/X-Tokens-Reset' Access-Control-Allow-Origin: $ref: '#/components/headers/Access-Control-Allow-Origin' content: application/json: schema: $ref: '#/components/schemas/Error400' examples: bad_request_param: $ref: '#/components/examples/bad_request_param' bad_request_gzip: $ref: '#/components/examples/bad_request_gzip' bad_request_cursor: $ref: '#/components/examples/bad_request_cursor' missing_param: $ref: '#/components/examples/missing_param' region_required: $ref: '#/components/examples/region_required' unknown_sport: $ref: '#/components/examples/unknown_sport' unknown_market: $ref: '#/components/examples/unknown_market' unknown_bookmaker: $ref: '#/components/examples/unknown_bookmaker' unknown_exchange: $ref: '#/components/examples/unknown_exchange' headers: X-Tokens-Limit: description: The account's tokens for the period, or `unlimited`. Sent when the balance is known. schema: type: string pattern: ^([0-9]+|unlimited)$ example: unlimited X-Tokens-Remaining: description: Tokens left this period, or `unlimited` on an unlimited account. Sent when the balance is known. schema: type: string pattern: ^([0-9]+|unlimited)$ example: unlimited Content-Encoding: description: '`gzip` when the body is gzip (see the gzip rules in the description).' schema: type: string enum: - gzip example: gzip X-Tokens-Reset: description: When the pool resets (ISO-8601 UTC, whole seconds); on a trial, the trial's end when that comes first. Sent when the balance is known. schema: type: string example: '2026-10-01T00:00:00Z' Vary: description: 'Keyed replies: `Authorization, Accept-Encoding, Origin`. Keyless `/v2/coverage`: `Authorization, X-Api-Key, Accept-Encoding`. `/v2/openapi.json`: `Accept-Encoding`. JSON errors on `/v2` routes: `Origin`.' schema: type: string example: Authorization, Accept-Encoding, Origin Access-Control-Allow-Origin: description: '`*` on every GET reply of a keyless route (`/v2/status`, keyless `/v2/coverage`, `/v2/openapi.json`), refusals and errors included. A keyed reply never sends `*`: a browser or dashboard key''s reply echoes the exact request `Origin` when the key allows it, with `Access-Control-Expose-Headers`.' schema: type: string example: '*' X-RateLimit-Limit: description: The account's flood cap for the window with fewer requests left (per 10 seconds or per minute; your numbers are in `/v2/usage` `flood_cap`). On every reply to a valid key after authentication, 429 `too_many_requests` included; never on a 401, the per-address 429 or a keyless route. schema: type: string pattern: ^[0-9]+$ X-Tokens-Used: description: Tokens the account has used this period. Sent with the other balance headers whenever the balance is known (while the key registry is unreachable a reply can carry `X-Tokens-Cost` alone). schema: type: string pattern: ^[0-9]+$ X-RateLimit-Remaining: description: Requests left in that window. schema: type: string pattern: ^[0-9]+$ X-Tokens-Cost: description: 'Tokens this reply cost: 0 on a free reply (304, an empty reply, discovery, `/v2/usage`, `/v2/pricing`, a quote and every refusal). On every reply to a valid key, refusals included; never on a 401, on the per-address 429 or on a keyless route.' schema: type: string pattern: ^[0-9]+$ X-RateLimit-Reset: description: Seconds until that window ends. schema: type: string pattern: ^[0-9]+$ securitySchemes: bearerAuth: type: http scheme: bearer description: '`Authorization: Bearer or_live_…`' apiKeyHeader: type: apiKey in: header name: x-api-key description: The same key in `x-api-key`.