Quickstart

Six steps. Working response in your terminal.

1

Get an API key

Sign in at /signup (magic link, GitHub, Google, or X). On first login the dashboard issues a live API key and shows it on /dashboard right away, with the exact first call from Step 2 already filled in with your key: copy it, or hit Run it to fire it from the browser. Copy the key once, the full value is only shown at creation time. Manage or rotate keys later at /dashboard/keys.

Treat the key as a secret. Never commit it to source control or paste it into a public chat. Rotate compromised keys from the dashboard.
2

Make your first call: confirm your key

Start here. GET /v1/user/me takes no params and no ids, so it cannot fail on a guessed argument — and unlike a public endpoint, it cannot succeed without your key. A 200 here proves the whole chain: your account exists, your key was issued, and it authenticates. The only thing to swap is the placeholder.

bashbash
curl https://api.bigballsdata.com/v1/user/me \
  -H "Authorization: Bearer bbs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Do not hand-type that placeholder. Your dashboard shows this exact command with your real key already in it, plus a Run it button. Copy from there and it just works.
Responsejson
{
  "data": {
    "key_id": "…784f",
    "plan": "free",
    "github_connected": false,
    "paused": false,
    "limits": {
      "per_minute": 100,
      "per_day": 1000
    }
  },
  "meta": { "request_id": "..." },
  "error": null
}

Those limits are yours, read back from the key that just authenticated — the same numbers your dashboard quota card shows. Connect GitHub and per_day becomes 2,000.

GET /v1/sports and GET /v1/coverage are public — they work with no header at all, which is exactly why neither is the call to start with: a 200 from them would not have told you your key works. Every other endpoint requires the header, so leave it in. Unauthenticated calls to the public endpoints share a smaller, per-IP rate limit (see rate limits) — worth knowing if you are iterating quickly before pasting in a real key.

The slug values here (football, not soccer) are the exact ones the other endpoints expect for ?sport=.

3

Fetch matches

bashbash
curl "https://api.bigballsdata.com/v1/matches?sport=football&league=epl&limit=3" \
  -H "Authorization: Bearer bbs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

This lists Premier League matches. sport= is optional — a bare /v1/matches returns matches across every sport. But use the exact slugs from GET /v1/sports (it's football, not soccer), and add ?league= for multi-league sports like football to scope to one competition. Add &status=live during a match window for the live feed; without it you get scheduled and recent matches, which return rows year-round.

Every response carries a meta block with meta.request_id (always — quote it in any support email) and meta.source. Live-adapter responses add meta.confidence (0.0 – 1.0) and meta.cached, which tell you the data tier the value came from and how fresh it is. Calls answered from our stored tier — which is what a bare /v1/matches or a multi-league ?sport= returns — report meta.source: "stored" with meta.cached and a meta.note, and carry no confidence. Read the field, don't assume it's there.

4

Discover IDs: list first, then fetch one by id

Every single-resource route — /v1/matches/:id, /v1/players/:id, /v1/teams/:id — takes a Big Balls UUID. You don't guess it and you don't bring one from another provider: you read it off the matching list endpoint. Every row a list returns carries the id field the :id routes expect.

Step one — list, and grab an id:

bashbash
curl "https://api.bigballsdata.com/v1/matches?sport=football&limit=3" \
  -H "Authorization: Bearer bbs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Response — the id is the first field of every rowjson
{
  "data": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",   // <- this is what /v1/matches/:id wants
      "sport": "football",
      "league": "Premier League",
      "home": { "name": "Arsenal", "short_name": "ARS", "logo_url": "https://..." },
      "away": { "name": "Chelsea", "short_name": "CHE", "logo_url": "https://..." },
      "kickoff_utc": "2026-07-16T19:00:00.000Z",
      "status": "scheduled",
      "score": null,
      "linescore": null,
      "attendance": null,
      "broadcast": null,
      "has_odds": true
    }
    /* ... 2 more ... */
  ],
  "meta": {
    "source": "stored",
    "cached": false,
    "request_id": "...",
    "note": "Showing stored football matches across all leagues. Add ?league= ..."
  },
  "error": null
}

?limit= caps the rows (1–200, default 50) on every form of this call, and the API Explorer sends limit=10 by default so a first look stays readable. Two things this endpoint does not return: there is no pagination object — count rows off data.length — and ?page= only applies when you scope to a single league (?sport=football&league=epl). On the cross-league and all-sport forms above, ?page= is accepted and ignored, so raise ?limit= instead of paging.

Step two — fetch that one match by its id:

bashbash
curl "https://api.bigballsdata.com/v1/matches/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
  -H "Authorization: Bearer bbs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

The bare call above needs no ?sport= — the gateway infers the sport from the match itself, and serves the stored record when no live adapter currently lists that match. Add ?sport=<sport>&fields=scores,odds,lineups,stats,events to force the live multi-field envelope instead. Note the trade: with an explicit ?sport=, a match the live adapters don't list returns 404 rather than falling back to the stored row.

The same list-then-fetch pattern powers players and teams — those two :id routes take a ?sport= (reuse the sport field from the list row):

bashbash
# Players: look up by name, then fetch by the id it returns
curl "https://api.bigballsdata.com/v1/players?name=lebron" -H "Authorization: Bearer bbs_live_..."
curl "https://api.bigballsdata.com/v1/players/{id}?sport=basketball" -H "Authorization: Bearer bbs_live_..."

# Teams: list by sport, then fetch by the id it returns
curl "https://api.bigballsdata.com/v1/teams?sport=basketball" -H "Authorization: Bearer bbs_live_..."
curl "https://api.bigballsdata.com/v1/teams/{id}?sport=basketball" -H "Authorization: Bearer bbs_live_..."

One id system, every sport, no source ids in the path.

A numeric id like 12345, or an id from any other system, is not a Big Balls UUID. Passing one to any /v1/matches/:id route returns 400 bad_request immediately — no upstream lookup, no waiting — plus a suggested_fix and a copy-paste example URL. The fix is always the same: call the list endpoint above and copy the id it returns.
5

Install the SDK

Node.jsbash
npm install @bigballsdata/sdk
Use the SDKtypescript
import { BigBallSportsClient } from '@bigballsdata/sdk';

const client = new BigBallSportsClient('bbs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
const { data } = await client.matches.list({ sport: 'football', status: 'live' });
console.log(data);
6

Subscribe to live updates

The gateway bridges live events over socket.io at /live, authenticated with the same API key you use for REST. Rooms are sport:<slug>, league:<id>, and match:<id>. The SDK does not bundle socket.io-client, so install it and pass it in as options.socketIo — that is what the example below does. See WebSockets for the full room model and event types.

Live score streamtypescript
import { BigBallSportsClient } from '@bigballsdata/sdk';
import { io } from 'socket.io-client';

const client = new BigBallSportsClient('bbs_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
const unsubscribe = client.subscribe(
  'sport:football',
  (event) => console.log(event.type, event.data),
  { socketIo: io },
);