A sportsbook API integration that works in a demo and one that works for months are different pieces of code. The first makes a request and parses the JSON. The second keeps the key out of logs, budgets requests against a rate limit, retries only what's worth retrying, caches what doesn't change, maps one book's team names onto another's, and copes when a book stops sending prices.
This guide walks through each of those, using ZippyOddsAPI as the concrete case because its behavior is documented down to the error codes. The patterns carry over to most sports betting api integrations.
Keep the key in a header, on a server
Send the key in an Authorization: Bearer header. ZippyOddsAPI also accepts X-API-Key. It refuses a key in the URL with 400 api_key_in_url, because URLs end up in proxy logs, browser history and analytics tools.
curl -s "https://zippyoddsapi.com/v1/usage" \
-H "Authorization: Bearer $ZIPPYODDS_KEY"
Keep the key on your server and have your front end call your own backend. A key shipped in a web bundle is a key anyone can copy. Two settings help if one does leak: an IP allowlist, which refuses other addresses with 403 ip_not_allowed, and scopes, which limit a key to the endpoints you name. A key's secret is shown once, so a lost key has to be rotated.
Budget credits and requests separately
Most APIs meter you two ways at once, and it's easy to watch one and trip the other.
- Credits pay for data. In ZippyOddsAPI each request costs a fixed 0 to 3 credits, whatever the size of the answer, and refused requests cost nothing.
- Rate limit protects the service. It's per account, shared by all of an account's keys, and every request counts, including free ones. The window is fixed: it starts with your first request and lasts 60 seconds.
The response headers tell you where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Remaining | Requests left in this window |
X-RateLimit-Reset | Seconds until the window ends, as a count |
X-Credits-Used | What this request cost |
X-Credits-Remaining | Balance after it, absent on an unlimited plan |
A worked budget: polling /v1/odds (2 credits) every 10 seconds is 6 requests and 12 credits a minute. The league parameter takes a list, so one request can cover several leagues. Over an 8-hour slate that's 2,880 requests and 5,760 credits. Run the same sum for your own schedule before you pick a plan on the pricing page.
Retry by error code, not by status
Branch on the stable error.code, not on the message. Some failures fix themselves with time, and some never will.
| Code | Status | What to do |
|---|---|---|
rate_limited | 429 | Wait Retry-After seconds, then retry |
internal_error | 500 | Retry with backoff. It wasn't charged |
invalid_parameter | 400 | Fix the request. Don't retry |
missing_api_key, invalid_api_key, key_revoked, key_expired | 401 | Stop and alert |
insufficient_credits, key_credit_cap | 402 | Stop and alert |
plan_feature_unavailable, key_scope | 403 | Change the request or the plan |
A minimal wrapper in JavaScript:
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function get(url, key, tries = 3) {
for (let i = 0; i < tries; i++) {
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
if (res.ok) return res.json();
const body = await res.json();
if (res.status === 429) {
await sleep(Number(res.headers.get("Retry-After")) * 1000);
continue;
}
if (res.status >= 500) { await sleep(2 ** i * 1000); continue; }
throw new Error(`${body.error.code}: ${body.error.message}`);
}
throw new Error("gave up");
}
Log meta.request_id with every failure. It's the one thing support needs to find your request.
Cache what doesn't change
Catalog endpoints change rarely: /v1/leagues, /v1/markets and /v1/sportsbooks cost 0 credits in ZippyOddsAPI, but they still count against the rate limit. Read them at start-up and every few minutes, not on every page view.
Prices are the opposite. Responses are sent Cache-Control: private, no-store, so a shared CDN won't replay one caller's answer to another. If you cache prices in your own app, cache them with their observed_at and let the age show, rather than serving a cached copy as if it were new.
Map teams, markets and selections by id
The hardest part of any sportsbook API integration is deciding that two listings are the same thing. Books spell teams differently, list games at slightly different times, and name markets their own way. Getting it wrong is worse than missing a match: a false merge between two different games invents edges that aren't there.
An aggregator that joins sources for you saves most of this work. In ZippyOddsAPI:
- An
evt_id is one real game, joined across sources. - A
sel_id is an outcome. The same side and point in the same market has the same id at every book, so you can line up prices without matching names. - A
mkt_id is one book's market, and the line is part of it. When a total moves from 224.5 to 225.5, the new line has a new id.
If you do your own matching, make it err towards no match: same start time, exactly one candidate, and no shared nickname taken as proof. Pair spreads by the home line, never by absolute value.
Handle missing books without filling gaps
Books go quiet. A venue changes its site, refuses reads for a while, or takes a game down. Your code should expect it:
- An event can come back with an empty
bookmakerslist when no book has anything matching your filters. /v1/best-linesleaves out a side with no fresh price, so a two-way market can come back with one side./v1/coverageand/v1/statussay which books are feeding now and which are failing or blocked.
Show a gap as a gap. Never fill a missing book with its last known price relabelled as current.
Page to the end
Paged endpoints say whether there's more, with meta.has_more and meta.next_cursor. Pass the cursor back unchanged with the same filters. A 409 cursor_expired means the board moved under you: start again from the first page.
Once requests are solid, the next decisions are about delivery. Our guide to the sportsbook data feed covers running a stream in production, what a sportsbook API is explains direct versus aggregated sources, and the sports odds API guide covers formats and markets.