Soccer odds captured from Hard Rock Bet and Winamax, aligned market-by-market so the same bet can be compared across both books.
https://datarium.top/coverage/api/
Data endpoints use HTTP Basic auth. Credentials are issued per consumer — ask the operator; they are not published here.
curl -u 'USER:PASSWORD' \
'https://datarium.top/coverage/api/fixtures?day=2026-08-31'
| Endpoint | Access | Purpose |
|---|---|---|
GET /health | open | Liveness: archive row count, capture count for today |
GET /proxy | open | Daily collection spend against cap |
GET /fixtures | auth | Games with captured odds |
GET /fixture | auth | Every market and price for one game |
GET /pairs | auth | Market-pairing decisions and their evidence |
GET /coverage | auth | Leagues, calendar, market vocabulary |
POST /lineup/confirm | auth | Tell the collector lineups are out for a game |
GET /lineup/signals | auth | Audit trail of pushed confirmations |
POST /pairs/status | localhost | Approve/reject a pairing. Not reachable externally. |
http://localhost:8010/
directly — no credential, one less hop. The paths are identical.GET /fixtures| Param | Default | Meaning |
|---|---|---|
day | — | Exact kickoff date, YYYY-MM-DD (UTC) |
days | 0 | Kickoffs within the last N days |
limit | 2000 | Maximum rows |
{"fixtures": [{
"name": "Dijon vs Saint-Étienne",
"kickoff": "2026-08-31 18:45:00",
"competition": "France - Ligue 2",
"books": ["hrb", "winamax"],
"phases": ["morning", "lineup"],
"rows": 1879, "markets": 141,
"captured_at": "2026-08-31 11:03:51.734000",
"lineup_confirmed": false
}]}
books tells you whether a game is comparable at all — ["hrb","winamax"]
means both priced it. name and kickoff together address the game in
the next call, and must be passed back verbatim.
GET /fixturecurl -s -u 'USER:PASSWORD' -G \
'https://datarium.top/coverage/api/fixture' \
--data-urlencode 'name=Dijon vs Saint-Étienne' \
--data-urlencode 'kickoff=2026-08-31 18:45:00'
{
"fixture": {
"name": "Dijon vs Saint-Étienne",
"kickoff": "2026-08-31 18:45:00",
"competition": "France - Ligue 2",
"books": ["hrb", "winamax"],
"lineup_confirmed": false,
"lineup_source": "unknown",
"event_ids": {"hrb": "992687190473572603", "winamax": "72572050"}
},
"totals": {"markets": 141, "selections": 1879, "both_books": 19, "suspect": 0},
"markets": [ ... ]
}
| Field | Meaning |
|---|---|
concept | The join key. Normalised and language-neutral — total_goals_ou, btts, handicap_spread. |
period | FT, H1 or H2. Always join on (concept, period). |
label | English display name. |
hrb_name / wx_name | Raw per-book market titles. Winamax titles are French; kept verbatim as an audit trail. |
both, books | Whether both books priced this market. |
paired_by | approved (human-confirmed) · concept (matched by meaning) · structure (matched by shape — weakest evidence). |
median_gap, suspect | Median implied-probability gap across selections, and whether that exceeds the sanity threshold. |
times_seen, pair_status | How often this pairing has been observed, and its review state. |
| Field | Meaning |
|---|---|
role | Identity aligned across books: home, away, draw, over, under, yes, no, named. |
hrb_odds / wx_odds | Decimal odds. null when that book did not offer the selection. |
hrb_label / wx_label | Raw selection text on each book. |
hrb_source_age_s / wx_source_age_s | Age of the price in seconds. HRB is live (0); Winamax is served from a 15-minute cache (900). |
hrb_captured_at / wx_captured_at | When each side was captured. |
hrb_unbettable / wx_unbettable | Present and true when the price is ≤ 1.00 — a placeholder, not a real price. |
prob_delta, suspect | Implied-probability difference and its sanity flag. Both null when a placeholder is involved. |
residual | true for catch-all buckets (“Other” / “Autre”), which are not the same bet on both books. |
POST /lineup/confirmEach game is captured twice: once in the morning, once near kickoff. The second capture is meant to fire when the starting lineups are published — but the upstream lineup feed is rate-limited during exactly that window, and not every game maps to it, so most captures fall back to a blind timer instead.
If your service already knows lineups are out, say so and the collector captures that game on its next poll rather than waiting.
curl -s -u 'USER:PASSWORD' -X POST -G \
'https://datarium.top/coverage/api/lineup/confirm' \
--data-urlencode 'name=Benfica vs Estoril' \
--data-urlencode 'kickoff=2026-08-31 19:15:00' \
--data-urlencode 'source=my-service'
{
"ok": true,
"signal": {
"event_id": "5638672854720577792",
"event_name": "Benfica vs Estoril",
"kickoff": "2026-08-31 19:15:00",
"source": "my-service",
"received_at": "2026-08-31 18:12:32"
},
"minutes_to_kickoff": 62.5,
"effect": "capture on the watcher's next poll"
}
| Param | Meaning |
|---|---|
name + kickoff | The game, verbatim from /fixtures. |
event_id | Alternative address — from /fixture's event_ids. |
source, note | Optional. Stored so it is clear later who asserted what. |
effect tells you plainly what your call will do:
capture on the watcher's next poll,
none - lineup snapshot already captured, or
none - kickoff has passed.
GET /lineup/signals?limit=50 returns recent confirmations and who sent them.
GET /pairs?status=approved|auto|rejectedEvery HRB↔Winamax market pairing with the evidence behind it: how it was matched, how many selections lined up, how often it has been seen, and whether a human confirmed or rejected it. Use it to audit any comparison that looks surprising.
Competition coverage is returned in GET /coverage under leagues.
Note its own caveat: a 0% overlap figure means discovery missed the competition, not
that Winamax does not price it.
(concept, period) — never on
concept alone, and never on market names. A full-time market compared against
a first-half one is the most common way to manufacture a disagreement that does not exist.source_age_s before comparing prices.
Winamax prices are always about 15 minutes old; HRB prices are live. Treating them as
simultaneous is the likeliest way to act on a price that has already moved.*_unbettable.
Winamax prices near-certain legs at exactly 1.00. That is a placeholder, not a
price — read as a probability it is 100%, and it will silently corrupt any edge or margin
calculation.event_ids, not display names.
Books spell teams differently and rename them — “Vancouver FC” on one side is
“Vancouver Langley FC” on the other.suspect: true and paired_by: "structure"
as unverified. Structural matches are made on market shape rather than meaning, which is
how a corners market can be lined up against a goals market./coverage was rebuilt on 2026-08-31 from 1,123 HRB and 412 Winamax fixtures,
replacing a 60-fixture sample. Each concept carries hrb_pct / wx_pct,
the fixture counts behind them, periods_seen, and observed.
Read the zeroes differently: a Winamax zero means the market is not offered, since the whole
match page is captured; an HRB zero may only mean it was never requested, because collection
is limited to market types discovery has learned for that competition.“Both books” overstates comparability. A game can be listed under both books while
one of them priced only a handful of markets. Depth is heavily skewed — a US-facing book does
not price minor European leagues deeply. Use totals.both_books, which counts
genuinely paired markets, rather than counting fixtures.
History has gaps. Collection has not been continuous since inception; some days are partial and at least one is missing entirely. Time-series queries should tolerate holes rather than assume a uniform series.
Two snapshots per game, not a live feed. Each fixture is captured once in the
morning and once near kickoff (phases). This is an archive for analysis, not a
streaming price feed.