FIFA WORLD CUP 2026
Group AMexico·Czechia·South Africa·South KoreaGroup BBosnia & Herzegovina·Canada·Qatar·SwitzerlandGroup CBrazil·Haiti·Morocco·ScotlandGroup DAustralia·Paraguay·Türkiye·USAGroup ECuraçao·Ecuador·Germany·Côte d'IvoireGroup FJapan·Netherlands·Sweden·TunisiaGroup GBelgium·Egypt·Iran·New ZealandGroup HCabo Verde·Saudi Arabia·Spain·UruguayGroup IFrance·Iraq·Norway·SenegalGroup JAlgeria·Argentina·Austria·JordanGroup KColombia·DR Congo·Portugal·UzbekistanGroup LCroatia·England·Ghana·Panama Group AMexico·Czechia·South Africa·South KoreaGroup BBosnia & Herzegovina·Canada·Qatar·SwitzerlandGroup CBrazil·Haiti·Morocco·ScotlandGroup DAustralia·Paraguay·Türkiye·USAGroup ECuraçao·Ecuador·Germany·Côte d'IvoireGroup FJapan·Netherlands·Sweden·TunisiaGroup GBelgium·Egypt·Iran·New ZealandGroup HCabo Verde·Saudi Arabia·Spain·UruguayGroup IFrance·Iraq·Norway·SenegalGroup JAlgeria·Argentina·Austria·JordanGroup KColombia·DR Congo·Portugal·UzbekistanGroup LCroatia·England·Ghana·Panama
Open full hub →
Growing Discord community — direct access to the developer, live coverage & picks. Join the Discord Join now →
Leagues Matches Predictions Stats
← All sports
Football WebSocket · Live

Football. Live.

Ball tracking, match state, xG and odds pushed over one persistent connection. Basic source updates every ~5s; Full source pushes every on-pitch action with x/y coordinates in ~100ms.

wss://sports.bzzoiro.com/live/football/?token=YOUR_TOKEN
Connect & Auth

Connect

Append your API token as a query parameter. After the connection opens, send JSON subscribe messages. You can subscribe to multiple matches on the same socket.

connect
wss://sports.bzzoiro.com/live/football/?token=YOUR_TOKEN
Your token is the same as the BSD REST API token. Find it in your dashboard.
Subscribe

Subscribe / Unsubscribe

Send these JSON messages over the open socket to control which matches you receive frames for.

outbound
// subscribe to a match
{ "action": "subscribe",   "event_id": 204849 }

// unsubscribe
{ "action": "unsubscribe", "event_id": 204849 }

// keepalive — send if your socket might sit idle >60s
{ "action": "ping" }

Find live event IDs at /api/v2/events/live/. On subscribe you immediately receive the latest snapshot: a subscribed frame containing the current event, livedata and odds.

Ping / Pong

The server does not send unsolicited pings. If your connection may be idle for more than ~60s (e.g. no live matches subscribed), send a ping to keep the socket alive. The server replies immediately with a pong frame:

→ outbound / ← inbound
// you send:
{ "action": "ping" }

// server replies:
{ "type": "pong" }
While subscribed to active matches you will receive livedata/event frames continuously — ping is only needed if you have no subscriptions or all your matches are finished.
Sources

Basic vs Full source

The subscribed frame includes a source field telling you which data quality is available for this match.

Basic

source: "basic" · ~5s tick
  • Ball x/y position (~5s)
  • Match score, minute, period
  • Live odds every ~30s
  • xG, shots, possession counters
  • No action frames

Full

source: "full" · per-action ~100ms
  • Everything in Basic
  • action frames per on-pitch event
  • x/y coordinates for every action
  • Player & team attribution per action
  • Qualifiers array (head, freekick, etc.)
Full coverage is available for top-tier matches (Premier League, La Liga, Champions League, etc.). Most other matches fall back to Basic.
Frame

subscribed

Sent immediately after a successful subscribe. Contains the full current state so you can render a complete view without waiting for the next tick.

inbound · subscribed
{
  "type":     "subscribed",
  "event_id": 204849,
  "source":   "basic",    // "basic" | "full"
  "event":    { /* latest event frame */ },
  "livedata": [ /* up to 30 recent livedata frames */ ],
  "history":  [],           // reserved
  "odds":     { /* latest odds frame */ }
}
FieldTypeDescription
sourcestring"basic" or "full" — determines whether action frames will be sent
eventobject|nullCurrent event snapshot (same shape as event frame)
livedataarrayUp to 30 recent livedata frames, oldest first
oddsobject|nullCurrent odds snapshot (same shape as odds frame)
Frame

event

Match snapshot. Sent every ~30s while the match is live, and on any status/score change.

inbound · event
{
  "type":       "event",
  "event_id":   204849,
  "home":       { "id": 17, "name": "Arsenal" },
  "away":       { "id": 32, "name": "Chelsea" },
  "score":      { "home": 1, "away": 1 },
  "minute":     67,
  "period":     "2H",        // "1H" | "2H" | "ET1" | "ET2" | "P"
  "status":     { "name": "live" },
  "stats": {
    "home": { "shots": 8, "shots_on_target": 3, "xg": 1.24, "possession": 54 },
    "away": { "...": "same shape" }
  }
}
FieldTypeDescription
scoreobject{"home": N, "away": N}
minuteintMatch minute (0-90+)
periodstring"1H", "2H", "ET1", "ET2", "P"
stats.*.xgfloatCumulative xG this match
stats.*.possessionintPercentage, 0–100
Frame

livedata

Ball position update. Sent every ~5s for Basic source; on every action for Full source. x/y are percentages of the pitch (0–100 left-to-right, 0–100 bottom-to-top).

inbound · livedata
{
  "type":      "livedata",
  "event_id":  204849,
  "uts":       1718780400123,   // unix timestamp ms
  "x":         62.4,             // 0–100, left→right
  "y":         48.7,             // 0–100, bottom→top
  "situation": "corner_kick",   // see situations list below
  "minute":    67,
  "period":    "2H"
}

Situations

The situation field describes the current game state:

ValueDescription
free_kickFree kick being taken
corner_kickCorner kick
goal_kickGoal kick
throw_inThrow-in
penaltyPenalty situation
kick_offKick-off (start of half / after goal)
regular_playNormal open play
Frame

action Full source only

Per-action event with x/y coordinates. Only sent when source == "full". Arrives within ~100ms of the on-pitch event.

inbound · action (Full only)
{
  "type":        "action",
  "event_id":    204849,
  "action_type": "shot",       // see types below
  "tid":         4419281,       // tracker action ID
  "x":           87.3,
  "y":           51.2,
  "team":        "home",        // "home" | "away"
  "player":      { "id": 11843, "name": "Saka" },
  "score":       { "home": 1, "away": 1 },
  "qualifiers":  ["on_target", "right_foot"],
  "minute":      67,
  "second":      34,
  "period":      "2H",
  "ts":          1718780434000  // unix ms
}

action_type values

ValueDescription
shotShot attempt
passPass
tackleTackle / duel
foulFoul committed
yellow_cardYellow card shown
red_cardRed card shown
goalGoal scored
saveGoalkeeper save
clearanceClearance
interceptionInterception
cornerCorner awarded
substitutionPlayer substitution
offsideOffside flag
var_reviewVAR check started
Frame

odds

Aggregated best odds across all tracked bookmakers. Sent every ~30s and whenever a significant line movement occurs.

inbound · odds
{
  "type":     "odds",
  "event_id": 204849,
  "uts":      1718780400000,
  "odds": {
    "1x2":     { "home": 2.10, "draw": 3.40, "away": 3.60 },
    "over_15": { "over": 1.18, "under": 4.80 },
    "over_25": { "over": 1.90, "under": 1.90 },
    "over_35": { "over": 3.20, "under": 1.33 },
    "btts":    { "yes":  1.72, "no":   2.05 }
  }
}
Frame

odds_book

Per-bookmaker odds. Sent alongside odds. Use bookmaker_slug to filter to a specific bookmaker.

inbound · odds_book
{
  "type":           "odds_book",
  "event_id":       204849,
  "bookmaker_slug": "bet365",
  "uts":            1718780400000,
  "odds": {
    "1x2": { "home": 2.10, "draw": 3.40, "away": 3.60 }
  }
}
Reference

Error codes

codeDescription
auth_failedToken missing, invalid or expired
subscription_requiredToken valid but WebSocket addon not active
not_trackedMatch exists but has no live tracking data right now
limit10 concurrent match limit reached; unsubscribe first
bad_actionUnknown action value in your message
bad_event_idevent_id not found
error frame
{ "type": "error", "code": "not_tracked", "message": "No live tracking for event 204849", "event_id": 204849 }
Reference

Limits

  • 10 concurrent matches per socket. Mix football and tennis on the same connection.
  • Ball / score frames pushed every ~5s (Basic) or per-action (Full)
  • Odds + match snapshot every ~30s
  • 30 frames replayed on subscribe (latest state)
  • One socket per token is recommended; multiple sockets are allowed but each counts against your limit independently
Reference

Code examples

JavaScript / TypeScript

JavaScript
const ws = new WebSocket(
  "wss://sports.bzzoiro.com/live/football/?token=YOUR_TOKEN"
);

ws.onopen = () => {
  ws.send(JSON.stringify({ action: "subscribe", event_id: 204849 }));
};

ws.onmessage = ({ data }) => {
  const frame = JSON.parse(data);
  switch (frame.type) {
    case "subscribed":
      console.log("source:", frame.source);      // "basic" | "full"
      applyEvent(frame.event);
      applyOdds(frame.odds);
      break;
    case "livedata":
      moveBall(frame.x, frame.y, frame.situation);
      break;
    case "event":
      updateScoreboard(frame.score, frame.minute, frame.period);
      updateStats(frame.stats);
      break;
    case "action":
      addActionMarker(frame.x, frame.y, frame.action_type, frame.team);
      break;
    case "odds":
      renderOdds(frame.odds);
      break;
    case "error":
      console.error(frame.code, frame.message);
      break;
  }
};
ws.onerror = err => console.error("WS error", err);
ws.onclose = ev => console.log("Closed", ev.code, ev.reason);

Python (asyncio)

Python
import asyncio, json, websockets

TOKEN = "YOUR_TOKEN"
URL   = f"wss://sports.bzzoiro.com/live/football/?token={TOKEN}"

async def main():
    async with websockets.connect(URL) as ws:
        await ws.send(json.dumps({"action": "subscribe", "event_id": 204849}))
        async for message in ws:
            frame = json.loads(message)
            if frame["type"] == "livedata":
                print(f'Ball at ({frame["x"]:.1f}, {frame["y"]:.1f}) — {frame["situation"]}')
            elif frame["type"] == "event":
                s = frame["score"]
                print(f'{frame["home"]["name"]} {s["home"]}–{s["away"]} {frame["away"]["name"]} [{frame["minute"]}\']')

asyncio.run(main())
Live demo

Try it — connect to a live match

Connects to wss://sports.bzzoiro.com/live/football/ directly from your browser.

Log in or create an account and activate the WebSocket addon to use the live demo.

Get access — $3.00/month

Football + Tennis included · 10 concurrent matches · auto-renewing