Football live channel#
wss://sports.bzzoiro.com/live/football/
Auth, subscribe protocol and close codes are shared across channels — see the overview. This page documents every server frame with its real shape.
Coverage: Basic vs Full (WS+)#
Every covered match streams score, stats, positional data and odds. Matches
flagged websocket_plus: true on GET /api/v2/events/live/ additionally
stream action frames: individual match actions (~100 ms latency) with
pitch coordinates and player info. The channel picks the best available
source automatically — the subscribed frame tells you which you got:
source |
You receive |
|---|---|
"basic" |
event, livedata (~5 s cadence), odds |
"full" |
Everything above + action frames + a history replay on subscribe |
subscribed — snapshot on subscribe#
{
"type": "subscribed",
"event_id": 223510,
"source": "full",
"event": { "…": "same shape as the event frame below" },
"livedata": [ "… up to 30 recent livedata frames …" ],
"history": [ "… recent action frames (full only) …" ],
"odds": { "…": "same shape as the odds frame below" }
}
event — score, clock and stats#
Sent whenever match state changes:
{
"type": "event",
"event_id": 223510,
"home": { "id": 3021, "name": "Aldosivi", "short_name": "ALD" },
"away": { "id": 3044, "name": "Gimnasia y Esgrima", "short_name": "GYE" },
"score": { "home": 1, "away": 0 },
"time": {
"minute": 67, "second": 34, "display": "67'",
"period": 2, "injury_time": null,
"period_started_at_uts": 1785692700,
"status": "live", "kickoff_at": "2026-08-02T17:30:00+00:00"
},
"stats": {
"home": { "possession": 55, "shots_total": 11, "corners": 5, "xg": 1.42, "…": "…" },
"away": { "possession": 45, "shots_total": 7, "corners": 3, "xg": 0.71, "…": "…" }
},
"websocket_plus": true
}
Note: the clock lives under
time(time.minute,time.display) — not at the top level.
livedata — ball position and situations#
Positional/situation updates, ~5 s cadence:
{
"type": "livedata",
"event_id": 223510,
"uts": 1785695254,
"side": "home",
"situation": "dangerous_attack",
"coordinates": [ { "x": 78.5, "y": 41.2 } ],
"commentary": "Dangerous attack — Aldosivi"
}
uts— unix timestamp in secondsside—"home","away"ornullcoordinates— an array of{x, y}points (percent of pitch length × width, attacking left→right); usually one pointsituationvalues include:goal,corner,freekick,throwin,offside,goalkeeper_saved,shotoffwoodwork,dangerous_attack,attack,possession,safe
action — per-action events (Full only)#
Individual match actions with coordinates, ~100 ms after they happen:
{
"type": "action",
"event_id": 223510,
"action_type": "goal",
"x": 87.3, "y": 51.2,
"team": "home",
"player": { "id": 40112, "name": "T. Fernández" },
"score": { "home": 1, "away": 0 },
"qualifiers": [ "left_foot" ],
"minute": 67, "second": 34, "period": 2,
"ts": 1785695254123
}
period#
| value | meaning |
|---|---|
| 1 | first half |
| 2 | second half |
| 3 | extra time, first half |
| 4 | extra time, second half |
| 5 | penalty shoot-out |
minute and second are the clock within the match, not within the
period, so a 47th-minute action in the second half reads "minute": 47,
"period": 2.
temp_* — provisional actions#
Three action types are provisional and always followed by a corrected frame:
| provisional | confirmed by |
|---|---|
temp_goal |
goal, or deleted_event if it is disallowed |
temp_attempt |
the settled attempt type (attempt_saved, miss, post, goal) |
temp_save |
save / attempt_saved |
Treat them as "this probably just happened" and never as final. A goal that VAR
disallows arrives as temp_goal and then deleted_event; if you have already
counted it, the second frame is your instruction to take it back.
Corrections and revisions#
Nothing is ever edited in place — a correction is a new frame:
action_type |
what it means |
|---|---|
deleted_event |
a previously sent action did not happen; drop it |
rescinded_card |
a card was withdrawn |
contentious_referee_decision |
flagged as disputed; the original stands |
The score field on every action frame carries the score as of that action,
so it is the cheapest way to stay consistent: if your running total ever
disagrees with the score on the newest frame, trust the frame.
history vs live#
On subscribe with "coverage": "full" the subscribed frame carries a
history array: the recent action frames replayed so a client joining at
minute 70 is not blind to the first 69. They are the same shape as live frames.
Everything that arrives afterwards is live. A history replay can contain a
temp_* that was already corrected — apply the corrections in order and you
land in the right place.
qualifiers#
Free-form string labels attached to an action (left_foot, header,
big_chance, assisted, …). They are provider vocabulary and the set is open:
treat an unknown qualifier as extra information, never as an error. An action
with no qualifiers sends an empty list.
action_type — the complete list#
64 values. Anything not in this table arrives as unknown_<tid>, which is the
signal that the upstream vocabulary grew and ours has not caught up yet.
On the ball
| tid | action_type |
|---|---|
| 1 | pass |
| 3 | take_on |
| 7 | tackle |
| 8 | interception |
| 9 | turnover |
| 10 | save |
| 11 | claim |
| 12 | clearance |
| 13 | miss |
| 14 | post |
| 15 | attempt_saved |
| 16 | goal |
| 38 | temp_goal |
| 39 | temp_attempt |
| 41 | punch |
| 42 | good_skill |
| 44 | aerial |
| 45 | challenge |
| 49 | ball_recovery |
| 50 | dispossessed |
| 51 | error |
| 52 | keeper_pickup |
| 53 | cross_not_claimed |
| 54 | smother |
| 56 | shield_ball_opp |
| 60 | chance_missed |
| 61 | ball_touch |
| 74 | blocked_pass |
| 83 | attempted_tackle |
Stoppages and restarts
| tid | action_type |
|---|---|
| 2 | offside_pass |
| 4 | foul |
| 5 | out |
| 6 | corner_awarded |
| 27 | start_delay |
| 28 | end_delay |
| 55 | offside_provoked |
| 57 | foul_throw_in |
| 58 | penalty_faced |
| 59 | keeper_sweeper |
| 64 | resume |
| 67 | fifty_fifty |
| 68 | referee_drop_ball |
| 80 | drop_of_ball |
Cards and discipline
| tid | action_type |
|---|---|
| 17 | card |
| 47 | rescinded_card |
| 65 | contentious_referee_decision |
Squad and setup
| tid | action_type |
|---|---|
| 18 | player_off |
| 19 | player_on |
| 20 | player_retired |
| 21 | player_returns |
| 34 | team_setup |
| 36 | player_changed_jersey |
| 40 | formation_change |
Period and coverage
| tid | action_type |
|---|---|
| 24 | condition_change |
| 25 | official_change |
| 30 | period_end |
| 32 | period_start |
| 37 | collection_end |
| 70 | injury_time_announcement |
| 75 | delayed_start |
| 76 | early_end |
| 79 | coverage_interruption |
Provisional and corrections
| tid | action_type |
|---|---|
| 43 | deleted_event |
| 63 | temp_save |
odds — consensus odds (~30 s)#
{
"type": "odds",
"event_id": 223510,
"odds": {
"match_winner": { "home": 2.10, "draw": 3.20, "away": 3.60 },
"over_under": { "over_15": 1.28, "under_15": 3.75, "over_25": 2.05,
"under_25": 1.78, "over_35": 3.90, "under_35": 1.26 },
"btts": { "yes": 2.00, "no": 1.80 },
"asian_handicap": [
{ "line": -0.75, "push": "half", "home": 2.28, "away": 1.66, "bookmaker_count": 8 },
{ "line": -0.5, "push": "none", "home": 1.98, "away": 1.88, "bookmaker_count": 11 },
{ "line": -0.25, "push": "half", "home": 1.74, "away": 2.12, "bookmaker_count": 8 },
{ "line": 0.0, "push": "full", "home": 1.52, "away": 2.54, "bookmaker_count": 9 }
]
},
"updated_at": "2026-08-02T18:37:02+00:00"
}
asian_handicap#
A list, not a fixed set of keys: a match quotes around sixteen lines at once and
which ones are live moves with the score, so there is no stable key set to
promise. Ascending by line, and always present — an empty list when nothing is
quoted, never null.
line is home-relative. -0.25 means the home team starts a quarter of a
goal down, so home is the price of that handicap and away is the price of
the mirrored +0.25. Quarter lines (±0.25, ±0.75) are quoted and so are
whole ones.
push is the settlement rule when the handicap-adjusted result lands exactly on
the line — the half of an Asian market that a price cannot express on its own:
push |
Lines | If the adjusted result lands on the line |
|---|---|---|
"none" |
±0.5, ±1.5, … | Cannot happen — every bet wins or loses outright |
"full" |
0, ±1, ±2, … | Stake returned |
"half" |
±0.25, ±0.75, … | Stake is split across the two neighbouring lines: half returned, half settled win or lose |
A rung is published only when both legs are quoted. A one-sided rung is a book that pulled the other half, not a price, and it looks exactly like value that is not there.
Prices are averaged across the books actually quoting that rung;
bookmaker_count is how many. While a match is in progress the average is
rebuilt from the books' own in-play rows.
The ~30 s is how often the channel checks, not how often prices move. This frame
is re-sent only when the values in its odds block actually changed, so quiet
stretches are normal and expected — the underlying prices are re-read on a
cadence measured in tens of minutes, not seconds. See
Refresh cadence.
odds_book — one bookmaker (~30 s)#
Sent only if you subscribed with bookmaker_slug. Same odds block plus
"bookmaker_slug" and "bookmaker_name". Slugs:
GET /api/v2/bookmakers/.
Its asian_handicap list carries that one book's own ladder and omits
bookmaker_count, which would always be 1.
Again 30 s is the check interval, not the rate at which that bookmaker moves its
price. This frame de-duplicates on updated_at rather than on the prices
themselves, so unlike the consensus frame above it can repeat an identical
odds block when we re-read the line and found it unchanged. Compare the values
if you need to act only on real moves.
Other frames#
unsubscribed, pong, and error (codes listed in the
overview).