> ## Documentation Index
> Fetch the complete documentation index at: https://docs.therundown.io/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript SDK

> Use TheRundown API from Node.js with Fetch and ws for real-time streaming.

<Note>
  An official JavaScript/TypeScript SDK is coming soon. These examples run in Node.js 22+ ESM (`.mjs` files or `"type": "module"`) and keep credentials in server-side environment variables. REST calls use built-in `fetch`; WebSocket examples use the [`ws`](https://www.npmjs.com/package/ws) package (`npm install ws`). Native browser WebSocket clients cannot set custom handshake headers, so browsers should use an authenticated backend relay.
</Note>

## Configuration

```javascript theme={null}
import WebSocket from "ws";

const API_KEY = process.env.THERUNDOWN_API_KEY;
if (!API_KEY) throw new Error("Set THERUNDOWN_API_KEY");
const BASE_URL = "https://therundown.io/api/v2";
const WS_URL = "wss://therundown.io/api/v2/ws/markets";
```

## Helper Function

```javascript theme={null}
async function apiGet(path, params = {}) {
  const url = new URL(`${BASE_URL}${path}`);
  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, value);
  }

  const response = await fetch(url.toString(), {
    headers: { "X-TheRundown-Key": API_KEY },
  });

  if (response.status === 429) {
    throw new Error("Rate limited. Check Retry-After header.");
  }

  if (!response.ok) {
    throw new Error(`API error: ${response.status} ${response.statusText}`);
  }

  return response.json();
}
```

## Getting Sports

```javascript theme={null}
const { sports } = await apiGet("/sports");

for (const sport of sports) {
  console.log(`${sport.sport_id}: ${sport.sport_name}`);
}
```

## Getting Events with Odds

```javascript theme={null}
const today = new Date().toISOString().split("T")[0];

const data = await apiGet(`/sports/4/events/${today}`, {
  market_ids: "1,2,3",       // Moneyline, Spread, Total
  affiliate_ids: "19,23",    // DraftKings, FanDuel
  main_line: "true",
});

function formatPrice(price) {
  if (price === 0.0001) return "N/A";
  return price > 0 ? `+${Math.round(price)}` : String(Math.round(price));
}

for (const event of data.events) {
  const away = event.teams[0].name;
  const home = event.teams[1].name;
  console.log(`\n${away} @ ${home}`);

  for (const market of event.markets || []) {
    console.log(`  ${market.name}:`);
    for (const participant of market.participants) {
      for (const line of participant.lines) {
        const lineStr = line.value ? ` (${line.value})` : "";
        const prices = Object.entries(line.prices)
          .map(([id, p]) => `${formatPrice(p.price)} @${id}`)
          .join("  ");
        console.log(`    ${participant.name}${lineStr}: ${prices}`);
      }
    }
  }
}
```

## Filtering by Affiliate

```javascript theme={null}
// Only FanDuel lines
const data = await apiGet(`/sports/4/events/${today}`, {
  market_ids: "1,2,3",
  affiliate_ids: "23",  // FanDuel only
  main_line: "true",
});
```

## Getting Player Props

```javascript theme={null}
const props = await apiGet(`/sports/4/events/${today}`, {
  market_ids: "29,35,38,39",  // Points, Rebounds, 3PT, Assists
  affiliate_ids: "19",
});

for (const event of props.events) {
  console.log(`\n${event.teams[0].name} @ ${event.teams[1].name}`);
  for (const market of event.markets || []) {
    console.log(`  ${market.name}:`);
    for (const participant of market.participants) {
      for (const line of participant.lines) {
        const price = line.prices["19"]?.price ?? "N/A";
        console.log(`    ${participant.name}: ${line.value} (${price})`);
      }
    }
  }
}
```

## Stats: sample first, then one completed event

The fixed sample is a zero-data-point schema check. For production stats, request one known completed event with a small `stats_ids` filter, cache the response, and fetch again only when your product needs a correction. Do not turn this into a per-team polling loop.

```javascript theme={null}
// Static sample: authenticate it, but do not poll it.
const sample = await apiGet("/stats/sample");
console.log(sample.sample, sample.event.event_id);

// Production: one completed MLB event and three example catalog fields only.
// Discover current IDs with GET /stats?sport_id=3; do not hard-code these IDs.
const eventId = "e241d4b4967e3bc003cdc2f77666bb82";
const teamBox = await apiGet(`/events/${eventId}/stats`, {
  stats_ids: "271,273,276", // hits, home runs, runs
});
```

The production call requires access to that event's current season. It becomes archive access after the season changes. The IDs shown are examples only: for your own request, get the event ID from the current events route and discover current supported IDs from `GET /stats?sport_id=3` before filtering. Production routes depend on effective stats access. See [Stats Access](/guides/stats-access).

## Historical Odds

```javascript theme={null}
const eventId = "abc123";

// Full market history
const data = await apiGet(`/events/${eventId}/markets/history`, {
  affiliate_ids: "19",
});

for (const entry of data.history) {
  console.log(
    `${entry.updated_at}: line=${entry.line} price=${entry.price} (${entry.change_type})`
  );
}

// Opening lines
const openers = await apiGet(`/events/${eventId}/openers`, {
  market_ids: "1,2,3",
});

// Closing lines
const closing = await apiGet(`/events/${eventId}/closing`, {
  market_ids: "1,2,3",
});
```

## WebSocket Streaming

Install `ws` with `npm install ws` and run this Node.js 22+ ESM code, not a browser.

```javascript theme={null}
function connectWebSocket(options = {}) {
  const { sportIds = "4", marketIds = "1,2,3", onUpdate } = options;

  const url = `${WS_URL}?sport_ids=${sportIds}&market_ids=${marketIds}`;
  const ws = new WebSocket(url, {
    headers: { "X-TheRundown-Key": API_KEY },
  });

  ws.on("open", () => {
    console.log("WebSocket connected");
  });

  ws.on("message", (raw) => {
    const msg = JSON.parse(raw.toString());

    if (msg.meta?.type !== "market_price") return;

    if (onUpdate) {
      onUpdate(msg.data);
    } else {
      const d = msg.data;
      console.log(`Update: event=${d.event_id} market=${d.market_id} price=${d.price}`);
    }
  });

  ws.on("error", (error) => {
    console.error("WebSocket error:", error);
  });

  ws.on("close", (code) => console.log(`WebSocket closed: ${code}`));

  return ws;
}

// Usage
const ws = connectWebSocket({
  sportIds: "4",
  marketIds: "1,2,3",
  onUpdate: (update) => {
    console.log(
      `Event ${update.event_id}: market=${update.market_id} ` +
      `aff=${update.affiliate_id} price=${update.price}`
    );
  },
});
```

## WebSocket with Auto-Reconnect

```javascript theme={null}
function createReconnectingSocket(options = {}) {
  const {
    sportIds = "4",
    marketIds = "1,2,3",
    onUpdate,
    onConnect,
    maxDelay = 30000,
  } = options;

  let ws;
  let reconnectDelay = 1000;
  let closed = false;

  function connect() {
    if (closed) return;

    const url = `${WS_URL}?sport_ids=${sportIds}&market_ids=${marketIds}`;
    ws = new WebSocket(url, {
      headers: { "X-TheRundown-Key": API_KEY },
    });

    ws.on("open", () => {
      console.log("WebSocket connected");
      reconnectDelay = 1000;
      if (onConnect) onConnect();
    });

    ws.on("message", (raw) => {
      const msg = JSON.parse(raw.toString());
      if (msg.meta?.type !== "market_price") return;
      if (onUpdate) onUpdate(msg.data);
    });

    ws.on("close", () => {
      if (closed) return;
      const jitter = Math.random() * 1000;
      const delay = Math.min(reconnectDelay + jitter, maxDelay);
      console.log(`Reconnecting in ${Math.round(delay)}ms...`);
      setTimeout(() => {
        reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
        connect();
      }, delay);
    });

    ws.on("error", () => {
      ws.close();
    });
  }

  connect();

  return {
    close() {
      closed = true;
      ws?.close();
    },
  };
}

// Usage
const connection = createReconnectingSocket({
  sportIds: "4",
  marketIds: "1,2,3",
  onUpdate: (update) => {
    console.log(`Update: event=${update.event_id} market=${update.market_id} price=${update.price}`);
  },
  onConnect: () => {
    console.log("Ready to receive updates");
  },
});

// Later: connection.close();
```

## Error Handling with Retry

```javascript theme={null}
async function apiGetWithRetry(path, params = {}, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const url = new URL(`${BASE_URL}${path}`);
    for (const [key, value] of Object.entries(params)) {
      url.searchParams.set(key, value);
    }

    const response = await fetch(url.toString(), {
      headers: { "X-TheRundown-Key": API_KEY },
    });

    if (response.ok) {
      return response.json();
    }

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("Retry-After") || "0");
      const wait =
        retryAfter > 0
          ? retryAfter * 1000
          : Math.pow(2, attempt) * 1000 + Math.random() * 1000;
      console.log(`Rate limited. Retrying in ${Math.round(wait)}ms...`);
      await new Promise((resolve) => setTimeout(resolve, wait));
      continue;
    }

    throw new Error(`API error: ${response.status} ${response.statusText}`);
  }

  throw new Error("Max retries exceeded");
}
```

## TypeScript Types

If you are using TypeScript, here are type definitions for the core response objects:

```typescript theme={null}
interface Sport {
  sport_id: number;
  sport_name: string;
}

interface Team {
  team_id: number;
  name: string;
}

interface Price {
  price: number;
  is_main_line: boolean;
  updated_at: string;
}

interface Line {
  value: string;
  prices: Record<string, Price>;
}

interface Participant {
  id: number;
  type: string;
  name: string;
  lines: Line[];
}

interface Market {
  market_id: number;
  name: string;
  period_id: number;
  participants: Participant[];
}

interface Event {
  event_id: string;
  sport_id: number;
  teams: Team[];
  markets: Market[];
}

interface EventsResponse {
  events: Event[];
}

interface MarketPriceMessage {
  meta: {
    type: "market_price";
    version: string;
    timestamp: number;
  };
  data: MarketPriceUpdate;
}

interface HeartbeatMessage {
  meta: { type: "heartbeat" };
  data: { now: string };
}

interface MarketPriceUpdate {
  id: number;
  event_id: string;
  affiliate_id: number;
  market_participant_id: number;
  market_id: number;
  line: string;
  price: string;
  previous_price: string;
  price_delta: number;
  is_main_line: boolean;
  normalized_market_participant_id: number;
  normalized_market_participant_type: number;
  sport_id: number;
  updated_at: string;
  liquidity_usd?: number;
}

type WebSocketMessage = MarketPriceMessage | HeartbeatMessage;

```

## Next Steps

<CardGroup cols={2}>
  <Card title="Getting Live Odds" icon="signal" href="/guides/getting-live-odds">
    Detailed guide on fetching odds
  </Card>

  <Card title="WebSocket Streaming" icon="bolt" href="/guides/websocket-streaming">
    Real-time data streaming guide
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    All authentication methods
  </Card>

  <Card title="Rate Limits" icon="gauge" href="/rate-limits">
    Rate limit details and best practices
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.