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
| Status | error | Meaning |
|---|---|---|
| 401 | invalid_key | Missing, unknown or revoked key. |
| 403 | plan_lacks_sse | Your plan does not include /v1/stream. Poll /v1/fixtures instead. |
| 404 | fixture not found | No such fixture on the board. Fixtures leave the board when they finish. |
| 405 | method not allowed | The API is read-only. Everything is GET. |
| 429 | rate_limited | See Rate limits. |
| 503 | feed_unavailable | The 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"
| Parameter | Values | Meaning |
|---|---|---|
event_type | live · prematch | Which board. |
sport | e.g. TENNIS, CS2 | One sport. |
status | fixture status | Filter by fixture state. |
tier | 1 · 2 · 3 | A ceiling, not an exact match: tier=2 returns tiers 1 and 2. See tiers. |
canonical | comma separated | e.g. main_total,match_winner. |
include | closed · suspended · resulted | Untradeable markets, off by default. See What is tradeable. |
limit | default 100, max 1000 | Cap 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.
structure | What it is | Sums to |
|---|---|---|
exclusive | Selections partition the outcome space. | 1 |
ladder | Cumulative thresholds on one subject — "A 1+", "A 2+". | Nothing. Each rung is a separate binary; probability must fall as the threshold rises. |
union | Selections overlap by construction — double chance, multigoals, goalscorer. | Nothing. |
partial | At 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.
| Tier | What it is | Promise |
|---|---|---|
| 1 | Match 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. |
| 2 | Player 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. |
| 3 | Everything 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.