Odds Feed Provider: Settle 289 Markets a Fixture From One Feed

Odds Feed Provider - OddsPapi API Blog
How To Guides September 16, 2026

Looking for an Odds Feed Provider? Start With What the Feed Has to Do

Search “odds feed provider” and you meet two kinds of vendor. The enterprise data houses sell to licensed operators through a sales call. The developer APIs sell a short list of soft books and stop at the price. A sportsbook or a trading desk needs the pipeline in between: a stream you can price off, a cursor you can resume from, a closing line you can audit against, a grade for every outcome once the fixture finishes, and an ID you can join to the feed you already run.

OddsPapi ships that pipeline as one provider. The B2B documentation at docs.oddspapi.io states that “the majority of the top-10 prediction markets run on OddsPapi data”, and lists market makers, hedge-fund desks, bookmakers and betting exchanges as production clients. This post walks the pipeline from the first WebSocket message to the settled bet, and runs every step below against the live API on a self-serve key. Every number was measured on September 8, 2026.

If you already hold a key and want the integration order, the trading desk onboarding playbook sequences the same pieces into a five-day build. This post is the map; that one is the runbook.

Why the Usual Two Options Fail a Trading Desk

Sportradar and Genius Sports price per deal and gate access behind a sales process. The Sportradar alternative post covers what that costs a small operator in weeks. The developer-tier APIs solve access and stop there: you get a price, and you build stale detection, closing-line capture, settlement and fixture reconciliation yourself, or you license four more vendors to do it. The odds API pricing comparison shows what the public tiers charge for the price alone.

The OddsPapi feed covers the whole loop. The v4 REST API on a self-serve key carries the catalogue of 350+ bookmakers (357 slugs on /v4/bookmakers today, 117 of them flagged as clones of another feed), free historical price timelines, per-outcome settlement and period scores. The v5 gateway documented at docs.oddspapi.io adds the WebSocket channels, the opening and closing line endpoints, mapping lookups, and the lineups, injuries and stats channels.

What the desk needs On a price-only API OddsPapi
Realtime prices Poll a short list of soft books WebSocket push from 350+ bookmakers, conflated on a ~10 ms window
Reconnect without a gap Rebuild state by hand serverEpoch + entryId cursors, 60-second replay buffer, snapshot_required fallback
Stale-book detection A frozen price looks live staleOdds flag per bookmaker per fixture
Closing line Capture it yourself at kick-off GET /fixtures/odds/clv on v5; free /v4/historical-odds timelines
Settlement License a results feed /v4/settlements: WIN, LOSE, HALFWIN, HALFLOSS, PUSH per outcome
Join to Betradar or Genius Build a mapping table externalProviders on every fixture; betradarId on 1,731 of 1,731 finished fixtures
Size at price Unknown limit field carried for close to every sharp book

Step 1: Price Off the Stream, Recover From the Cursor

The production gateway is wss://v5.oddspapi.io/ws. You send one login message with your key, the channels you want and the filters that cut the noise, and the server pushes every change after login_ok. The documented login shape:

{
  "type": "login",
  "apiKey": "YOUR_API_KEY",
  "channels": ["fixtures", "bookmakers", "odds", "scores"],
  "receiveType": "zstd",
  "sportIds": [10],
  "bookmakers": ["pinnacle", "betfair-ex", "draftkings"]
}

Three design points from the docs matter more than the rest for a desk that trades on the feed.

The odds channel is state, not a tick ledger

The odds channel conflates updates on a fixed batching window of about 10 milliseconds. If a price moves twice inside one window you receive the newer value. The final value of every move arrives, and a delivered price is never more than one window behind the observation at the bookmaker. For full tick history you go to the REST history endpoints in Step 2.

A disconnect is a cursor problem, not an outage

Every message carries an entryId of the form <ts_ms>-<seq>. The login_ok reply carries a resume block with the serverEpoch, a resumeWindowMs buffer (the docs show 60,000) and the server’s latest cursor per channel. Persist the epoch and your last processed entryId per channel. On reconnect you send both back and the gateway replays what you missed inside the window. Outside the window it sends snapshot_required with a reason (server_restarted, resume_window_exceeded or client_backpressure) and you rebuild those channels from one REST snapshot. Before a release the server pushes a reconnect control frame and keeps streaming for a few seconds, so you reconnect on the message and never wait for the socket to die. The WebSocket pricing pipeline post wires this loop in Python.

Every price carries its own delivery chain

Each odds update ships bookmakerChangedAt (the book’s own clock, where the book publishes one), changedAt (when OddsPapi observed the change) and the envelope ts (when the gateway sent it). You measure the last hop yourself. The reliability page publishes representative rolling p50 observation delays per book: 0.05 s for Polymarket, 0.10 s for Betfair Exchange, 0.38 s for Pinnacle, and an estimated 1 s in-play for Singbet. It puts the internal pipeline at about 30 ms p50 to gateway egress, and the delivery hop as low as about 5 ms when you colocate in Central Europe. The same page publishes no uptime percentage; SLA terms are set per agreement.

Step 2: Audit Every Line Against the Close

A desk that cannot measure its own closing line value is guessing about its edge. On v5 the endpoints are GET /fixtures/odds/historical for the full timeline and GET /fixtures/odds/clv for opening versus closing values, both keyed on the same oddsId as the live stream, so a fill joins to its closing record on one string. On the self-serve v4 key the same audit runs off /v4/historical-odds, which returns every snapshot from the first price to the final whistle and costs nothing extra.

The script below reads Arsenal v Chelsea from last Sunday (Premier League, kick-off 15:30 UTC on September 6) at three books and prints the opening and closing 1X2 prices. The closing price is the last active snapshot before kick-off; snapshots keep arriving after the clock starts, so the filter on createdAt is load-bearing.

import time
from datetime import datetime, timezone
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"

# Arsenal v Chelsea, Premier League, kicked off 2026-09-06 15:30 UTC.
# Both values come from the /fixtures record.
FIXTURE = "id1000001772221232"
KICKOFF = datetime(2026, 9, 6, 15, 30, tzinfo=timezone.utc)

# /historical-odds accepts at most 3 bookmakers per call.
r = requests.get(f"{BASE}/historical-odds", params={
    "apiKey": API_KEY, "fixtureId": FIXTURE,
    "bookmakers": "pinnacle,bet365,draftkings"})
r.raise_for_status()
timeline = r.json()["bookmakers"]
time.sleep(4.6)   # cooldown after a successful /historical-odds call

def ts(s):
    return datetime.fromisoformat(s["createdAt"].replace("Z", "+00:00"))

labels = {"101": "home", "102": "draw", "103": "away"}
print(f"{'book':11s}{'side':6s}{'open':>7s}{'close':>7s}{'move':>8s}")
for book, data in timeline.items():
    for oid, outcome in data["markets"]["101"]["outcomes"].items():
        snaps = outcome["players"]["0"]          # oldest first
        opening = snaps[0]["price"]
        # The closing price is the last ACTIVE snapshot before kick-off.
        # Snapshots keep arriving after kick-off, so filter on the clock.
        pre = [s for s in snaps if ts(s) < KICKOFF and s["active"] and s["price"] > 1]
        closing = pre[-1]["price"]
        move = (closing / opening - 1) * 100
        print(f"{book:11s}{labels[oid]:6s}{opening:7.3f}{closing:7.3f}{move:+7.1f}%")

Output, measured September 8, 2026:

book       side     open  close    move
bet365     home    1.570  1.660   +5.7%
bet365     draw    3.750  3.800   +1.3%
bet365     away    5.250  4.750   -9.5%
draftkings home    1.645  1.714   +4.2%
draftkings draw    3.600  3.900   +8.3%
draftkings away    5.250  4.800   -8.6%
pinnacle   home    1.675  1.751   +4.5%
pinnacle   draw    3.800  3.900   +2.6%
pinnacle   away    4.850  5.000   +3.1%

Arsenal won 2-1. Between the opening print on August 23 and kick-off, bet365 recorded 689 snapshots on the home price and Pinnacle 620. All three books drifted Arsenal out by 4 to 6 percent. They split on Chelsea: bet365 and DraftKings cut the away price from 5.25 to 4.75 and 4.80, while Pinnacle pushed it out to 5.00. A book that closed Chelsea at 4.75 sat 5.3 percent under the sharp close on the same outcome, which is the number a CLV audit exists to surface. The operator CLV audit post turns this read into a per-market report across your own book.

Step 3: Grade Every Outcome When the Fixture Finishes

Settlement is where most feeds stop and hand you to a results vendor. OddsPapi grades the outcomes it priced. On v5 the endpoint is GET /fixtures/settlement; on the self-serve v4 key it is GET /v4/settlements?fixtureId=, paired with GET /v4/scores for the period scores. The grades are a frozen vocabulary: WIN, LOSE, PUSH, HALFWIN, HALFLOSS, CANCELLED and UNDECIDED. The two half grades settle Asian quarter lines natively, so a -0.25 handicap on a draw grades as a half loss without a rule table on your side.

Two traps. A finished fixture flips hasOdds to false, so select finished fixtures on statusName and never on that flag. And /settlements has its own cooldown: two of ten calls spaced 1.1 seconds apart returned 429 during this test, and 2 seconds cleared it.

import time
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"

def get(path, **params):
    params["apiKey"] = API_KEY
    r = requests.get(f"{BASE}/{path}", params=params)
    r.raise_for_status()
    return r.json()

# 1. Finished fixtures in one tournament. hasOdds is false once a fixture
#    finishes, so select on statusName, never on hasOdds.
fixtures = get("fixtures", tournamentId=325,          # Brasileiro Serie A
               **{"from": "2026-09-06T00:00:00Z", "to": "2026-09-08T00:00:00Z"})
finished = [f for f in fixtures if f["statusName"] == "Finished"]
fx = finished[0]
print(fx["participant1Name"], "v", fx["participant2Name"], fx["fixtureId"])

# 2. Final score by period.
scores = get("scores", fixtureId=fx["fixtureId"])["scores"]["periods"]
for period in ("p1", "fulltime", "result"):
    p = scores[period]
    print(f"  {period:9s} {p['participant1Score']}-{p['participant2Score']}")
time.sleep(2)

# 3. Per-outcome grades. UNDECIDED means nobody priced that rung.
settlement = get("settlements", fixtureId=fx["fixtureId"])
graded, results = 0, {}
for market_id, market in settlement["markets"].items():
    grades = [p["result"] for o in market["outcomes"].values()
              for p in o["players"].values()]
    for g in grades:
        results[g] = results.get(g, 0) + 1
    if all(g != "UNDECIDED" for g in grades):
        graded += 1

print(f"  markets returned: {len(settlement['markets'])}, fully graded: {graded}")
print("  ", {k: v for k, v in sorted(results.items())})
labels = {"101": "home", "102": "draw", "103": "away"}
for oid, o in settlement["markets"]["101"]["outcomes"].items():
    print(f"  1X2 {labels[oid]}: {o['players']['0']['result']}")

Output for the first finished fixture in the window, Fluminense v Vasco da Gama:

Fluminense FC RJ v CR Vasco da Gama RJ id1000032566886978
  p1        0-0
  fulltime  1-0
  result    1-0
  markets returned: 1066, fully graded: 289
   {'HALFLOSS': 12, 'HALFWIN': 12, 'LOSE': 283, 'PUSH': 13, 'UNDECIDED': 1622, 'WIN': 282}
  1X2 home: WIN
  1X2 draw: LOSE
  1X2 away: LOSE

The response carries 1,066 market IDs. 289 of them come back fully graded, 602 outcomes in all, and the rest read UNDECIDED because nobody priced that rung. Eight finished Brasileiro Serie A fixtures from the same weekend and an Ecuador Serie B fixture from the same week returned the same grid: 1,066 market IDs and 289 graded markets each, with the WIN and LOSE split moving fixture to fixture. Scores arrive keyed by the generic period vocabulary (p1, fulltime, result), so the same parser reads a soccer half and a basketball quarter. Day 4 of the onboarding playbook closes the loop from this call to the ledger.

Step 4: Join the Feed to the IDs You Already Hold

A regulated book runs a licensed data feed for official results and a pricing feed for the market. The two have to agree on which fixture is which. Every OddsPapi fixture record ships an externalProviders block, so the join is a dictionary lookup on a key you already hold rather than a name-matching project. On v5, GET /fixtures/mapping also runs the lookup in reverse: pass a bookmaker and its own fixture IDs and get the OddsPapi IDs back.

import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.oddspapi.io/v4"

# Every fixture record carries an externalProviders block. Read it off
# the same /fixtures call you already make for the schedule.
r = requests.get(f"{BASE}/fixtures", params={
    "apiKey": API_KEY, "sportId": 10,
    "from": "2026-09-06T00:00:00Z", "to": "2026-09-08T00:00:00Z"})
r.raise_for_status()
finished = [f for f in r.json() if f.get("statusName") == "Finished"]

# One fixture, every provider key it ships.
fx = next(f for f in finished if f["fixtureId"] == "id1000032566886978")
print(fx["participant1Name"], "v", fx["participant2Name"])
for provider, ext_id in fx["externalProviders"].items():
    print(f"  {provider:13s} {ext_id}")

# Population rate per provider across the whole finished window.
counts = {}
for f in finished:
    for provider, ext_id in f["externalProviders"].items():
        if ext_id is not None:
            counts[provider] = counts.get(provider, 0) + 1
print(f"\n{len(finished)} finished soccer fixtures, Sep 6-7 2026")
for provider, n in sorted(counts.items(), key=lambda kv: -kv[1]):
    print(f"  {provider:13s} {n:5d}  {100 * n / len(finished):5.1f}%")

Output:

Fluminense FC RJ v CR Vasco da Gama RJ
  betradarId    66886978
  mollybetId    2026-09-06,26061,773
  opticoddsId   202609068ADA0CB6
  lsportsId     19906694
  txoddsId      None
  sofascoreId   None
  betgeniusId   13391445
  flashscoreId  MwalAAYH
  pinnacleId    1634729439
  oddinId       None

1731 finished soccer fixtures, Sep 6-7 2026
  betradarId     1731  100.0%
  pinnacleId     1089   62.9%
  flashscoreId    882   51.0%
  opticoddsId     850   49.1%
  betgeniusId     697   40.3%
  lsportsId       631   36.5%
  mollybetId      212   12.2%
  sofascoreId      35    2.0%

The Betradar ID is present on every one of the 1,731 finished soccer fixtures in the window, from the Premier League down to the Spanish fourth tier. The Pinnacle ID covers 62.9 percent, which tracks the share of that slate Pinnacle prices, the OpticOdds ID sits at 49.1 percent and the Genius ID at 40.3. The fixture mapping for compliance post builds the reconciliation table and the exception report on top of this block.

Lineups, Injuries, Futures and the Rest of the Board

The v5 gateway added injuries, lineups and stats channels in December 2025 and a clocks channel in April 2026, all with the same resume and replay semantics as odds. Futures run on their own channels (futures, oddsFutures, bookmakersFutures) with their own history and CLV endpoints, and the same model covers non-sports prediction-market topics as futures on sportId 69 to 78. The docs state that futures settlement is rolling out and the endpoint does not yet serve results, so grade outrights off the fixture pipeline until it does. Exchanges and prediction markets ship their order books in the meta field as uniform back and lay ladders, and a currencies channel streams fiat and crypto rates for odds conversion.

What the Limits Are

The v5 docs set 10 requests a second on the odds snapshot endpoints and 200 a minute on everything else, per key and per endpoint, with X-RateLimit-Remaining and Retry-After headers on every response. The WebSocket gateway allows 5 concurrent connections per key group and closes a client that cannot keep up with close code 4002. Filters on sportIds, tournamentIds and bookmakers, plus receiveType: "zstd" (5 to 6 times smaller frames on the odds channel, per the docs), keep a single connection inside those bounds. Distributed systems that need more raise the limits by agreement.

Pricing and Access

Access is self-serve. Sign up for a free key, run the three scripts above against last weekend’s fixtures, and scale from there. Pricing is usage-based: the pricing page is a calculator that prices the bookmakers and sports you select, multiplied by your requests per month, so a desk that reads three sharp books on two sports pays for that and not for the catalogue. The 2026 pricing comparison puts the public tiers of four providers side by side. Licensed operators who need SLA terms, raised limits or the v5 gateway write to [email protected], and the B2B reference for everything in this post is docs.oddspapi.io.

For a plain REST introduction to the same feed, the odds feed API post starts from the first call.

One Provider From First Price to Settled Bet

A WebSocket you resume from a cursor, a closing line you audit against, a grade for 289 markets a fixture and a Betradar ID on every record: that is the pipeline a sportsbook or a trading desk needs from an odds feed provider, and it runs on one key. Get your free key, then open docs.oddspapi.io for the channel schemas.

Frequently Asked Questions

What does an odds feed provider need to deliver for a sportsbook?

Five things: a realtime price stream, a recovery path that replays what you missed, a closing line to audit against, per-outcome settlement, and fixture IDs that join to your official data feed. OddsPapi delivers all five: a WebSocket with resume and replay across 350+ bookmakers, OLV/CLV endpoints, settlement grades and an externalProviders block on every fixture.

Does OddsPapi settle bets?

Yes, for fixtures. GET /v4/settlements returns WIN, LOSE, HALFWIN, HALFLOSS or PUSH for every priced outcome; on a finished Brasileiro Serie A fixture it graded 289 markets and 602 outcomes. GET /v4/scores returns the period scores. The v5 endpoint is GET /fixtures/settlement, and futures settlement is rolling out per the docs.

How does the feed recover from a disconnect?

Every message carries an entryId cursor. On reconnect you send your serverEpoch and last entryId per channel, and the gateway replays what you missed inside the resume window (60 seconds in the documented example). Past the window it sends snapshot_required and you rebuild those channels from a single REST snapshot.

Can I run OddsPapi next to Betradar or Genius Sports?

Yes. Each fixture record ships externalProviders with betradarId, betgeniusId, pinnacleId, opticoddsId, lsportsId, flashscoreId and more. On September 8, 2026 the Betradar ID was present on all 1,731 finished soccer fixtures from the previous two days, so the join is a dictionary lookup.

Is there an enterprise tier or an SLA?

Access is self-serve with usage-based pricing: the calculator prices the bookmakers and sports you pick, multiplied by requests per month. The reliability page publishes a live status page and states that uptime targets and support response times are set per agreement for B2B customers, through [email protected].