> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bayse.markets/llms.txt
> Use this file to discover all available pages before exploring further.

# Combos

> Real-time state changes for your combos

## Overview

The `combos` channel on the `/ws/v1/user` endpoint streams live state changes for your own [combos](https://docs.bayse.markets/concepts/combos.md): placement, games starting, the cashout window closing, settlement, cashout, and payout.

```
wss://socket.bayse.markets/ws/v1/user
```

| Subscription | Channel | Server event types | Description |
| - | - | - | - |
| Combos | `combos` | `combo_placed`, `combo_updated` | State changes for every combo you place. |

## Authentication

Every message you send to `/ws/v1/user` must include an `auth` field, the same as the [orders channel](https://docs.bayse.markets/websocket/user-orders.md#authentication). Use the same public API key you use for authenticated HTTP requests.

```json theme={null}
{
  "auth": {
    "apiKey": "pk_live_..."
  }
}
```

## Subscribe

```json theme={null}
{
  "type": "subscribe",
  "channel": "combos",
  "auth": {
    "apiKey": "pk_live_..."
  }
}
```

The `combos` channel takes no other fields. You receive updates for all of your own combos and nobody else's.

> [!NOTE]
>
> The server does not send a confirmation for the subscribe message. Your first `combo_placed` or `combo_updated` event confirms the subscription is live.

Room name: `combos:<userId>`

## Events received

**`combo_placed`** — Sent once when you place a combo.

**`combo_updated`** — Sent on every later state change of a combo, including:

* A leg's game starting (`uiStatus` becomes `ACTIVE`).
* The cashout window closing (`cashoutAvailable` becomes `false`).
* Settlement (`status` becomes `WON`, `LOST`, or `VOIDED`).
* Cashout (`status` becomes `CASHED_OUT`).
* The payout landing in your wallet (`payoutStatus` becomes `PAID`).

Each event carries the **full current state** of the combo. Replace your local copy with it instead of applying it as a diff. Legs are not included — call [Get combo](https://docs.bayse.markets/api-reference/pm/get-combo.md) when you need the selections.

```json theme={null}
{
  "type": "combo_updated",
  "data": {
    "comboId": "3f8a2c61-9d4e-4b7a-8c15-6e2f0a9b4d73",
    "code": "K7Q2M9XA",
    "status": "WON",
    "uiStatus": "COMPLETED",
    "payoutStatus": "PAID",
    "amount": 10,
    "currency": "USD",
    "price": 0.18,
    "multiplier": 2.93,
    "potentialPayout": 29.33,
    "cashoutAvailable": false,
    "legCount": 3,
    "updatedAt": "2026-11-02T18:32:05.412000Z",
    "statusReason": "1 leg(s) voided",
    "amountPaidOut": 29.33
  },
  "timestamp": 1793644325
}
```

### Combo fields

| Field | Type | Present on | Description |
| - | - | - | - |
| `comboId` | string | All | Combo UUID. |
| `code` | string | All | 8-character share code. |
| `status` | string | All | `PENDING`, `WON`, `LOST`, `VOIDED`, or `CASHED_OUT`. |
| `uiStatus` | string | All | `PENDING`, `ACTIVE`, or `COMPLETED`. |
| `payoutStatus` | string | All | `NONE`, `PROCESSING`, `PAID`, or `FAILED`. |
| `amount` | number | All | Stake, in `currency`. |
| `currency` | string | All | `USD` or `NGN`. |
| `price` | number | All | Combo price per share (0.00–1.00). |
| `multiplier` | number | All | Payout per unit staked. Recomputed when legs are voided. |
| `potentialPayout` | number | All | What you receive if every remaining leg wins. |
| `cashoutAvailable` | boolean | All | Whether cashout is currently offered. |
| `legCount` | number | All | Number of legs. |
| `updatedAt` | string | All | Time of the change, as fixed-width UTC RFC 3339 (`2026-11-02T18:32:05.412000Z`). |
| `statusReason` | string | When present | Why the combo changed — for example, `1 leg(s) voided`, `All legs voided`, or `Only one leg remained after voids`. |
| `amountPaidOut` | number | Once money has moved | Amount credited to your wallet for the payout, refund, or cashout. |

> [!NOTE]
>
> `updatedAt` is always UTC with six fractional digits, so comparing two values as strings orders them correctly. The outer `timestamp` field is Unix seconds.

> [!TIP]
>
> Several updates can arrive close together during one settlement pass — for example, a `status` change followed by `payoutStatus: PAID`. Events are not guaranteed to arrive in order. Keep the latest event per `comboId` by `updatedAt`, and skip any event older than the one you hold.

## Unsubscribe

```json theme={null}
{
  "type": "unsubscribe",
  "channel": "combos",
  "auth": {
    "apiKey": "pk_live_..."
  }
}
```

The server confirms with an `unsubscribed` message for the `combos:<userId>` room.

## Full example

```javascript JavaScript theme={null}
const ws = new WebSocket("wss://socket.bayse.markets/ws/v1/user");

const auth = { apiKey: "pk_live_..." };
const combos = new Map();

ws.addEventListener("open", () => {
  ws.send(JSON.stringify({ type: "subscribe", channel: "combos", auth }));
});

ws.addEventListener("message", (event) => {
  for (const line of event.data.split("\n")) {
    if (!line.trim()) continue;
    const msg = JSON.parse(line);

    switch (msg.type) {
      case "combo_placed":
      case "combo_updated": {
        const known = combos.get(msg.data.comboId);
        // Skip a snapshot older than the one already held.
        if (known && known.updatedAt > msg.data.updatedAt) break;
        combos.set(msg.data.comboId, msg.data);
        console.log(msg.data.code, msg.data.status, msg.data.payoutStatus);
        break;
      }
      case "error":
        console.error(msg.data.message);
        break;
    }
  }
});
```

```python Python theme={null}
import asyncio
import json
import websockets

async def stream_combos(api_key):
    combos = {}
    async with websockets.connect("wss://socket.bayse.markets/ws/v1/user") as ws:
        await ws.send(json.dumps({
            "type": "subscribe",
            "channel": "combos",
            "auth": {"apiKey": api_key},
        }))

        async for message in ws:
            for line in message.split("\n"):
                if not line.strip():
                    continue
                msg = json.loads(line)

                if msg["type"] in ("combo_placed", "combo_updated"):
                    combo = msg["data"]
                    known = combos.get(combo["comboId"])
                    # Skip a snapshot older than the one already held.
                    if known and known["updatedAt"] > combo["updatedAt"]:
                        continue
                    combos[combo["comboId"]] = combo
                    print(f"{combo['code']}: {combo['status']} ({combo['payoutStatus']})")
                elif msg["type"] == "error":
                    print("Error:", msg["data"]["message"])

asyncio.run(stream_combos("pk_live_..."))
```

```go Go theme={null}
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"strings"

	"github.com/gorilla/websocket"
)

func main() {
	conn, _, err := websocket.DefaultDialer.Dial(
		"wss://socket.bayse.markets/ws/v1/user", nil,
	)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	conn.WriteJSON(map[string]any{
		"type":    "subscribe",
		"channel": "combos",
		"auth": map[string]string{
			"apiKey": "pk_live_...",
		},
	})

	for {
		_, data, err := conn.ReadMessage()
		if err != nil {
			log.Fatal(err)
		}
		for _, line := range strings.Split(string(data), "\n") {
			if strings.TrimSpace(line) == "" {
				continue
			}
			var msg struct {
				Type string         `json:"type"`
				Data map[string]any `json:"data"`
			}
			if err := json.Unmarshal([]byte(line), &msg); err != nil {
				continue
			}
			if msg.Type == "combo_placed" || msg.Type == "combo_updated" {
				fmt.Printf("%v: %v (%v)\n", msg.Data["code"], msg.Data["status"], msg.Data["payoutStatus"])
			}
		}
	}
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.