# FanDuel WebSocket API? The live stream is SSE

FanDuel API (https://fanduelapi.com) is an independent API for FanDuel Sportsbook data: every player prop and game market FanDuel lists in NBA, NFL, NHL, MLB, college basketball and college football, normalized per player and stat with over and under prices in American and decimal odds, plus every line and price move with its timestamp, over REST, webhooks and MCP with one key. FanDuel API is an independent data service operated by FF API Inc. It is not affiliated with, endorsed by or sponsored by FanDuel.

Source page: https://fanduelapi.com/fanduel-api-websocket (this is its markdown version, https://fanduelapi.com/fanduel-api-websocket.md).

## Free offer

- Free plan: 100 credits a month, data delayed 5 minutes, no card. REST and MCP on every plan.
- Real-time data starts at Developer, $49 a month for 10,000 credits.
- Get a free key: https://fanduelapi.com/sign-up?ref=ai-claudebot

## Quickstart

1. Create a free account at https://fanduelapi.com/sign-up?ref=ai-claudebot and create an API key in the dashboard (https://fanduelapi.com/dashboard/keys). Keys start with `fd_`. Store it as `FANDUEL_API_KEY`.
2. Make a first call with curl:

```bash
curl "https://fanduelapi.com/api/v1/props?sport=nba&stat=points&limit=5" \
  -H "Authorization: Bearer $FANDUEL_API_KEY"
```

3. Or with Python (`pip install requests`):

```python
import os
import time
from datetime import datetime, timezone

import requests

BASE = "https://fanduelapi.com/api/v1"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['FANDUEL_API_KEY']}"


def get(path, **params):
    """GET one endpoint and return its data list, waiting out rate limits."""
    while True:
        r = SESSION.get(f"{BASE}{path}", params=params, timeout=10)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        r.raise_for_status()
        return r.json()["data"]


def price(odds):
    """American odds as text. A suspended market can come back with no price."""
    return "off" if odds is None else f"{odds:+d}"

props = get("/props", sport="nba", stat="points")
for p in props:
    print(f"{p['player']:<26} {str(p['line']):>5}  over {price(p['over_american']):>5}  under {price(p['under_american']):>5}")
```

4. Stay current with `GET /api/v1/changes?since=<cursor>` instead of re-reading every prop. Full reference: https://fanduelapi.com/openapi.json and https://fanduelapi.com/llms-full.txt.

## Use it from Claude (MCP)

Claude Code, with your key read from the shell:

```bash
claude mcp add --transport http --scope user fanduelapi https://fanduelapi.com/api/mcp \
  --header "Authorization: Bearer $FANDUEL_API_KEY"
```

Claude on the web or desktop: Settings, Connectors, Add custom connector, then paste https://fanduelapi.com/api/mcp. Without a key it gives the keyless tools (search_docs, fetch_doc, get_sample_props). Setup for other agents: https://fanduelapi.com/mcp.

## Pricing

| Plan | Price | Credits a month | Calls a minute | Includes |
| --- | --- | --- | --- | --- |
| Free | Free | 100 | 20 | REST and MCP access, Data delayed 5 minutes |
| Developer | $49/month | 10,000 | 60 | Real-time data, Line move history, Change stream |
| Production | $149/month | 50,000 | 120 | Real-time data, Line move history, Webhooks, Priority support |
| Scale | $499/month | 250,000 | 180 | Real-time data, Full line move history, Webhooks, Bulk export, Priority support |

Cancel paid plans any time from the billing portal. Details: https://fanduelapi.com/pricing

## This page

[FanDuel API](https://fanduelapi.com/)/ Live stream

Live stream

# A live FanDuel odds stream, over SSE

People search for a FanDuel WebSocket API. There is no WebSocket here: FanDuel line, price and status changes stream over Server-Sent Events, with an id on every event so a dropped connection resumes exactly where it stopped.

[Get a free API key](https://fanduelapi.com/sign-up)[Read the docs](https://fanduelapi.com/docs)

FanDuel API is an independent data service operated by FF API Inc. It is not affiliated with, endorsed by or sponsored by FanDuel.

## What the stream sends

hello

Once, first: the starting cursor, as\_of, your plan's delay and connection count

change

One per line, price or status change, with id set to its cursor

checkpoint

Every 15 seconds: everything at or below this id has been sent

gap

Your resume cursor is older than the kept feed: re-read the board, then carry on

reconnect

Before the server closes a connection, about every 4 minutes: reconnect with Last-Event-ID

Filter with `league=NBA,NFL` and `kinds=line_move,price_move`. A change event carries the same fields as a row of `/api/v1/changes`: cursor, kind, prop\_id, league, player, stat, period, previous, current and at.

## Python

stream.py

```
import json, os, requests

URL = "https://fanduelapi.com/api/v1/stream"
headers = {"Authorization": f"Bearer {os.environ['FANDUEL_API_KEY']}", "Accept": "text/event-stream"}
last_id = None

while True:  # the server recycles connections; reconnect with Last-Event-ID and lose nothing
    if last_id:
        headers["Last-Event-ID"] = last_id
    with requests.get(URL, params={"league": "NBA,NFL", "kinds": "line_move,price_move"},
                      headers=headers, stream=True, timeout=(10, 60)) as r:
        r.raise_for_status()
        event = None
        for raw in r.iter_lines(decode_unicode=True):
            if raw.startswith("id: "):
                last_id = raw[4:]
            elif raw.startswith("event: "):
                event = raw[7:]
            elif raw.startswith("data: ") and event == "change":
                c = json.loads(raw[6:])
                print(c["at"], c["kind"], c["player"], c["stat"], c["previous"]["line"] if c["previous"] else None, "->", c["current"]["line"])
            elif raw.startswith("data: ") and event == "gap":
                print("Changes were pruned before this cursor: re-read /api/v1/props, then carry on")
```

## Node.js

stream.mjs

```
// node stream.mjs (Node 18+). Server-side only: the key must never reach a browser.
let lastId = null;
for (;;) {
  const res = await fetch("https://fanduelapi.com/api/v1/stream?league=NBA", {
    headers: { Authorization: `Bearer ${process.env.FANDUEL_API_KEY}`, Accept: "text/event-stream", ...(lastId ? { "Last-Event-ID": lastId } : {}) },
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  const decoder = new TextDecoder();
  let buffer = "";
  for await (const chunk of res.body) {
    buffer += decoder.decode(chunk, { stream: true });
    let end;
    while ((end = buffer.indexOf("\n\n")) >= 0) {
      const frame = Object.fromEntries(buffer.slice(0, end).split("\n").filter((l) => !l.startsWith(":")).map((l) => [l.slice(0, l.indexOf(":")), l.slice(l.indexOf(":") + 2)]));
      buffer = buffer.slice(end + 2);
      if (frame.id) lastId = frame.id;
      if (frame.event === "change") console.log(JSON.parse(frame.data));
    }
  }
}
```

Keep `lastId` somewhere durable and a restart resumes from it too. A worker that applies these changes to a local board is on [the Node.js page](https://fanduelapi.com/fanduel-api-node).

## Stream, webhooks or polling

The stream suits a process that stays up. If your code runs on a schedule or a serverless function, poll `/api/v1/changes` with a cursor instead; if you have a public HTTPS endpoint and Production, take [webhooks](https://fanduelapi.com/fanduel-api-webhooks). How fresh each option is: [real-time data](https://fanduelapi.com/fanduel-api-real-time). A bot built on the change feed is on [the Discord bot page](https://fanduelapi.com/fanduel-discord-bot).

## Questions

## Related

- [Webhooks Register a webhook for FanDuel line and price changes, verify the HMAC signature in Python or Node, read the payload and recover a missed batch.](https://fanduelapi.com/fanduel-api-webhooks)
- [Real-time data How fresh FanDuel odds are on each plan, the meta fields every response carries about it, and which delivery to use when seconds matter.](https://fanduelapi.com/fanduel-api-real-time)
- [Node.js worker A Node.js worker that loads the FanDuel props board once, then applies the change feed with a saved cursor, so a local copy stays current cheaply.](https://fanduelapi.com/fanduel-api-node)
- [Discord bot Post FanDuel prop line moves to a Discord channel: poll the change feed with a saved cursor, filter by size and league, and send through a Discord webhook.](https://fanduelapi.com/fanduel-discord-bot)
- [Pricing Free on delayed data, then real time from the Developer plan.](https://fanduelapi.com/pricing)

## Make your first call in a few minutes

Sign up, create a key in the dashboard, and send it as a Bearer token. The free plan needs no card.

[Get a free API key](https://fanduelapi.com/sign-up)

## FAQ

### Is there a free plan?

Yes. Free includes API access with delayed data, enough to build and test a model.

### Is this affiliated with FanDuel?

No. This is an independent data service operated by FF API Inc. and is not affiliated with or endorsed by FanDuel.

### Which sports are covered?

NBA, NFL, NHL, MLB and college sports as FanDuel lists them, starting with player props.

### How do I cancel?

From the billing portal in your dashboard, at any time.

### How do I get a free key?

Sign up at https://fanduelapi.com/sign-up?ref=ai-claudebot. Free plan: 100 credits a month, data delayed 5 minutes, no card. REST and MCP on every plan.

### Can an AI agent sign up for me?

Yes. The agent sends POST https://fanduelapi.com/api/v1/agent/signup with your email and what you are building (add "ref": "ai-claudebot"), you approve the emailed link, and the agent collects the key. Details: https://fanduelapi.com/connect.
