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

# Real-time Events

> Subscribe to live lobby updates with Server-Sent Events (SSE). Stop polling. Get instant notifications when players join, matches form, servers launch.

PlayFlow provides a single SSE endpoint per player that streams everything you need: lobby updates, queue stats, match notifications, and server status changes. No polling required.

## The Endpoint

```bash theme={null}
GET /v3/lobbies/{config}/me/events
x-player-id: {player_id}
api-key: {key}
Accept: text/event-stream
```

* **One connection per player** is enough — all events for their current lobby flow through it
* The connection stays open indefinitely (with keep-alive pings every 30s)
* When the player leaves the lobby, close the connection

<Info>
  The SSE connection doubles as a **heartbeat**. As long as the stream is open, PlayFlow knows the player is alive. You don't need a separate heartbeat endpoint unless you're not using SSE (e.g., polling clients on legacy platforms).
</Info>

***

## Event Types

| Event           | When it fires                | Data                                                   |
| :-------------- | :--------------------------- | :----------------------------------------------------- |
| `connected`     | Right after connection opens | Full lobby state                                       |
| `lobby_updated` | Any time the lobby changes   | Full lobby state                                       |
| `queue_stats`   | Every 10s while `in_queue`   | `{ playersSearching, lobbiesInQueue, avgWaitSeconds }` |
| `lobby_deleted` | Lobby was deleted            | `{ id }`                                               |
| `ping`          | Every 30s (keep-alive)       | Empty string                                           |

### Event: `connected`

Fires immediately on connection. Gives you the full current state — no need to call `GET /me` first.

```
event: connected
data: {"id":"abc-123","status":"waiting","host":"p1","players":[...],...}
```

### Event: `lobby_updated`

Fires whenever the lobby changes:

* A player joins or leaves
* Host kicks someone
* A player updates their state
* Host changes settings
* Host starts matchmaking / game
* Match found → status becomes `in_game`
* Game server status changes (launching → running)

```
event: lobby_updated
data: {"id":"abc-123","status":"in_game","server":{"ip":"...","port":7770,...},...}
```

**Key usage:** When `status` becomes `in_game` and `server.status` becomes `running`, connect your game client.

### Event: `queue_stats`

Only fires while `status: in_queue`. Emitted every 10 seconds.

```
event: queue_stats
data: {"playersSearching":42,"lobbiesInQueue":8,"avgWaitSeconds":12.3}
```

Use this to show live "42 searching, \~12s wait" in your matchmaking UI.

### Event: `lobby_deleted`

Fires when the lobby is deleted (last player left, host deleted it, or cleanup timeout).

```
event: lobby_deleted
data: {"id":"abc-123"}
```

Close the connection on this event — there's nothing left to subscribe to.

### Event: `ping`

Keep-alive every 30 seconds. Empty data. You can ignore these in your handler — they exist only to keep the connection alive through proxies and load balancers.

```
event: ping
data:
```

***

## Client Examples

<Tabs>
  <Tab title="JavaScript (Browser)">
    ```javascript theme={null}
    const playerId = "player_123";
    const config = "ranked";
    const apiKey = "pfclient_...";

    // Note: EventSource doesn't support custom headers directly.
    // Pass the api-key and player-id via query params or use a polyfill.
    const url = `https://api.computeflow.cloud/api/v3/lobbies/${config}/me/events`;

    // Use fetch + ReadableStream for full header support:
    const res = await fetch(url, {
      headers: {
        "api-key": apiKey,
        "x-player-id": playerId,
        "Accept": "text/event-stream",
      },
    });

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      buffer += decoder.decode(value, { stream: true });
      const events = buffer.split("\n\n");
      buffer = events.pop() ?? "";
      
      for (const block of events) {
        const eventMatch = block.match(/^event: (.+)$/m);
        const dataMatch = block.match(/^data: (.+)$/m);
        if (!eventMatch || !dataMatch) continue;
        
        const eventType = eventMatch[1];
        const data = JSON.parse(dataMatch[1]);
        
        switch (eventType) {
          case "connected":
          case "lobby_updated":
            console.log("Lobby:", data);
            if (data.status === "in_game" && data.server?.status === "running") {
              const port = data.server.network_ports[0];
              connectToGameServer(port.host, port.external_port);
            }
            break;
          case "queue_stats":
            updateQueueUI(data.playersSearching, data.avgWaitSeconds);
            break;
          case "lobby_deleted":
            console.log("Lobby deleted");
            return;
        }
      }
    }
    ```
  </Tab>

  <Tab title="Unity (C#)">
    ```csharp theme={null}
    using System.Collections;
    using UnityEngine;
    using UnityEngine.Networking;

    public class LobbyEventStream : MonoBehaviour
    {
        [SerializeField] string apiKey = "pfclient_...";
        [SerializeField] string playerId = "player_123";
        [SerializeField] string config = "ranked";
        
        UnityWebRequest request;
        
        public IEnumerator Connect()
        {
            var url = $"https://api.computeflow.cloud/api/v3/lobbies/{config}/me/events";
            request = UnityWebRequest.Get(url);
            request.SetRequestHeader("api-key", apiKey);
            request.SetRequestHeader("x-player-id", playerId);
            request.SetRequestHeader("Accept", "text/event-stream");
            request.downloadHandler = new SSEDownloadHandler(OnEvent);
            yield return request.SendWebRequest();
        }
        
        void OnEvent(string eventType, string data)
        {
            switch (eventType)
            {
                case "connected":
                case "lobby_updated":
                    var lobby = JsonUtility.FromJson<Lobby>(data);
                    HandleLobbyUpdate(lobby);
                    break;
                case "queue_stats":
                    var stats = JsonUtility.FromJson<QueueStats>(data);
                    UpdateQueueUI(stats);
                    break;
                case "lobby_deleted":
                    CleanupAndGoToMenu();
                    break;
            }
        }
        
        void HandleLobbyUpdate(Lobby lobby)
        {
            if (lobby.status == "in_game" && lobby.server?.status == "running")
            {
                var port = lobby.server.network_ports[0];
                // Connect your netcode (Mirror, Fishnet, Netcode for GameObjects...)
                NetworkManager.singleton.StartClient(port.host, port.external_port);
            }
        }
    }
    ```

    <Tip>
      The PlayFlow Unity SDK ships with an SSE handler. You don't need to write this from scratch.
    </Tip>
  </Tab>

  <Tab title="Godot (GDScript)">
    ```gdscript theme={null}
    extends Node

    var api_key = "pfclient_..."
    var player_id = "player_123"
    var config = "ranked"
    var http_client: HTTPClient

    func connect_events():
        http_client = HTTPClient.new()
        http_client.connect_to_host("api.computeflow.cloud", 443, TLSOptions.client())
        
        while http_client.get_status() == HTTPClient.STATUS_CONNECTING or http_client.get_status() == HTTPClient.STATUS_RESOLVING:
            http_client.poll()
            await get_tree().process_frame
        
        var headers = [
            "api-key: " + api_key,
            "x-player-id: " + player_id,
            "Accept: text/event-stream",
        ]
        http_client.request(HTTPClient.METHOD_GET, "/api/v3/lobbies/%s/me/events" % config, headers)
        
        # Process stream (simplified)
        while http_client.get_status() == HTTPClient.STATUS_BODY:
            http_client.poll()
            var chunk = http_client.read_response_body_chunk()
            if chunk.size() > 0:
                process_sse_chunk(chunk.get_string_from_utf8())
            await get_tree().process_frame
    ```
  </Tab>

  <Tab title="curl (testing)">
    ```bash theme={null}
    curl -N \
      -H "api-key: pfclient_..." \
      -H "x-player-id: test_player" \
      -H "Accept: text/event-stream" \
      "https://api.computeflow.cloud/api/v3/lobbies/default/me/events"
    ```

    Output (live):

    ```
    event: connected
    data: {"id":"abc-123","status":"waiting",...}

    event: lobby_updated
    data: {"id":"abc-123","status":"waiting","currentPlayers":2,...}

    event: ping
    data:

    ```

    Useful for debugging — watch events live as you trigger state changes from another terminal.
  </Tab>
</Tabs>

***

## Handling Disconnects

SSE connections can drop due to network issues, proxy timeouts, or server restarts. Your client should:

1. **Detect disconnect** — the read loop exits or the connection errors
2. **Reconnect automatically** — with exponential backoff (1s, 2s, 4s, max 30s)
3. **Replay state on reconnect** — the `connected` event sends the current full state, so you can recover cleanly

<Warning>
  While disconnected, you **won't receive events**. You might miss a match, a kick, or a server ready event. Your reconnect logic should fetch `GET /me` on reconnect to ensure state consistency, OR rely on the `connected` event's initial state.
</Warning>

***

## Polling as a Fallback

If SSE isn't available (e.g., your platform blocks streaming), poll `GET /me` instead:

```bash theme={null}
# Every 2-5 seconds
curl "https://api.computeflow.cloud/api/v3/lobbies/{config}/me" \
  -H "api-key: pfclient_..." \
  -H "x-player-id: {player}"
```

When polling, enable `heartbeat: true` in your lobby config and send a heartbeat on each tick (well under `heartbeatTimeout` seconds). Without SSE keeping the player implicitly alive, the `lobby-sweep` cron will otherwise evict inactive players:

```bash theme={null}
POST /v3/lobbies/{config}/me/heartbeat
x-player-id: {player}
```

***

## The Full Matchmaking Flow with SSE

This is what a player's event stream looks like during a matchmaking session:

```
# 1. Connect
event: connected
data: {"status":"waiting","players":[{"id":"p1","isHost":true}],...}

# 2. Player starts matchmaking
event: lobby_updated
data: {"status":"in_queue","matchmaking":{"mode":"5v5","queueStats":{...}}}

# 3. Queue stats stream every 10s
event: queue_stats
data: {"playersSearching":42,"avgWaitSeconds":12.3}

event: queue_stats
data: {"playersSearching":45,"avgWaitSeconds":11.8}

# 4. Match found! Server launching
event: lobby_updated
data: {"status":"in_game","matchId":"m_abc","server":{"status":"launching","network_ports":[]}}

# 5. Server ready
event: lobby_updated
data: {"status":"in_game","server":{"status":"running","network_ports":[{"name":"game_udp","host":"1.2.3.4","external_port":30234,"internal_port":7770,"protocol":"udp","tls_enabled":false}]}}
# → Client now connects to 1.2.3.4:30234 (external_port)

# 6. Game ends, lobby cleanup
event: lobby_deleted
data: {"id":"abc-123"}
```

That's the entire competitive matchmaking experience — queue, find opponents, launch server, connect — delivered through one SSE stream.

<CardGroup cols={2}>
  <Card title="Matchmaking" icon="swords" href="/lobbies/matchmaking">
    How matchmaking finds opponents and forms matches.
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/lobbies-and-matchmaking/lobbyevents">
    Full SSE endpoint reference.
  </Card>
</CardGroup>
