Join on IDs, not on names
Thefixtures channel carries an externalProviders object on every fixture:
If your current feed is one of these, the join is a single field. Store it once per fixture and your two systems share a key from then on.
Mappings are populated where a match exists — treat any field as nullable and fall back to start time plus participants for the remainder.
Bookmaker-side mapping
For joins against a bookmaker’s own identifiers rather than a data provider’s, use the mapping endpoints:GET /futures/mapping does the same for outright markets. Per price, the odds channel also carries bookmakerMarketId and bookmakerOutcomeId, and the bookmakers channel carries bookmakerFixtureId and fixturePath.
Compare prices on a stable key
Within OddsPapi a price is keyed by:{fixtureId}:{outcomeId}:{playerId} — which is what you want when comparing across feeds, because a selection wins or loses identically regardless of who quoted it.
To line up a selection against another feed’s market model, decompose it into coordinates rather than matching market name strings:
marketType (1x2, totals, spreads, …), period (fulltime, p1, …), and handicap (the line value) are all explicit on GET /markets. Every distinct line value is its own marketId, so “Over 2.5” and “Over 3.5” never collapse into one market. See Markets and outcomes.
1
Map the fixture
Join on
externalProviders, or via /fixtures/mapping for bookmaker-side IDs.2
Map the market
Translate your existing model into
marketType + period + handicap and resolve the marketId from /markets.3
Map the side
Pick the
outcomeId for the side; add playerId for player markets (0 otherwise).4
Store the pair
Persist your key ↔
{fixtureId}:{outcomeId}:{playerId} so the mapping is done once, not per update.Measure which feed is faster
Cross-validation is only worth the second integration if you can tell which source wins. Two measurements, both available without instrumentation on our side:- Observation delay —
changedAt − bookmakerChangedAtper update, against the equivalent timestamp on your other feed. Per-book bounds are published onGET /bookmakersasmaxDelayLiveInSec/maxDelayPregameInSec/maxDelayPregameMainInSec, with representative p50s in Latency and freshness. - Delivery hop — envelope
tsagainst your receipt time. This one depends on where you deploy; see Server location.
GET /fixtures/odds/clv. Same oddsId format as the live stream, so the join is free.
What tends to be complementary
Coverage overlap is rarely total, which is the point of running two. In practice OddsPapi adds sharp trading books, Asian lines and quarter-line settlement, exchange and prediction-market depth, and stake limits on close to every sharp book — alongside broad US and European retail coverage. See Bookmaker coverage for the catalog shape andGET /bookmakers for what your key can see.
Cutover checklist
- Fixtures joined via
externalProvidersor/fixtures/mapping, with a fallback for nulls - Market model translated to
marketType/period/handicapcoordinates - Storage keyed by
oddsId, with selection keys for cross-feed comparison -
staleOddsandsuspendedwired into the same kill switch as your existing feed -
snapshot_requiredhandling and resume implemented (Resume & Replay) - Delay comparison running on both sources before you shift any weight
What you can rely on
- IDs do not move under you. Registries are append-only and published identifiers are frozen, so a mapping table you build during evaluation is still correct a year later. New enum values are appended rather than repurposed — ignore what you do not recognize instead of failing.
- Recovery is bounded and deterministic. Per-channel cursors (
serverEpoch+entryId) replay insideresumeWindowMs; past that, the worst case is one REST snapshot, not a silent gap you discover in reconciliation. - Mapping resolves both directions.
externalProvidersgets you in from a provider ID you already hold, andGET /fixtures/mappinggoes either way — so a second feed is a lookup, not a matching project.