Overview

Oddsradarwire serves live and prematch odds for tennis, soccer, cricket, basketball and the rest of the traditional book, sourced from the BETRADAR feed.

Every selection carries a no-vig probability supplied by the provider, next to the priced odds. You do not have to derive it, and we do not compute it — see No-vig probability for exactly what that means and what it does not.

Base URL:

https://oddsradarwire.com

All responses are JSON, UTF-8, and cache-control: no-store. Prices change by the second; nothing here is safe to cache.

Authentication

Every endpoint except the liveness probe needs an API key. Three ways to send it, in the order they are checked:

curl -H "x-api-key: or_your_key" https://oddsradarwire.com/v1/fixtures
curl -H "Authorization: Bearer or_your_key" https://oddsradarwire.com/v1/fixtures
curl "https://oddsradarwire.com/v1/fixtures?key=or_your_key"

The query parameter exists because it is the only form that works in a browser address bar and in SSE clients that cannot set headers. Prefer the header everywhere else — query strings end up in logs and referrers.

A bad header is not retried against ?key=. If you send a header, that is the credential that is checked.

Rate limits

Limits are per key, enforced per second, per minute and per day depending on plan. GET /v1/health and GET /v1/me do not count against them, so polling your own status never costs you quota.

A limited request returns 429 with a Retry-After header in seconds, and a body naming the window that rejected it:

{
  "error": "rate_limited",
  "window": "minute",
  "limit": 20,
  "retry_after_ms": 41200,
  "message": "Rate limit reached for the \"trial\" plan (20 per minute)..."
}

Honor Retry-After. A rejected request is not counted against your other windows, so backing off correctly costs you nothing.

Errors

StatuserrorMeaning
401invalid_keyMissing, unknown or revoked key.
403plan_lacks_sseYour plan does not include /v1/stream. Poll /v1/fixtures instead.
404fixture not foundNo such fixture on the board. Fixtures leave the board when they finish.
405method not allowedThe API is read-only. Everything is GET.
429rate_limitedSee Rate limits.
503feed_unavailableThe upstream feed is unreachable right now. Retry shortly — this is not a client error.

GET /v1/fixtures

The board: every fixture we currently hold, with markets and prices.

curl -H "x-api-key: $KEY" \
  "https://oddsradarwire.com/v1/fixtures?event_type=live&tier=1&limit=50"
ParameterValuesMeaning
event_typelive · prematchWhich board.
sporte.g. TENNIS, CS2One sport.
statusfixture statusFilter by fixture state.
tier1 · 2 · 3A ceiling, not an exact match: tier=2 returns tiers 1 and 2. See tiers.
canonicalcomma separatede.g. main_total,match_winner.
includeclosed · suspended · resultedUntradeable markets, off by default. See What is tradeable.
limitdefault 100, max 1000Cap on fixtures returned.

An unrecognised include or an out-of-range tier is ignored rather than treated as "everything".

GET /v1/fixtures/:id

One fixture in full. Takes the same tier, canonical and include parameters.

{
  "id": "adbccd06-b292-43e8-abd7-4af23d9dd72f",
  "sport": "TENNIS",
  "phase": "live",
  "provider": "BETRADAR",
  "competition": { "name": "ATP Basel", "category": "Switzerland" },
  "competitors": [ { "name": "Player A", "isHome": true }, { "name": "Player B" } ],
  "markets": [
    {
      "market_id": "c309a5d9-…",
      "market": "Total games",
      "canonical": "main_total",
      "tier": 1,
      "structure": "exclusive",
      "status": "OPEN",
      "in_play": true,
      "selections": [
        { "side": "under", "line": 45.5, "price": 3.7,  "fractional": "27/10", "probability": 0.2350778601 },
        { "side": "over",  "line": 45.5, "price": 1.25, "fractional": "1/4",   "probability": 0.7649221399 }
      ]
    }
  ],
  "withheldMarkets": 2
}

GET /v1/counts

Live, upcoming and outright fixture counts per sport, plus the time of the last catalog walk. Cheap — use it to decide whether a full board fetch is worth making.

GET /v1/health

Upstream counters, board size, walk errors and market coverage. Needs a key but does not count against your rate limit.

coverage is the honest number: of every tradeable market we hold, the share we can identify well enough to serve. markets_withheld and withheld_reasons say what the rest were.

For an anonymous liveness check, GET / returns only whether the service is up. It carries no measurements.

GET /v1/me

Your key: plan, limits, billing state and live usage. Does not count against your rate limit.

{
  "email": "you@example.com",
  "plan": "scale",
  "sse": true,
  "limits": { "per_sec": 30, "per_min": null, "per_day": null },
  "usage": { "today": 1841, "last_sec": 3, "per_second": [ … 60 slots … ] }
}

SSE /v1/stream

Server-sent events, one odds event per upstream price push. Requires a plan with SSE.

curl -N -H "x-api-key: $KEY" https://oddsradarwire.com/v1/stream
event: hello
data: {"events":721}

event: odds
data: {"event_id":"adbccd06-…","status":"PREMATCH","published_at":"…",
       "markets":[{"market":"Winner","canonical":"match_winner","tier":1,
                   "structure":"exclusive","status":"OPEN","selections":[…]}]}

Lines beginning : are keepalive pings. The stream sends one every 15 seconds so an idle connection is distinguishable from a dead one.

The stream withholds exactly what REST withholds. A market you cannot get from /v1/fixtures will not appear here either — an update is rebuilt from the merged fixture before it is sent, not read off the raw upstream patch.

A price-only update carries no market names. That is why the stream re-derives structure, canonical key, tier and side after merging rather than trusting the patch.

No-vig probability

Every selection carries probability beside price. These are the provider's own no-vig numbers, passed through unmodified. We do not recompute, smooth or re-normalise them, and there is no method of ours to document.

The priced odds do carry vig: overround ran a median of 1.084 across a full board snapshot.

Only a market whose selections partition the outcome space sums to 1. Read structure before summing anything.

Market structure

Every market carries a structure saying which arithmetic applies to it. Grading them all as distributions is a mistake that reports thousands of false vig failures on correct data.

structureWhat it isSums to
exclusiveSelections partition the outcome space.1
ladderCumulative thresholds on one subject — "A 1+", "A 2+".Nothing. Each rung is a separate binary; probability must fall as the threshold rises.
unionSelections overlap by construction — double chance, multigoals, goalscorer.Nothing.
partialAt least one selection is unpriced, so this is a subset.Nothing.

Measured over a 94-minute run: 32,632 of 32,653 fully-priced exclusive markets summed to 1.0000. The residue is named promo markets that carry 2–4% by design, and cricket run ranges split across market ids.

Canonical keys and tiers

The feed carries 3,708 distinct market names across 21 sports, and every sport spells the same bet differently: 1x2, Winner, Match Winner - Twoway, Winner (incl. overtime) and Winner (incl. super over) are one market. Asking for "the moneyline on every live event" by name is not possible.

So every market carries three things: the provider's own wording in market, a stable canonical key meaning the same thing in every sport, and a tier saying what is promised about it.

TierWhat it isPromise
1Match winner, main handicap, main total, team totals, their first-period equivalents, and the esports map markets.Documented and stable. The schema does not move under it.
2Player props and alternate lines.Served, but beta. Thin markets — and a no-vig number on a thin market is noise wearing a probability's clothes.
3Everything else.Carried raw under the provider's own name. No canonical key, no stability promise.

Ten Tier 1 keys cover 53.6% of open markets:

match_winner · main_handicap · main_total · team_total
first_period_winner · first_period_handicap · first_period_total
map_winner · map_handicap · total_maps

Nothing is withheld by tier. Tier says what is promised, not what is served.

"First period" is whatever the sport's first scoring segment is: half in soccer, set in tennis, inning in cricket, game in darts. Later periods are Tier 3.

A market only takes a main-line key when it is a line on the sport's own scoring unit — goals, points, games, sets, runs, rounds, kills, maps. Total wides, Total fours and Corner handicap all read like a main line and are not one; handing you the over/under on wides under main_total is the failure this rule exists to prevent. Combination markets and near-misses that answer a different question with the same words — Draw no bet, Halftime/fulltime — are always Tier 3.

Lines and sides

Both sides of a total arrive from upstream carrying the same display name: a soccer total ships two selections both named 5.5, a handicap ships two both named -3.5. That is the difference between a bet and its opposite, so each selection carries line, line_type and side.

The over/under mapping is measured, not read off a spec sheet. Within a fixture's totals ladder, P(over) must fall as the line rises; restricted to OPEN markets with both selections trading, 840 of 840 ladders were monotonic.

Handicap sides are deliberately null. The two outcomes came out 69/8 and 8/69 on the same test, so there is no clean mapping. A null is recoverable; naming the wrong side of a bet is not.

What is tradeable

Only OPEN markets are served by default. On a live snapshot the board held 17,131 markets of which 1,667 were open — 14,029 closed, 1,322 suspended, 113 resulted. Serving all of them means serving finished events priced like live ones, and 0.771 on a market that has ended is indistinguishable from 0.771 on one that has not.

CLOSED and SUSPENDED are not the same thing and are not collapsed. Closed is dead; suspended reopens in seconds after a goal review or a timeout. Both keep their status and can be asked for separately:

GET /v1/fixtures/:id?include=suspended         # watch one come back
GET /v1/fixtures/:id?include=closed,suspended  # the whole lifecycle

A RESULTED selection never carries a price under any query. Its price, fractional and probability are null and its status says why. There is no flag that turns this off.

A live match whose result is effectively decided closes its moneyline. That is the market lifecycle working, not a coverage failure — so coverage counts tradeable markets only and reports the rest as markets_untradeable.

What we withhold, and why we count it

A market is servable when it is grouped and every selection in it is named. Anything else is a price with no outcome attached — possibly a slice of a larger market, possibly several handicap lines bundled under one id.

We do not serve a market we cannot fully identify. Withheld markets are counted per fixture as withheldMarkets and in aggregate on /v1/health, so the omission is never silent and you can always see the size of it.

This is why the vig-free figure above holds. Serving unidentified markets would raise coverage and make every aggregate downstream of it wrong.