Overview
Oddiwire serves live and prematch odds for League of Legends, CS2, Dota 2, Valorant, King of Glory, plus efootball and ebasketball simulations, sourced from the ODDIN 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://oddiwire.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: ow_your_key" https://oddiwire.com/v1/fixtures
curl -H "Authorization: Bearer ow_your_key" https://oddiwire.com/v1/fixtures
curl "https://oddiwire.com/v1/fixtures?key=ow_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://oddiwire.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": "ODDIN",
"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://oddiwire.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.