Guide

Pagination & CSV

Cursor paging, page sizes, and CSV export.

Cursors

List endpoints are keyset-paginated. Each page returns next_cursor: an opaque, URL-safe string you pass back as ?cursor=. It is null on the last page. Cursors never skip or repeat rows even while new finals are being added.

A cursor is bound to the filters that produced it. Change any filter (other than limit or format) and you get a 400 asking you to restart from the first page:

400
{
  "error": {
    "code": "invalid_param",
    "message": "cursor was issued for a different set of filters — restart from the first page",
    "param": "cursor"
  }
}

Page size

limit defaults to 100 and goes up to 1,000. There is no offset parameter and no total count — for bulk pulls, iterate until next_cursor is null. meta.count is the number of rows in the current page.

Ordering

Games and odds are ordered by start_time, then game_id (odds additionally by book_id), oldest first. Catalog endpoints are ordered by sport, then season or team abbreviation. Ordering is fixed; there is no sort parameter in v1.

CSV

Add ?format=csv or send Accept: text/csv. Rules:

  • Header row first; nested objects become dotted columns (consensus.total.close); arrays of scalars are joined with |.
  • RFC 4180 quoting; UTF-8; null is an empty cell; booleans are true/false.
  • Pagination is identical — the cursor is in the X-Next-Cursor response header (absent on the last page).
pandas
import io, requests, pandas as pd

H = {"Authorization": "Bearer tbi_live_YOUR_KEY"}
url = "https://api.thebettinginsider.com/v1/games"
params = {"sport": "ncaaf", "season": 2024, "limit": 1000, "format": "csv"}
frames = []
while True:
    r = requests.get(url, headers=H, params=params)
    r.raise_for_status()
    frames.append(pd.read_csv(io.StringIO(r.text)))
    nxt = r.headers.get("X-Next-Cursor")
    if not nxt:
        break
    params["cursor"] = nxt
df = pd.concat(frames, ignore_index=True)
print(df.shape, df["consensus.spread.close"].describe())

Tips for bulk pulls

  • Use limit=1000. A full NFL season is one request; a full MLB season is three.
  • Filter by season rather than wide from/to ranges — past seasons are cached for 24h and come back in well under a second.
  • Store game_id; it is stable and is the join key between /v1/games and /v1/odds.
  • To keep a local copy current, run nightly with from=<yesterday> — new finals appear once graded, usually within a couple of hours of the final whistle.