A live odds API is only as live as the time each price was observed. Two prices can arrive in the same response, one read four seconds ago and one read twelve minutes ago, and look identical unless the API tells you when each was observed. So the first question to ask of any live odds API isn't how fast it responds. It's whether every price carries its own observation time, and whether that time is honest.
The second question is how you get updates: by polling a REST endpoint on a timer, or by holding a stream open and receiving changes. Both work. They cost different amounts, fail in different ways, and suit different products. This guide covers both, with the numbers.
Three clocks on every price
A price passes through three moments, and a good API keeps them apart:
| Clock | What it means | In ZippyOddsAPI |
|---|---|---|
| Observed | When the venue's price was actually read | observed_at on outcomes and bookmakers |
| Received | When that response arrived at the collector | received_at |
| Served | When the API built the answer you're reading | meta.generated_at on REST, ts on stream messages |
The gap that matters is between observed and served. That's the age of the price. ZippyOddsAPI clocks a copy at the earliest of its receipt, the response's HTTP Date, receipt minus its Age header, and any read time the venue states. So a price a venue's CDN held for ten minutes shows as ten minutes old, not as fresh.
A fourth clock sometimes appears: venue_updated_at, the venue's own stamp for when it last changed the price. An older stamp than observed_at just means the price hadn't changed since. It doesn't make a price stale.
Working out a price's age
Judge age on the server's clock, not your device's. A phone with a clock two minutes fast will think every price is two minutes older than it is. Here's a generic check in JavaScript against a REST answer:
function ageSeconds(observedAt, generatedAt) {
return (Date.parse(generatedAt) - Date.parse(observedAt)) / 1000;
}
const limit = game.status === "live" ? 90 : 900;
const stale = ageSeconds(book.observed_at, meta.generated_at) > limit;
Those limits, 15 minutes before a game and 90 seconds once it has started, are the ones ZippyOddsAPI applies to its own +EV board. Pick yours to suit your product. An alert that tells someone to bet needs a tighter limit than a chart.
There's one more case. Once a game's start time has passed, a price read before kickoff is a pre-match price, however recent the read. The API marks it with quality.observed_before_start and treats it as stale.
Polling versus streaming
With polling, you call /v1/odds on a timer. With streaming, you open a WebSocket to /v1/stream, subscribe to leagues or events, and get a snapshot followed by deltas whenever a read changes something.
| Polling REST | Streaming | |
|---|---|---|
| What you receive | The whole answer every time | One snapshot, then only changes |
| Latency | Up to your polling interval | When the next read changes a market |
| Cost model | Credits per request | 1 credit to connect, then 1 a minute |
| Failure mode | A missed poll | A gap in sequence numbers, or a dropped socket |
| Plan | Any plan | Plans that include streaming |
Here's the cost side worked through for one hour of NBA main markets on ZippyOddsAPI, where /v1/odds costs 2 credits a request:
- Polling every 10 seconds: 6 requests a minute, 360 an hour, 720 credits.
- Polling every 60 seconds: 60 requests, 120 credits, and prices can be up to a minute behind your screen.
- One stream: 1 credit to connect plus 1 for each minute open, so about 61 credits.
Polling is simpler to write, easier to debug and fine for a dashboard that refreshes once a minute. A live sports odds api used for alerts usually wants the stream. The sportsbook API integration guide covers the rate-limit and retry side of polling, and the plans that include streaming are on the pricing page.
What a live odds feed can't tell you without help
A stream sends a delta when a price changes. When a book is read again and the price hasn't moved, there's nothing to send, so the observed_at you hold stops moving. Judged by that held clock, a price that's still being quoted would look stale after 15 minutes.
ZippyOddsAPI handles this with clocks messages. Subscribe with "capabilities": ["clocks_v1"] and you'll hear when a book was re-read and found unchanged, with the new observation time per game or per market. Move your held clocks forward only, never back.
Gaps are the other thing to watch. Every snapshot, delta, event and clocks message raises seq by exactly 1. A jump means you missed something:
if (lastSeq && message.seq !== lastSeq + 1) {
ws.send(JSON.stringify({ type: "resync" }));
}
lastSeq = message.seq;
A resync sends a fresh snapshot for your current subscription. If nothing at all arrives for about 45 seconds, treat the socket as dead and reconnect with a backoff.
What no real time odds provider can promise
Sportsbook prices on public sites are read, not pushed to anyone, so every live sports odds feed built on them has a read interval. ZippyOddsAPI polls each venue, which means its prices are minutes old rather than milliseconds, and deltas follow how often each venue is read rather than a fixed tick. Some venues serve cached copies, so a price can be older than the read that fetched it, and the observation time says so.
That isn't a reason to avoid a live odds feed. It's a reason to read the clocks. Ask any real time odds provider three things:
- Does every price carry the time it was observed, separate from the time the answer was built?
- What happens to a price when its source stops answering? It should age in place, not vanish or keep looking current.
- How do you learn about a re-read that found no change?
If the answers are vague, the feed's freshness is too. For the difference between reading a book directly and reading an aggregator, see what a sportsbook API is. For formats, markets and leagues, the sports odds API guide is the broader overview.