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.
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:
{
"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
price is decimal
american is the same price as a string. price: null means no usable price. It is never 0.
2
status is separate
OPEN, SUSPENDED or CLOSED. A price on a suspended selection is display-only.
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.
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.
| Method | Path | Returns |
|---|---|---|
| GET | /v1/me | The authenticated customer and the plan's entitlements. |
| GET | /v1/account/usage | Today's requests by endpoint, the rate-limit bucket, open sockets, keys. |
| GET | /v1/events | The board: one summary row per event, cursor-paginated, filterable by league, book, phase, date. |
| GET | /v1/events/:id | One event's flat odds map: every odd, every book's cell with its main line and ladder. |
| GET | /v1/odds | Selected odds across events by oddID, league or book, for a column rather than a board. |
| GET | /v1/props | The player-prop screener: one row per player and stat, each book's line, sorted by gap or movement. |
| GET | /v1/schedules | Upcoming events with kickoff and the books already pricing them. |
| GET | /v1/changes | The change log over HTTP, from a cursor, for polling clients. |
| WS | /v1/updates | The stream: hello, subscribe, one frame per change, resume by cursor and epoch. |
| GET | /v1/docs | The 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.
Reference
Served by the API itself.
- OpenAPI documentgenerated from the contract types; the source of truth
- llms.txtthe docs index for coding agents
- WebSocket guideframe shapes and the reconnect model
- Odd idsthe five-part grammar and every vocabulary
- Entitlementswhat a plan can see and how absence is reported
- Errorsevery non-200 body and what to do about it
Status page, terms, privacy and acceptable use are published with the launch. Until then, incidents are reported by email to every customer.
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.