draftkings apiDashboard ↗

INDEPENDENT DEVELOPER REFERENCE

DraftKings API in Python and cURL

Call the API with cURL and the Python standard library: timeouts, retries on 503, bounded pagination and clear error handling.

Editorial review: · Not a live coverage report

No SDK needed

The API is plain HTTPS with JSON responses. cURL and Python's standard-library urllib both work, and the examples here install nothing. Each reads DRAFTKINGS_API_KEY from your environment and contains no credential.

cURL
curl --fail-with-body "https://draftkingsapi.com/api/v1/pick6/projections?sport=NBA&limit=10" \
  -H "Authorization: Bearer $DRAFTKINGS_API_KEY"

--fail-with-body makes cURL exit non-zero on an HTTP error while still printing the JSON error. Scripts then stop rather than parse an error as data.

A small Python client

This client sets a timeout, retries transient failures with backoff, reads the error envelope and stops at a page budget. Python 3.9 or later is enough.

client.py
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request

BASE = os.environ.get("DRAFTKINGS_API_BASE", "https://draftkingsapi.com")
KEY = os.environ["DRAFTKINGS_API_KEY"]


def get(path, params=None, attempts=3):
    query = urllib.parse.urlencode({k: v for k, v in (params or {}).items() if v is not None})
    url = f"{BASE}{path}?{query}" if query else f"{BASE}{path}"
    request = urllib.request.Request(url, headers={"Authorization": f"Bearer {KEY}"})
    for attempt in range(1, attempts + 1):
        try:
            with urllib.request.urlopen(request, timeout=20) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            body = json.loads(error.read() or b"{}")
            message = body.get("error", {}).get("message", "unknown error")
            # 429 means wait for a UTC boundary, so a short retry only spends time.
            if error.code != 503 or attempt == attempts:
                raise RuntimeError(f"{error.code}: {message}") from error
        except urllib.error.URLError:
            if attempt == attempts:
                raise
        time.sleep(2**attempt)


def pages(path, params, max_pages=3, limit=100):
    offset = 0
    for _ in range(max_pages):
        page = get(path, {**params, "limit": limit, "offset": offset})
        yield from page["data"]
        if not page["pagination"]["has_more"]:
            return
        offset += limit


for row in pages("/api/v1/pick6/projections", {"sport": "NBA"}, max_pages=2):
    if not row["stale"] and not row["is_paused"]:
        print(row["stat"], row["target_value"], row["observed_at"])

Retry only what can recover

Retrying everything wastes quota, because authenticated calls count even when they return nothing useful. Choose by status.

StatusRetry?Why
400NoFix the query. Unknown or duplicate parameters will fail the same way.
401, 403NoFix the key or the access boundary. Never try to bypass a 403.
429Not in a loopThe allowance resets at a UTC minute or month boundary. Sleep until then.
503A few times, with backoffStorage or a dataset may be briefly unavailable. Check status if it persists.
Network errorA few times, with backoffNo response arrived, so the outcome is unknown.

Paginate deliberately

On Pick6, DFS, sportsbook and context lists, limit defaults to 25 and accepts 1 to 100, and offset defaults to 0 and accepts 0 to 10,000. Continue while pagination.has_more is true, adding limit to offset. Captures can change across calls, so pagination is not a frozen snapshot.

Statistics endpoints use a cursor instead, and Pick6 history uses after. The pagination guide has one working loop for each style.

Budget your calls

Free includes 500 requests/month at 20/minute; Developer is $49/month with 10,000 requests/month at 60/minute; Production is $149/month with 50,000 requests/month at 120/minute. Limits are shared across keys. Existing legacy accounts retain their original daily terms, and the dashboard shows the active allowance.

Valid authenticated calls count even for empty results or unavailable datasets, and invalid queries authenticated before validation count too. Keep a page cap in every script, as max_pages does above, so a bug cannot spend a month of quota overnight.

Cache responses in your own process or database for at least the collector interval. The API sends Cache-Control: no-store, and polling faster than the worker target returns no newer data.

Filter to what you can use

Filter on the server where a parameter exists, then apply capture checks on the client. sport and stat reduce pages and quota on Pick6. sport, event_id and market do the same on odds. Freshness has no server filter, so check stale per row.

One league, one market
curl --fail-with-body "https://draftkingsapi.com/api/v1/odds?sport=NBA&market=total&limit=25" \
  -H "Authorization: Bearer $DRAFTKINGS_API_KEY"

Unsupported parameters return 400 rather than being ignored, so a typo fails loudly. Read the valid filters in the docs or the machine-readable OpenAPI document.

Try it

Run the downloadable script, which applies the same retry and page limits: python3 draftkings-quickstart.py --sport NBA --limit 5.

Questions

Do I need a third-party Python package?

No. The examples use urllib and json from the standard library. You can use requests or httpx if your project already has them, because the API is ordinary HTTPS with a Bearer header.

Why does my script get a 400 for a parameter that looks harmless?

The API rejects unknown and duplicate query parameters. Check the parameter name against the docs. Each endpoint has its own list of accepted filters.

Should I retry a 429 automatically?

Not in a tight loop. A 429 clears at the next UTC minute or the next calendar month, depending on which limit you hit. Sleep to the next minute once, and stop if it fails again.

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.