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
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
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 connectedRight after connection opens Full lobby state lobby_updatedAny time the lobby changes Full lobby state queue_statsEvery 10s while in_queue { playersSearching, lobbiesInQueue, avgWaitSeconds }lobby_deletedLobby was deleted { id }pingEvery 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.
Client Examples
JavaScript (Browser)
Unity (C#)
Godot (GDScript)
curl (testing)
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" ) {
connectToGameServer ( data . server . ports [ 0 ]. host , data . server . ports [ 0 ]. port );
}
break ;
case "queue_stats" :
updateQueueUI ( data . playersSearching , data . avgWaitSeconds );
break ;
case "lobby_deleted" :
console . log ( "Lobby deleted" );
return ;
}
}
}
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 ip = lobby . server . ports [ 0 ]. host ;
var port = lobby . server . ports [ 0 ]. port ;
// Connect your netcode (Mirror, Fishnet, Netcode for GameObjects...)
NetworkManager . singleton . StartClient ( ip , port );
}
}
}
The PlayFlow Unity SDK ships with an SSE handler. You don’t need to write this from scratch.
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
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.
Handling Disconnects
SSE connections can drop due to network issues, proxy timeouts, or server restarts. Your client should:
Detect disconnect — the read loop exits or the connection errors
Reconnect automatically — with exponential backoff (1s, 2s, 4s, max 30s)
Replay state on reconnect — the connected event sends the current full state, so you can recover cleanly
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:
# Every 2-5 seconds
curl "https://api.computeflow.cloud/api/v3/lobbies/{config}/me" \
-H "api-key: pfclient_..." \
-H "x-player-id: {player}"
With polling, you should also enable heartbeat in your lobby config (since the SSE connection isn’t keeping the player alive) and send periodic heartbeats:
POST /v3/lobbies/{config}/me/heartbeat
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","ports":[...]}}
# 5. Server ready
event: lobby_updated
data: {"status":"in_game","server":{"status":"running","ports":[{"host":"1.2.3.4","port":7770}]}}
# → Client now connects to 1.2.3.4:7770
# 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.
Matchmaking How matchmaking finds opponents and forms matches.
API Reference Full SSE endpoint reference.