> ## 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.

# V1 to V2 Migration Guide

> Understand the differences between V1 and V2 data models and migrate your application from the legacy lines format to the new markets-based system.

TheRundown V2 API uses a market-based data model that is more flexible and extensible than V1's flat line structure. This guide covers the key differences and provides a mapping to help you migrate.

## Important Notes Before You Start

<Warning>
  **V1 only: Use `teams_normalized`, not `teams`.** V1 event responses contain both a `teams` array and a `teams_normalized` array. The `teams` array uses internal book-specific IDs that are inconsistent across sportsbooks and seasons. **Always use `teams_normalized`** in V1 — these are canonical, stable team IDs that work across all endpoints. In V2, the `teams` array already uses normalized IDs, so you can use `event["teams"]` directly.
</Warning>

<Warning>
  **The V2 delta endpoint is a V1 feature.** Despite living at `/api/v2/delta` and `/api/v2/markets/delta`, these are currently the only V2-path endpoints available and function as upgraded versions of the V1 delta. They return V2-formatted market data but are accessible to V1 API plans. The rest of the V2 API (events, markets, teams, players, stats) requires a V2 subscription.
</Warning>

## Data Model: Lines vs. Markets

The fundamental change in V2 is how odds data is structured.

### V1: Flat Lines Model

In V1, each event contains a `lines` object keyed by `affiliate_id`. Each affiliate entry has separate `moneyline`, `spread`, and `total` objects.

```json theme={null}
{
  "event_id": "abc123",
  "lines": {
    "19": {
      "affiliate_id": 19,
      "moneyline": {
        "moneyline_away": 150,
        "moneyline_home": -170
      },
      "spread": {
        "point_spread_away": 3.5,
        "point_spread_away_money": -110,
        "point_spread_home": -3.5,
        "point_spread_home_money": -110
      },
      "total": {
        "total_over": 220.5,
        "total_over_money": -110,
        "total_under": 220.5,
        "total_under_money": -110
      }
    },
    "23": {
      "affiliate_id": 23,
      "moneyline": {
        "moneyline_away": 145,
        "moneyline_home": -165
      }
    }
  }
}
```

### V2: Markets Model

In V2, each event contains a `markets` array. Each market has `participants` with `lines` and `prices` organized by sportsbook.

```json theme={null}
{
  "event_id": "abc123",
  "markets": [
    {
      "market_id": 1,
      "name": "Moneyline",
      "period_id": 0,
      "participants": [
        {
          "id": 1,
          "type": "TYPE_TEAM",
          "name": "Team A",
          "lines": [
            {
              "value": "",
              "prices": {
                "19": { "price": 150, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" },
                "23": { "price": 145, "is_main_line": true, "updated_at": "2026-02-12T18:28:00Z" }
              }
            }
          ]
        },
        {
          "id": 2,
          "type": "TYPE_TEAM",
          "name": "Team B",
          "lines": [
            {
              "value": "",
              "prices": {
                "19": { "price": -170, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" },
                "23": { "price": -165, "is_main_line": true, "updated_at": "2026-02-12T18:28:00Z" }
              }
            }
          ]
        }
      ]
    }
  ]
}
```

### Key Structural Differences

| Aspect              | V1                                    | V2                                                            |
| ------------------- | ------------------------------------- | ------------------------------------------------------------- |
| Data access         | `event.lines[affiliate_id].moneyline` | `event.markets[].participants[].lines[].prices[affiliate_id]` |
| Sportsbook grouping | Top level (by affiliate)              | Nested inside lines (prices map)                              |
| Market types        | Fixed: moneyline, spread, total       | Extensible: any market ID                                     |
| Player props        | Not supported                         | Full support via market IDs                                   |
| Alternate lines     | Limited                               | Full support, `is_main_line` flag on price                    |
| Period data         | `line_periods` object                 | `period_id` on each market                                    |

## Period Data

### V1: line\_periods

V1 uses a nested `line_periods` object for half/quarter data:

```json theme={null}
{
  "lines": {
    "19": {
      "moneyline": { "moneyline_away": 150, "moneyline_home": -170 },
      "line_periods": {
        "period_first_half": {
          "moneyline": { "moneyline_away": 130, "moneyline_home": -150 },
          "spread": { "point_spread_away": 2.0, "point_spread_away_money": -110 }
        },
        "period_first_period": {
          "moneyline": { "moneyline_away": 180, "moneyline_home": -210 }
        }
      }
    }
  }
}
```

### V2: period\_id

V2 includes period data as separate market entries distinguished by `period_id`:

```json theme={null}
{
  "markets": [
    {
      "market_id": 1,
      "name": "Moneyline",
      "period_id": 0,
      "participants": [...]
    },
    {
      "market_id": 1,
      "name": "Moneyline",
      "period_id": 1,
      "participants": [...]
    }
  ]
}
```

See [Period IDs](/reference/periods) for the complete list of period identifiers.

## Delta Endpoints

There are four delta endpoints available. The V2-path delta endpoints (`/api/v2/delta` and `/api/v2/markets/delta`) are available to all API plans — they were built as upgrades to the V1 delta and happen to live under the `/v2/` path.

| Endpoint                    | Format          | Description                                                           |
| --------------------------- | --------------- | --------------------------------------------------------------------- |
| `GET /api/v1/delta`         | V1 lines        | Returns full V1 events that changed. Use for legacy integrations.     |
| `GET /api/v1/deltaV2`       | V2 markets      | Returns V2-formatted data via the V1 path. Useful during migration.   |
| `GET /api/v2/delta`         | V2 events       | Returns changed events with V2 market data.                           |
| `GET /api/v2/markets/delta` | V2 markets only | Returns only changed market/price data — most granular and efficient. |

The V2 market delta is the most efficient option — it returns individual price changes rather than entire event objects, significantly reducing bandwidth.

### Delta Polling Pattern

1. **Bootstrap the cursor**: Fetch a `/api/v2/sports/{id}/events/{date}` snapshot and read its integer `meta.delta_last_id` — this is the cursor for `markets/delta`. (Don't bootstrap with `last_id=0`; a cursor too far behind the head is rejected.)
2. **Store the cursor**: Each `markets/delta` response returns its own integer `meta.delta_last_id` — save it for the next poll
3. **Subsequent polls**: Pass the stored cursor to get only the price changes since your last poll
4. **Replace, don't merge**: Each delta contains the full updated object — replace your cached version entirely

```bash theme={null}
# Most efficient: V2 markets delta (integer cursor from the events snapshot)
curl "https://therundown.io/api/v2/markets/delta?\
key=YOUR_API_KEY&last_id=PREVIOUS_DELTA_LAST_ID&sport_id=4&market_ids=1,2,3"
```

<Note>
  `/api/v2/markets/delta` (price changes) and `/api/v2/delta` (full event-object changes) use different cursors: an **integer** for markets delta, an **ordered UUID** for event delta. The integer `meta.delta_last_id` from the events snapshot bootstraps `markets/delta` only — `/api/v2/delta` rejects it. For odds polling, use `markets/delta`.
</Note>

## WebSocket Endpoints

V2 has dedicated WebSocket endpoints for different data types, while V1 uses a single endpoint.

| Version | Endpoint                                | Description                     |
| ------- | --------------------------------------- | ------------------------------- |
| V1      | `wss://therundown.io/api/v1/ws`         | Single stream, all line updates |
| V2      | `wss://therundown.io/api/v2/ws/markets` | Market/price updates            |

V2 WebSockets support more granular filtering (by `market_ids`, `event_ids`, `affiliate_ids`).

## Endpoint Mapping Table

Use this table to find the V2 equivalent of each V1 endpoint.

| V1 Endpoint                             | V2 Equivalent                             | Notes                           |
| --------------------------------------- | ----------------------------------------- | ------------------------------- |
| `GET /api/v1/sports`                    | `GET /api/v2/sports`                      | Same response format            |
| `GET /api/v1/affiliates`                | `GET /api/v2/affiliates`                  | Same response format            |
| `GET /api/v1/sports/{id}/events/{date}` | `GET /api/v2/sports/{id}/events/{date}`   | Response uses markets model     |
| `GET /api/v1/events/{id}`               | `GET /api/v2/events/{id}`                 | Response uses markets model     |
| `GET /api/v1/sports/{id}/events/delta`  | `GET /api/v2/markets/delta`               | V2 returns market-level changes |
| `GET /api/v1/events/{id}/lines`         | `GET /api/v2/events/{id}/markets`         | Markets replace lines           |
| `wss://therundown.io/api/v1/ws`         | `wss://therundown.io/api/v2/ws/markets`   | V2 supports more filters        |
| N/A                                     | `GET /api/v2/events/{id}/markets/history` | New in V2                       |
| N/A                                     | `GET /api/v2/events/{id}/openers`         | New in V2                       |

## Migration Code Example

Here is a side-by-side comparison of extracting odds data in V1 vs. V2.

<CodeGroup>
  ```python V1 (Legacy) theme={null}
  import requests

  API_KEY = "YOUR_API_KEY"

  # V1: Fetch events
  response = requests.get(
      "https://therundown.io/api/v1/sports/4/events/2026-02-12",
      params={"key": API_KEY}
  )
  data = response.json()

  for event in data["events"]:
      away = event["teams_normalized"][0]["name"]
      home = event["teams_normalized"][1]["name"]
      print(f"{away} @ {home}")

      lines = event.get("lines", {})

      # Access DraftKings lines (affiliate 19)
      dk = lines.get("19", {})
      if dk:
          ml = dk.get("moneyline", {})
          spread = dk.get("spread", {})
          total = dk.get("total", {})

          print(f"  ML: {ml.get('moneyline_away')} / {ml.get('moneyline_home')}")
          print(f"  Spread: {spread.get('point_spread_away')} ({spread.get('point_spread_away_money')})")
          print(f"  Total: O {total.get('total_over')} ({total.get('total_over_money')})")

      # Access first half lines
      periods = dk.get("line_periods", {})
      first_half = periods.get("period_first_half", {})
      if first_half:
          fh_ml = first_half.get("moneyline", {})
          print(f"  1H ML: {fh_ml.get('moneyline_away')} / {fh_ml.get('moneyline_home')}")
  ```

  ```python V2 (Recommended) theme={null}
  import requests

  API_KEY = "YOUR_API_KEY"

  # V2: Fetch events with markets
  response = requests.get(
      "https://therundown.io/api/v2/sports/4/events/2026-02-12",
      params={
          "key": API_KEY,
          "market_ids": "1,2,3",
          "affiliate_ids": "19",
      }
  )
  data = response.json()

  for event in data["events"]:
      away = event["teams"][0]["name"]
      home = event["teams"][1]["name"]
      print(f"{away} @ {home}")

      for market in event.get("markets", []):
          mid = market["market_id"]
          period = market.get("period_id", 0)

          period_label = "" if period == 0 else f" (Period {period})"
          print(f"  {market['name']}{period_label}:")

          for participant in market["participants"]:
              for line in participant["lines"]:
                  price_obj = line["prices"].get("19", {})
                  price = price_obj.get("price", "N/A")
                  line_val = line.get("value", "")
                  line_str = f" {line_val}" if line_val else ""
                  print(f"    {participant['name']}{line_str}: {price}")
  ```
</CodeGroup>

## V2 Advantages

<AccordionGroup>
  <Accordion title="Player props and new market types">
    V2 supports player props (points, rebounds, assists, combos), team totals, and other market types that V1 cannot represent. New markets are added as new market IDs without API changes.
  </Accordion>

  <Accordion title="Alternate lines with is_main flag">
    V2 returns all available lines (main and alternates) with an `is_main` flag to distinguish the primary line. Use `main_line=true` to filter to main lines only.
  </Accordion>

  <Accordion title="Granular delta and WebSocket updates">
    V2 delta and WebSocket endpoints deliver market-level updates rather than full event objects, reducing bandwidth and processing overhead.
  </Accordion>

  <Accordion title="Historical data endpoints">
    V2 adds price history and opening lines endpoints that are not available in V1.
  </Accordion>
</AccordionGroup>

## Common Gotchas

### The 0.0001 sentinel value

A price of `0.0001` means the sportsbook has taken that line **off the board** — it is not an error. Display it as "Off Board" or "N/A" and never use it in calculations. See [Sentinel Values](/reference/sentinel-values) for details.

### V1 team IDs are unreliable

V1 events contain both `teams` and `teams_normalized` arrays. The `teams` array uses internal book-specific IDs that vary by sportsbook and can change between seasons. **Always use `teams_normalized`** for stable, canonical team identifiers. In V2, the `teams` array already uses normalized IDs.

### Null vs. empty vs. zero

The API may return `null`, empty strings, empty arrays, or `0` depending on the field and state. Your parsing code should handle all of these:

* A missing `score` object means the game hasn't started
* An empty `markets` array means no odds are available yet
* A `0` value for `game_clock` can mean the period hasn't started or has ended — check `event_status` for context

### line\_value\_is\_participant

This flag on market definitions tells you how to interpret the `value` field on lines:

* `true` — the participant carries the selection. The line value may be a placeholder such as `"0"` or a label.
* `false` — the line value is meaningful and should be displayed when present. It may be a number, player stat threshold, method, round, or other outcome qualifier.

## Next Steps

<CardGroup cols={2}>
  <Card title="Getting Live Odds" icon="signal" href="/guides/getting-live-odds">
    Start using V2 market data
  </Card>

  <Card title="Data Model" icon="diagram-project" href="/reference/data-model">
    V2 hierarchy: events, markets, lines, and prices
  </Card>

  <Card title="Player Props" icon="user" href="/guides/player-props">
    New in V2: player prop markets
  </Card>

  <Card title="WebSocket Streaming" icon="bolt" href="/guides/websocket-streaming">
    V2 WebSocket endpoints
  </Card>

  <Card title="FAQ" icon="circle-question" href="/faq">
    Common questions about the API
  </Card>

  <Card title="Market IDs" icon="hashtag" href="/reference/markets">
    Complete list of V2 market IDs
  </Card>
</CardGroup>
