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:
{
"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;
nullis an empty cell; booleans aretrue/false. - Pagination is identical — the cursor is in the
X-Next-Cursorresponse header (absent on the last page).
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
seasonrather than widefrom/toranges — 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/gamesand/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.