Growing Discord community — direct access to the developer, live coverage & picks. Join now →
ADDON · $3/MONTH · REAL-TIME

Live Match WebSocket

Subscribe to any live match and receive ball positions, situations, score and stats as they happen. Sub-5 second latency. One socket, many matches.

WebSocket endpoint:
wss://sports.bzzoiro.com/ws/live/

Why is this a paid addon?

The REST API and MCP server are free and stay free. The live WebSocket runs on a different data tier:

  • Premium real-time source. The live ball-position and situation feed comes from a higher-cost provider — well above what we pay for the rest of the platform.
  • Always-on infrastructure. Each subscribed match keeps a polling job alive on our side, multiplexed across subscribers.
  • Sponsor-style billing. $3 per month, paid via PayPal. Cancel by simply not renewing — we don't auto-charge.

What you receive

event Match snapshot

Re-pushed every ~30s and on every score / status change. Stats block is the same shape as /api/v2/events/{id}/stats/ — all 40+ live counters plus tactical metrics (attack / dangerous-attack / ball-safe and their *_pct), computed pass accuracy, and one xg number per side (sum of per-shot xG from the live shotmap).

{
  "type": "event",
  "event_id": 204849,
  "home": {"id": 4823, "name": "Kyoto Sanga",
           "short_name": "Kyoto"},
  "away": {"id": 4825, "name": "Gamba Osaka",
           "short_name": "Gamba"},
  "score": {"home": 1, "away": 1},
  "time": {"minute": 90, "period": "penalties",
           "status": "penalties",
           "kickoff_at": "2026-05-02T11:00:00+00:00"},
  "stats": {
    "home": {
      "ball_possession": 52, "total_shots": 14,
      "shots_on_target": 5, "shots_off_target": 6,
      "shots_inside_box": 9, "blocked_shots": 3,
      "corner_kicks": 7, "fouls": 11,
      "yellow_cards": 2, "red_cards": 0,
      "passes": 412, "accurate_passes":
          {"value": 348, "total": 412, "pct": 84.5},
      "pass_accuracy_pct": 84.5,
      "big_chances": 2, "big_chances_missed": 1,
      "dribbles": 8, "interceptions": 11,
      "tackles": 14, "tackles_won": 9,
      "duels": 78, "ground_duels": 65,
      "aerial_duels": 13, "recoveries": 22,
      "long_balls": 28, "crosses": 16,
      "throw_ins": 14, "free_kicks": 9,
      "goal_kicks": 6, "offsides": 2,
      "clearances": 18, "dispossessed": 12,
      "goalkeeper_saves": 4, "total_saves": 4,
      "high_claims": 1, "punches": 0,
      "hit_woodwork": 1, "touches_in_penalty_area": 24,
      "final_third_entries": 38,
      "fouled_in_final_third": 4,
      "attack": 67, "ball_safe": 64,
      "dangerous_attack": 37,
      "attack_pct": 33, "ball_safe_pct": 41,
      "dangerous_attack_pct": 26,
      "xg": 1.42
    },
    "away": { /* same shape */ }
  }
}

livedata Ball / situation tick

Pushed every ~5s — one frame per upstream timeline event (ball trajectory, corner, free kick, goal, shot, etc.).

{
  "type": "livedata",
  "event_id": 204849,
  "uts": 1777745495516,
  "side": "home",
  "situation": "attack",
  "coordinates": [
    {"x": 65.5, "y": 42.0},
    {"x": 68.2, "y": 44.5},
    {"x": 71.0, "y": 47.0},
    {"x": 73.8, "y": 49.5}
  ]
}

side: "home" / "away" / null. situation values include attack, goal, corner, free_kick, goal_kick, throw_in, penalty, substitution, periodscore, etc.

odds Bookmaker consensus (default)

Pushed every ~30s and only when the consensus actually changes (no redundant frames). Consensus is the arithmetic mean across 11–12 bookmakers; the line is refreshed every 3 min for live + imminent kickoffs.

{
  "type": "odds",
  "event_id": 205764,
  "odds": {
    "match_winner": {
      "home": 2.07, "draw": 4.08, "away": 2.87
    },
    "over_under": {
      "over_15": 1.06, "under_15": 7.96,
      "over_25": 1.26, "under_25": 3.61,
      "over_35": 1.84, "under_35": 1.89
    },
    "btts": {"yes": 1.34, "no": 3.08}
  },
  "updated_at": "2026-05-06T08:36:23+00:00"
}

Decimal odds. Any market not present in the consensus is omitted (the key may be null). Use updated_at for staleness checks; the same value is repeated until upstream moves the line.

odds Markets exposed

GroupOutcomes
match_winnerhome, draw, away
over_underover_15, under_15, over_25, under_25, over_35, under_35
bttsyes, no

Consensus is the true arithmetic mean across the books that quote the line. Goals only — over/under is the match total, not per-team or cards/corners. For a per-bookmaker breakdown or other markets call /api/v2/events/{id}/odds/comparison/.

Per-bookmaker odds subscription

Pass bookmaker_slug in the subscribe message and you will only receive that book's quotes for the match — and only when those specific quotes change. Both the slug (pinnacle, bet365) and the display name (Pinnacle, Bet365) are accepted; the server canonicalises to the slug. Use /api/v2/bookmakers/ to list the available bookmakers — each entry is {slug, name} so you can show the display name in your UI and pass either back here. Unknown bookmakers return a clear error frame (code: "unknown_bookmaker") instead of silently sending nothing.

Subscribe

// Default (consensus)
ws.send(JSON.stringify({
  action: "subscribe",
  event_id: 205764
}));

// Per-bookmaker — only Pinnacle quotes for this match
ws.send(JSON.stringify({
  action: "subscribe",
  event_id: 205764,
  bookmaker_slug: "pinnacle"
}));

// Stop the per-book stream (event/livedata stays)
ws.send(JSON.stringify({
  action: "unsubscribe",
  event_id: 205764,
  bookmaker_slug: "pinnacle"
}));

A client can keep both subscriptions open at once: the consensus odds frame still arrives every ~30 s, and on top of it you get one odds_book frame per change for each book you've asked for.

odds_book Per-bookmaker frame

Re-emitted only when a quote from this specific book moves. Same market shape as odds; missing markets are null.

{
  "type": "odds_book",
  "event_id": 205764,
  "bookmaker_slug": "pinnacle",
  "bookmaker_name": "Pinnacle",
  "odds": {
    "match_winner": {"home": 2.04, "draw": 4.10, "away": 2.84},
    "over_under": {
      "over_15": 1.06, "under_15": 8.50,
      "over_25": 1.285, "under_25": 3.70,
      "over_35": 1.78, "under_35": 1.96
    },
    "btts": {"yes": 1.31, "no": 3.20}
  },
  "updated_at": "2026-05-06T10:18:42+00:00"
}

Live data fields

Two streams reach you: an event snapshot with the running scoreboard / per-side stats, and continuous livedata ticks describing what's happening on the pitch right now.

event Per-side stats stats.home / stats.away

FieldMeaning
ball_possession% of in-play time on the ball
total_shots · shots_on_target · shots_off_target · shots_inside_box · shots_outside_box · blocked_shots · hit_woodworkfull shot-volume breakdown
corner_kicks · fouls · yellow_cards · red_cards · offsidesstandard set-piece + discipline counters
passes · accurate_passes · pass_accuracy_pct · long_balls · crosses · through_ballspassing — ratio fields are {value, total, pct}
big_chances · big_chances_missed · big_saveschance-quality counters
tackles · tackles_won · total_tackles · interceptions · clearances · dispossessed · recoveriesdefensive actions
duels · ground_duels · aerial_duelsduel counters
dribbles · final_third_entries · final_third_phase · fouled_in_final_third · touches_in_penalty_areaattacking-phase counters
goalkeeper_saves · total_saves · high_claims · punchesgoalkeeper actions
throw_ins · free_kicks · goal_kicksset-piece counts
xgcumulative xG — sum of per-shot xg values from the live shotmap, recomputed every frame so it updates the moment a shot is added. The REST /stats/ endpoint additionally exposes an upstream post-match aggregate as xg.actual; that one is almost always null during a live match, so the WS frame drops it.
Premium momentum counters — refreshed every ~2 min
attackattacking phases entered (cumulative)
dangerous_attackdangerous attacks — phases entering the final third with goal threat
ball_safesafe possession phases (mid/own third, low threat)
attack_pct% of total attack phases that were this side's
dangerous_attack_pct% of dangerous attacks that were this side's
ball_safe_pct% of safe possession that was this side's

The eight base fields appear once the upstream stats feed publishes them (expected_goals typically around minute 5–10). The six premium counters arrive on a 2 min cadence while the match is in play and reach the final values once it ends.

livedata situation values

Pair with side (home / away / null) to know which team triggered the situation. Counting "dangerous" situations over a rolling window gives you the equivalent of a "dangerous attacks" stat.

GroupValues
Phase of play possession, attack, dangerous, safe
Set pieces corner, freekick, throwin, goalkick, penalty
Shots shotontarget, shotofftarget, shotoffwoodwork, goalkeeper_saved
Match events goal, offside, injury, substitution, periodscore
Lifecycle match_about_to_start, match_started, kickoff, possible_event

Build your own counters from the stream

Most bookmaker-style live indicators are derivable client-side. Some examples:

// "Dangerous attacks (home, last 3 min)"
const dangerous = {home: 0, away: 0};
ws.onmessage = (e) => {
  const f = JSON.parse(e.data);
  if (f.type === "livedata" && f.situation === "dangerous" && f.side) {
    dangerous[f.side]++;
  }
  if (f.type === "livedata" && f.situation === "shotontarget" && f.side) {
    // shots-on-target counter, set-piece momentum, etc.
  }
};

Protocol

Authenticate by passing your API token in the URL query string (or as the WebSocket subprotocol). One socket can subscribe to up to 10 matches concurrently.

Client → server

{"action": "subscribe",   "event_id": 204849}
{"action": "unsubscribe", "event_id": 204849}
{"action": "ping"}

Server → client

typeWhen
odds~30s tick — consensus across 11–12 bookmakers; re-emitted only when the line actually moves
odds_book~30s tick — single bookmaker's quotes (only when you opt in via bookmaker_slug); re-emitted only on change
subscribedConfirms subscription; embeds initial event, the last 30 livedata frames, and the latest odds snapshot (when available)
event~30s tick + on score/status change
livedata~5s tick — one per new upstream timeline event
unsubscribedAfter your unsubscribe
pongReply to ping
errornot_tracked (event has no live mapping), limit (max subs reached), bad_action, bad_event_id

Connect in 30 seconds

Browser (JavaScript)

const ws = new WebSocket(
  "wss://sports.bzzoiro.com/ws/live/?token=YOUR_TOKEN"
);
ws.onopen = () => ws.send(JSON.stringify({
  action: "subscribe", event_id: 204849
}));
ws.onmessage = (e) => {
  const f = JSON.parse(e.data);
  if (f.type === "livedata") drawTick(f);
  if (f.type === "event")    updateScoreboard(f);
};

Python (websocket-client)

import json, websocket
ws = websocket.create_connection(
    "wss://sports.bzzoiro.com/ws/live/?token=YOUR_TOKEN"
)
ws.send(json.dumps({"action": "subscribe", "event_id": 204849}))
while True:
    print(json.loads(ws.recv()))

Limits & details

10
concurrent matches per socket
~5s
livedata tick interval
30
livedata frames replayed on subscribe
~30s
event refresh tick

Activate live WebSocket — $3.00/month

One-time PayPal payment. No auto-renew, no surprises. The REST API and MCP server stay free.

Log in to activate Create account