draftkings apiDashboard ↗

INDEPENDENT DEVELOPER REFERENCE

DraftKings API pagination: offset, after and cursors

Three pagination styles with working loops: offset lists, the Pick6 next_after sequence and statistics cursors, plus their limits.

Editorial review: · Not a live coverage report

Which style each endpoint uses

The API has three pagination styles, and each endpoint uses one. Using the wrong parameter returns 400.

EndpointsStylePage sizeSort order
/events, /oddsoffsetlimit 1 to 100, default 25Row id
/historyoffsetlimit 1 to 100, default 25seq
/pick6/projectionsoffsetlimit 1 to 100, default 25pick_group_id, pickable_id, pickable_market_id
/pick6/historyafter with offset=0limit 1 to 100, default 25seq, returned as change_id
/dfs/*offsetlimit 1 to 100, default 25Row id
/context/*offsetlimit 1 to 100, default 25sport, event_id, entity_id
/stats/*after from pagination.nextlimit 1 to 200, default 50Event and row key

/api/v1/coverage takes no parameters, and every parameter name can appear only once in a query.

Offset pagination

offset defaults to 0 and accepts 0 to 10,000. Continue while pagination.has_more is true, adding limit each time. Always set a page cap in your code.

Python, using the get helper from the Python guide
offset = 0
for _ in range(max_pages):
    page = get("/api/v1/odds", {"sport": "NBA", "limit": 100, "offset": offset})
    handle(page["data"])
    if not page["pagination"]["has_more"]:
        break
    offset += 100

Beyond an offset of 10,000 the request returns 400. If you reach that, narrow the query by sport, stat, market or event_id and page each subset.

Pick6 history with next_after

Pick6 history is an ordered change log. Start with after=0 and limit=100, then send pagination.next_after as the next after, keeping offset at 0. Each row's change_id is its sequence number.

Resumable change loop
after = load_cursor()  # 0 on the first run
for _ in range(max_pages):
    page = get("/api/v1/pick6/history", {"sport": "NHL", "after": after, "limit": 100})
    store_changes(page["data"])       # idempotent on change_id
    after = page["pagination"]["next_after"]
    save_cursor(after)                # only after the rows are safely stored
    if not page["pagination"]["has_more"]:
        break

Save the cursor after processing, not before. If the process dies mid-page, the next run repeats that page, and storing rows by change_id makes the repeat harmless. An empty page returns next_after equal to the after you sent, so waiting and asking again is safe.

/api/v1/changes is an alias for this endpoint. It covers Pick6 only. Sportsbook history is /api/v1/history, which uses offset.

Statistics cursors

/api/v1/stats/games, player-games and team-games use an opaque cursor. sport and season are required. Send pagination.next back as after until it is null. offset and format are rejected.

Cursor loop
after = None
for _ in range(max_pages):
    page = get("/api/v1/stats/player-games",
               {"sport": "NFL", "season": 2026, "limit": 200, "after": after})
    handle(page["data"])
    after = page["pagination"]["next"]
    if after is None:
        break

Do not parse or build the cursor yourself. An empty matching page returns available: false, which means no stored rows match and says nothing about whether the games happened.

Pages are not a frozen snapshot

Collectors keep writing while you page. A row can move between pages, or change, between your calls, so offset lists can skip or repeat rows at the boundaries. Treat a paginated read as a sequence of observations.

  • Deduplicate by the row's identity: id on odds and DFS rows, the three-part tuple on Pick6 rows.
  • Keep the newest observed_at when you see the same row twice.
  • Use Pick6 history when you need every change, because its sequence order is stable.
  • Pick6 stream snapshots read atomically, on plans that include the stream. See the webhook guide.

A full page is not evidence of complete coverage either. has_more: false means you reached the end of stored matching rows, not the end of the source's board.

Spend fewer requests

Every page costs one request whatever its size, so use the largest page you can handle. Reading 1,000 rows costs 10 requests at limit=100 and 40 at the default of 25. Filter first, and stop paging once you have what you need.

With a 20-per-minute limit on Free, a tight loop can hit 429 before any monthly cap. Space pages if you expect long reads, and set a page budget for every job.

Try it

curl --fail-with-body "https://draftkingsapi.com/api/v1/pick6/history?sport=NHL&after=0&limit=25" \
  -H "Authorization: Bearer $DRAFTKINGS_API_KEY"

Questions

What is the maximum page size?

100 rows on Pick6, DFS, sportsbook and context lists, and 200 on statistics endpoints. Asking for more returns 400.

Why did a row appear on two pages?

Captures can change while you page, because pagination is not a frozen snapshot. Deduplicate by row identity and keep the newest observation.

Does an empty page cost a request?

Yes. Valid authenticated calls count even when no rows match, so stop when has_more is false rather than requesting another page to check.

How do I read more than 10,000 rows?

Offset stops at 10,000. Split the query with filters such as sport, stat, market or event_id, or use the Pick6 change sequence, which continues with after.

Related resources

Independent service operated by DD Inc., not affiliated with or endorsed by DraftKings. Technical access is not a redistribution license. No wagering: this service does not place, recommend or settle entries or bets. Check sources and status for actual capture context.