Skip to content
OddsHouse.io

Documentation

Three calls to a live board.

The full reference is generated from the API's own contract and served by the API, so it cannot drift from the code. This page is the quick start.

Quick start

Authenticate, read, follow.

STEP 1

Authenticate

Send the key in a header. The ?key= form exists for WebSocket clients that cannot set headers; keep keys out of shared URLs.

terminal
API=https://api.oddshouse.io/v1

# any of the three; the header forms keep the key out of logs
curl -H "Authorization: Bearer $ODDSHOUSE_KEY" $API/me
curl -H "x-api-key: $ODDSHOUSE_KEY"             $API/me
curl "$API/me?key=$ODDSHOUSE_KEY"

/v1/me answers with your plan, which is exactly what the feed will let you see:

response
{
  "customer": "acme",
  "usage": "/v1/account/usage",
  "plan": {
    "id": "pro",
    "leagues": "*",
    "books": "*",
    "props": true,
    "rateLimitPerMin": 1200,
    "maxAltLines": null
  }
}

STEP 2

Read the board

List events for a league, then fetch one event's odds map. Every snapshot carries asOf and the stream cursor it reflects.

# The NBA board: one summary row per event,
# stamped with the stream cursor it reflects
curl "https://api.oddshouse.io/v1/events?league=nba&limit=50" \
  -H "x-api-key: $ODDSHOUSE_KEY"

# One event: the flat odds map keyed by oddID,
# every book inside each odd
curl "https://api.oddshouse.io/v1/events/evt_2f9c1e" \
  -H "x-api-key: $ODDSHOUSE_KEY"

# The player-prop screener: one row per (player, stat),
# widest gap between books first
curl "https://api.oddshouse.io/v1/props?league=nba&shared=1&sort=gap" \
  -H "x-api-key: $ODDSHOUSE_KEY"

STEP 3

Follow the stream

Open the socket, optionally subscribe to a narrower scope, and apply every frame whose cursor is after your snapshot's. Remember the last cursor; reconnect with it.

import WebSocket from "ws";

const URL = "wss://api.oddshouse.io/v1/updates";
const ws = new WebSocket(`${URL}?key=${process.env.ODDSHOUSE_KEY}`);
let epoch = null, cursor = null;

ws.on("open", () => ws.send(JSON.stringify({
  action: "subscribe",
  filters: {
    leagues: ["nba"],
    books: ["draftkings", "fanduel"],
    market_groups: ["player_props"],
  },
})));

ws.on("message", (raw) => {
  const frame = JSON.parse(raw);
  if (frame.action === "hello") epoch = frame.data.epoch;
  if (frame.cursor) cursor = frame.cursor; // for resume_from on reconnect
  if (frame.action === "line_update") {
    const { book, player, market, prev, price, dir } = frame.data;
    console.log(book, player ?? market.type, prev, "->", price, dir);
  }
  if (frame.action === "resync_required") snapshotAgain();
});

Reading a price

Three rules that prevent most integration bugs.

  1. 1

    price is decimal

    american is the same price as a string. price: null means no usable price. It is never 0.

  2. 2

    status is separate

    OPEN, SUSPENDED or CLOSED. A price on a suspended selection is display-only.

  3. 3

    the line is per book

    byBookmaker.<book>.overUnder (or .spread), with that book's other lines in altLines. Two books on one odd can sit on different lines.

Identity

The odd id.

points
statID
the stat, or the market
-
all
statEntityID
home · away · all · a player id
-
game
periodID
full game, a half, a map
-
ou
betTypeID
ml · ml3way · sp · ou · yn
-
over
sideID
home · away · draw · over · under

The same id on every book. The line is deliberately not part of it: it lives in each book's cell as overUnder or spread, because two books can sit on different lines for the same odd, and the ladder beneath a main line belongs to that book alone.

Endpoints

Everything under /v1.

MethodPathReturns
GET/v1/meThe authenticated customer and the plan's entitlements.
GET/v1/account/usageToday's requests by endpoint, the rate-limit bucket, open sockets, keys.
GET/v1/eventsThe board: one summary row per event, cursor-paginated, filterable by league, book, phase, date.
GET/v1/events/:idOne event's flat odds map: every odd, every book's cell with its main line and ladder.
GET/v1/oddsSelected odds across events by oddID, league or book, for a column rather than a board.
GET/v1/propsThe player-prop screener: one row per player and stat, each book's line, sorted by gap or movement.
GET/v1/schedulesUpcoming events with kickoff and the books already pricing them.
GET/v1/changesThe change log over HTTP, from a cursor, for polling clients.
WS/v1/updatesThe stream: hello, subscribe, one frame per change, resume by cursor and epoch.
GET/v1/docsThe reference itself: OpenAPI, llms.txt, guides and real example responses.

The stream

Seven frames.

hello
First frame. Your plan, the schema version, the stream epoch, the current cursor, the outcome of any resume.
subscribed
Reply to subscribe. The effective scope after intersecting with the plan, and what was dropped.
line_update
One price or status change: oddID, book, prev, price, dir, deep link, ts_received, ts_broadcast.
score_update
A live score changed: home and away score, period, clock, which book reported it.
updates
With batch=1: a burst of frames as one message, every cursor kept, nothing coalesced.
resumed
The replay after resume_from is complete; what follows is live.
resync_required
The cursor could not be honoured or continuity was lost. Snapshot again, then apply frames after its cursor.

Start on the trial. Upgrade when your users do.

One league, every sportsbook, 7 days, no card. The key you get today is the key you scale with.