A lobby progresses through several states during its lifetime. Understanding the lifecycle helps you design your game’s UI and handle every edge case cleanly.
Status Flow
waiting ─────→ in_queue ─────→ matched ─────→ in_game ─────→ waiting (round end)
│ │ │ │
│ │ │ └──→ waiting (server stopped)
│ │ └──→ in_queue (server create failed)
│ └──→ waiting (timeout / cancel / player leave)
│
└──→ starting ─────→ in_game (host started game directly)
│
└──→ waiting (server create failed)
| Status | Meaning |
|---|
waiting | Open lobby, accepting players |
in_queue | Matchmaking active, searching for opponents |
starting | Host triggered direct start, server provisioning |
matched | Claimed for a match, server being created |
match_found | Match proposed, waiting for player confirmations (optional) |
in_game | Game server running, players connected |
finished | Game complete (rarely used — most lobbies return to waiting) |
Creating a Lobby
POST /v3/lobbies/{config}
x-player-id: host_player
Content-Type: application/json
{
"name": "Friday Night CTF",
"maxPlayers": 8,
"region": "us-east",
"isPrivate": false,
"allowLateJoin": true,
"settings": { "map": "forest", "mode": "ctf" },
"state": { "ready": true }
}
The creator becomes the host. An invite code is generated automatically (even for public lobbies, so friends can share it).
| Field | Required | Default | Notes |
|---|
name | Yes | - | Display name |
maxPlayers | No | 2 | 1–100 |
region | No | us-east | Game server region |
isPrivate | No | false | Hide from GET /v3/lobbies/{config} browse |
allowLateJoin | No | true | Can players join after game starts? |
settings | No | {} | Game-specific data passed to the server |
state | No | null | Host’s initial player state |
The host can only be in one lobby at a time per config. Trying to create a second returns 409 Already in another lobby.
Joining a Lobby
Two ways: by invite code or by lobby ID.
By Invite Code
By Lobby ID
POST /v3/lobbies/{config}/join
x-player-id: new_player
{
"code": "MEOW-42",
"state": { "team": "blue" }
}
Best for “friend shares a code” flow. Works for both public and private lobbies.POST /v3/lobbies/{config}/join
x-player-id: new_player
{
"lobbyId": "a1b2c3d4-...",
"state": { "team": "blue" }
}
Best when the player browsed public lobbies and clicked one.
Join Errors
| Status | Error | Cause |
|---|
404 | Lobby not found | Wrong ID/code |
400 | Lobby is full | currentPlayers >= maxPlayers |
400 | Lobby is not accepting players | status: in_game and allowLateJoin: false |
409 | Already in another lobby | Player is in a different lobby in the same config |
Joining is idempotent — if the player is already in the lobby, it returns the lobby unchanged (no error).
Updating Player State
Every player can update their own state. Use PATCH /me:
PATCH /v3/lobbies/{config}/me
x-player-id: my_player
{
"state": {
"ready": true,
"character": "wizard",
"mmr": 1450
}
}
State merges, not replaces. Existing keys are preserved unless your update overwrites them. To clear a key, set it to null.
State flows automatically into matchmaking_data when the lobby queues, so fields like mmr are used by matchmaking rules.
Host Actions
Only the host can perform these actions. Non-hosts get 403 Not host.
Update Lobby Settings
PATCH /v3/lobbies/{config}/me/settings
x-player-id: host_player
{
"name": "New Name",
"maxPlayers": 16,
"isPrivate": true,
"region": "eu-west",
"settings": { "map": "desert" }
}
Every field is optional — only include what you want to change.
Kick a Player
DELETE /v3/lobbies/{config}/me/players/{playerId}
x-player-id: host_player
The kicker is the host (from x-player-id). The {playerId} in the path is the target.
Hosts can’t kick themselves. Use DELETE /me to leave (which auto-transfers host).
Start the Game Directly
Skip matchmaking and launch a server immediately with the current players:
POST /v3/lobbies/{config}/me/start
x-player-id: host_player
The response has server.status: "launching". Within ~10-30s (or ~5s with pool servers), status transitions to running and clients can connect.
End the Round
DELETE /v3/lobbies/{config}/me/game
x-player-id: host_player
The server stops, lobby returns to waiting, players stay in the lobby.
For matchmaking games, ending just clears this lobby’s reference to the shared server — the actual server stops when all matched lobbies end. For direct games (started via /me/start), ending stops the server entirely.
Leaving
Any player can leave at any time:
DELETE /v3/lobbies/{config}/me
x-player-id: my_player
What happens depends on who leaves:
| Scenario | Behavior |
|---|
| Regular player leaves | Removed from players, currentPlayers decrements |
| Host leaves (with others remaining) | Host auto-transfers to next player |
| Last player leaves | Lobby is deleted, returns {"status": "lobby_deleted"} |
| Player leaves during matchmaking | Matchmaking auto-cancels, lobby returns to waiting |
Transferring Host Manually
The host can pass leadership to another player:
POST /v3/lobbies/{config}/me/host/{playerId}
x-player-id: current_host
Useful when the host needs to AFK or hand off the lobby.
Browsing Lobbies
Anyone (even without a player-id) can browse public lobbies in a config:
GET /v3/lobbies/{config}?region=us-east&status=waiting&limit=20
Returns a paginated list. Private lobbies (isPrivate: true) are excluded automatically.
Query params:
| Param | Purpose |
|---|
region | Filter by region |
status | Filter by lobby status |
limit | Page size (default 50, max 100) |
offset | Pagination offset |
Cleanup & Timeouts
Lobbies don’t stick around forever. The PlayFlow lobby-sweep cron runs continuously and handles:
- Expired waiting lobbies — deleted after
timeout seconds (default 5 min) with no activity
- Stale in-game lobbies — in-game lobbies past 2 hours are cleaned up
- Heartbeat expiry — if
heartbeat: true is enabled in config, players that stop sending heartbeats are removed
- Matchmaking timeout — lobbies in
in_queue past the mode’s timeout drop back to waiting
For most games, you don’t need to send heartbeats. The SSE connection itself is the heartbeat — as long as the player is connected to /me/events, they’re alive. Only enable heartbeat in config if your players use HTTP polling instead of SSE.
Error Codes
| HTTP | Code | Meaning |
|---|
400 | LOBBY_FULL | Capacity exceeded |
400 | LOBBY_NOT_ACCEPTING | In-game with no late-join |
400 | INVALID_ACTION | E.g., host trying to self-kick |
403 | NOT_HOST | Non-host tried a host-only action |
404 | LOBBY_NOT_FOUND | Lobby doesn’t exist |
404 | PLAYER_NOT_FOUND | Player isn’t in the lobby |
409 | ALREADY_IN_LOBBY | Player is already in another lobby |
409 | GAME_ALREADY_RUNNING | Started a game that’s already in progress |
409 | INVALID_STATE | Wrong status for this operation |
All errors follow the same format:
{
"error": "Lobby is full",
"detail": "Lobby is full (4 of 4)",
"status": 400
}