> ## 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

> [!NOTE]
>
> **Player auth:** If your project has a player-auth provider configured (`custom_jwt`, `playfab`, or `steam`), client-key (`pfclient_*`) requests must also send an `x-player-token: {verified token}` header — `x-player-id` alone only works when the provider is `none` or when you use a server key (`pf_*`). Without it, every request below returns `401`. See [Player Authentication](https://docs.playflowcloud.com/guides/player-authentication) for setup.

> [!NOTE]
>
> 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).

***

## 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, reason }` |
| `ping` | Every 30s (keep-alive) | `{ ts }` (epoch ms) |

### 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":{"status":"running","network_ports":[{"name":"game_udp","host":"1.2.3.4","external_port":30234,"internal_port":7770,"protocol":"udp","tls_enabled":false}]},...}
```

**Key usage:** When `status` becomes `in_game` and `server.status` becomes `running`, read `host` and `external_port` from `server.network_ports[]` (never assume a fixed port — the external port is proxy-allocated) and 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","reason":"match_ended"}
```

The `reason` field tells you why the lobby closed, so you can differentiate a completed match from a timeout:

| `reason` | Meaning |
| :- | :- |
| `match_ended` | The game finished (lobby was `in_game`) |
| `closed_or_expired` | Closed before play — last player left, cleanup timeout, or expiry |
| `closed` | Closed for any other reason |

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

### Event: `ping`

Keep-alive every 30 seconds. Carries a `{ ts }` payload (epoch milliseconds). You can ignore these in your handler — they exist only to keep the connection alive through proxies and load balancers.

```
event: ping
data: {"ts":1752345678901}
```

***

## Client Examples

> [tab] 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] 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.

> [tab] 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] 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: {"ts":1752345678901}
>
> ```
>
> Useful for debugging — watch events live as you trigger state changes from another terminal.

***

## 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.

***

## 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":[]}}
# Note: if match confirmation is enabled for the mode, an intermediate
# lobby_updated with "status":"match_found" fires first — each player must
# call POST /me/confirm-match before the lobby advances to "in_game".

# 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","reason":"match_ended"}
```

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

> [card] [Matchmaking](https://docs.playflowcloud.com/lobbies/matchmaking.md)
>
> How matchmaking finds opponents and forms matches.

> [card] [API Reference](https://docs.playflowcloud.com/api-reference/introduction.md)
>
> Full SSE endpoint reference.


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