BlogOdds APIs

A Sportsbook API Integration That Survives Production

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:

HeaderMeaning
X-RateLimit-RemainingRequests left in this window
X-RateLimit-ResetSeconds until the window ends, as a count
X-Credits-UsedWhat this request cost
X-Credits-RemainingBalance 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.

CodeStatusWhat to do
rate_limited429Wait Retry-After seconds, then retry
internal_error500Retry with backoff. It wasn't charged
invalid_parameter400Fix the request. Don't retry
missing_api_key, invalid_api_key, key_revoked, key_expired401Stop and alert
insufficient_credits, key_credit_cap402Stop and alert
plan_feature_unavailable, key_scope403Change 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 bookmakers list when no book has anything matching your filters.
  • /v1/best-lines leaves out a side with no fresh price, so a two-way market can come back with one side.
  • /v1/coverage and /v1/status say 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.