> For the complete documentation index, see [llms.txt](https://info.soccerfootball.info/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://info.soccerfootball.info/websocket/channels/stats.md).

# STATS

## STATS Channel

The STATS channel delivers complete statistics snapshots periodically during live matches.

### Availability

| Plan  | Available |
| ----- | --------- |
| BASIC | No        |
| PRO   | No        |
| ULTRA | Yes       |
| MEGA  | Yes       |

### Update Frequency

Update frequency depends on your subscription scope:

* **`match` / `league` scope:** statistics are delivered **immediately** when data changes
* **`all` scope:** statistics are accumulated and delivered as a **batch every 30 seconds** (`batch_update`)

### Events

| Event         | Scope             | Description                                    |
| ------------- | ----------------- | ---------------------------------------------- |
| stats\_update | `match`, `league` | Full statistics snapshot (immediate)           |
| batch\_update | `all`             | Batched statistics for all matches (every 30s) |

***

### stats\_update

Delivers a complete snapshot of all match statistics.

#### Message

```json
{
  "type": "event",
  "channel": "STATS",
  "scope": "match",
  "id": "12345",
  "eventType": "stats_update",
  "eventId": "abc123",
  "ts": 1705314600000,
  "data": {
    "timer": "35",
    "extraTimer": null,
    "status": "IN_PLAY",
    "score": "1;0",
    "score1h": null,
    "score2h": null,
    "possession": "55;45",
    "attacks": "48;32",
    "dangerousAttacks": "22;15",
    "shotsOnTarget": "4;2",
    "shotsOffTarget": "3;5",
    "totalShots": "7;7",
    "shotsBlocked": "2;1",
    "corners": "4;2",
    "corners1h": null,
    "fouls": "8;10",
    "yellowCards": "1;2",
    "redCards": "0;0",
    "yellowToRed": "0;0",
    "substitutions": "0;1",
    "throwIns": "12;9",
    "injuries": "0;1",
    "penalties": "0;0",
    "offsides": "2;1",
    "xg": "0.85;0.42"
  }
}
```

#### Data Fields

**Match State**

| Field        | Type        | Description                             |
| ------------ | ----------- | --------------------------------------- |
| `timer`      | string      | Current match minute                    |
| `extraTimer` | string/null | Extra time minutes (e.g., "3" for 45+3) |
| `status`     | string      | Match status                            |
| `score`      | string      | Current score "teamA;teamB"             |
| `score1h`    | string/null | First half score (after halftime)       |
| `score2h`    | string/null | Second half score (after full time)     |

**Statistics**

All statistics are in format `"teamA;teamB"`:

| Field              | Type        | Description                         |
| ------------------ | ----------- | ----------------------------------- |
| `possession`       | string      | Ball possession percentage          |
| `attacks`          | string      | Total attacks                       |
| `dangerousAttacks` | string      | Dangerous attacks                   |
| `shotsOnTarget`    | string      | Shots on target                     |
| `shotsOffTarget`   | string      | Shots off target                    |
| `totalShots`       | string      | Total shots                         |
| `shotsBlocked`     | string      | Blocked shots                       |
| `corners`          | string      | Total corners                       |
| `corners1h`        | string/null | First half corners                  |
| `fouls`            | string      | Fouls committed                     |
| `yellowCards`      | string      | Yellow cards                        |
| `redCards`         | string      | Red cards                           |
| `yellowToRed`      | string      | Second yellow (yellow to red) cards |
| `substitutions`    | string      | Substitutions made                  |
| `throwIns`         | string      | Throw-ins                           |
| `injuries`         | string      | Injuries                            |
| `penalties`        | string      | Penalties awarded                   |
| `offsides`         | string      | Offsides                            |
| `xg`               | string/null | Expected goals (xG)                 |

#### Match Status Values

| Status        | Description             |
| ------------- | ----------------------- |
| `NOT_STARTED` | Match hasn't started    |
| `IN_PLAY`     | First half in progress  |
| `HALFTIME`    | Halftime break          |
| `IN_PLAY_2H`  | Second half in progress |
| `EXTRA_TIME`  | Extra time in progress  |
| `PENALTIES`   | Penalty shootout        |
| `ENDED`       | Match finished          |
| `POSTPONED`   | Match postponed         |
| `CANCELLED`   | Match cancelled         |

***

### batch\_update

Delivers a batch of statistics snapshots for all live matches. Sent every 30 seconds to `all` scope subscribers only.

#### Message

```json
{
  "type": "batch_update",
  "channel": "STATS",
  "scope": "all",
  "timestamp": 1705314600000,
  "matches": [
    {
      "matchId": "12345",
      "leagueId": "67890",
      "data": {
        "timer": "35",
        "status": "IN_PLAY",
        "score": "1;0",
        "possession": "55;45",
        "..."
      }
    },
    {
      "matchId": "12346",
      "leagueId": "67890",
      "data": {
        "timer": "72",
        "status": "IN_PLAY_2H",
        "score": "2;2",
        "possession": "48;52",
        "..."
      }
    }
  ]
}
```

#### Fields

| Field                | Type    | Description                                              |
| -------------------- | ------- | -------------------------------------------------------- |
| `type`               | string  | Always `"batch_update"`                                  |
| `channel`            | string  | Always `"STATS"`                                         |
| `scope`              | string  | Always `"all"`                                           |
| `timestamp`          | integer | Batch creation timestamp in milliseconds                 |
| `matches`            | array   | Array of match updates accumulated since last batch      |
| `matches[].matchId`  | string  | Match public ID                                          |
| `matches[].leagueId` | string  | League/Championship public ID                            |
| `matches[].data`     | object  | Statistics snapshot (same fields as `stats_update` data) |

> **Note:** If a match has multiple updates within a 30-second window, only the latest data for that `matchId` is included in the batch.

***

### Subscription Examples

#### Subscribe to all stats updates

```json
{"action": "subscribe", "channel": "STATS", "scope": "all"}
```

#### Subscribe to a specific match

```json
{"action": "subscribe", "channel": "STATS", "scope": "match", "id": "b01d28ac92221034"}
```

#### Subscribe to a league

```json
{"action": "subscribe", "channel": "STATS", "scope": "league", "id": "f2b16fa54cb525d"}
```

***

### Example: Processing Stats

```javascript
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);

  if (msg.type !== 'stats_update') return;

  const stats = msg.data;

  // Parse possession
  const [homePoss, awayPoss] = stats.possession.split(';').map(Number);
  console.log(`Possession: Home ${homePoss}% - Away ${awayPoss}%`);

  // Parse shots
  const [homeShots, awayShots] = stats.totalShots.split(';').map(Number);
  const [homeSoT, awaySoT] = stats.shotsOnTarget.split(';').map(Number);
  console.log(`Shots: Home ${homeShots} (${homeSoT} on target) - Away ${awayShots} (${awaySoT} on target)`);

  // Parse corners
  const [homeCorners, awayCorners] = stats.corners.split(';').map(Number);
  console.log(`Corners: Home ${homeCorners} - Away ${awayCorners}`);

  // Parse xG if available
  if (stats.xg) {
    const [homeXg, awayXg] = stats.xg.split(';').map(parseFloat);
    console.log(`xG: Home ${homeXg.toFixed(2)} - Away ${awayXg.toFixed(2)}`);
  }
};
```

### Example: Building a Stats Display

```javascript
function formatStats(stats) {
  const parseValue = (val) => val ? val.split(';') : ['-', '-'];

  return {
    timer: `${stats.timer}'${stats.extraTimer ? `+${stats.extraTimer}` : ''}`,
    score: parseValue(stats.score).join(' - '),
    possession: {
      home: parseValue(stats.possession)[0] + '%',
      away: parseValue(stats.possession)[1] + '%'
    },
    shots: {
      home: parseValue(stats.totalShots)[0],
      away: parseValue(stats.totalShots)[1]
    },
    shotsOnTarget: {
      home: parseValue(stats.shotsOnTarget)[0],
      away: parseValue(stats.shotsOnTarget)[1]
    },
    corners: {
      home: parseValue(stats.corners)[0],
      away: parseValue(stats.corners)[1]
    },
    fouls: {
      home: parseValue(stats.fouls)[0],
      away: parseValue(stats.fouls)[1]
    },
    cards: {
      home: `${parseValue(stats.yellowCards)[0]}Y ${parseValue(stats.redCards)[0]}R`,
      away: `${parseValue(stats.yellowCards)[1]}Y ${parseValue(stats.redCards)[1]}R`
    }
  };
}
```

***

### Notes

* Statistics are only sent while the match is live
* Null values indicate the statistic is not yet available
* The snapshot includes ALL available statistics, not just changed values
* For real-time delta updates, use the STATS\_REALTIME channel (MEGA plan only)
