Skip to main content
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

Lobbies never dead-end. They either stay alive (rematch-ready) or get deleted entirely. There’s no “completed” terminal state.
Self-healing lobbies. When the game server stops — whether the host clicks “End Match”, the server crashes, or its TTL expires — the lobby automatically returns to waiting with all players retained. There is no completed status. Players can queue or start another match immediately.

Creating a Lobby

The creator becomes the host. An invite code is generated automatically (even for public lobbies, so friends can share it).
The host can only be in one lobby at a time per config. Trying to create a second returns 409 Player is already in a lobby.

Fresh lobby on reconnect

Set forceFresh: true on create to atomically replace a lobby the caller already hosts in this config. The backend deletes the old lobby (stopping its game server best-effort) and then creates the new one — all in a single call. It’s a no-op when the player has no prior lobby, so it’s safe to always pass on reconnect. This replaces the old 3-call workaround (TryReconnect → detect stale host lobby → DeleteLobbyCreateLobby) with one request.
Only lobbies the caller hosts are force-deleted. If the caller is merely a guest in someone else’s lobby, the create still returns 409 Player is already in a lobby — they need to leave that one first.

Joining a Lobby

Two ways: by invite code or by lobby ID.
Best for “friend shares a code” flow. Works for both public and private lobbies.

Join Errors

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:
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 Only the host can perform this action.

Update Lobby Settings

Every field is optional — only include what you want to change.

Kick a 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:
The response has server.status: "launching". Within ~10-30s status transitions to running and clients can connect. Read the connection host and port from the server response’s network_ports[] array — the external port is proxy-allocated and differs from your internal port.
playflow.json parity with matchmaking: a directly-started server receives the same custom_data shape a matchmaker-launched one does — mode, teams (grouped from each player’s state.team), players, lobby_id, match_id, and version. A directly-created lobby never enters the queue, so declare the mode yourself in lobby settings.mode (at create time, or later with PATCH /me/settings) and your game server reads it from playflow.json as custom_data.mode. If the lobby did queue at some point, the active matchmaking mode wins.

End the Match (Rematch)

The server stops, the lobby returns to waiting, and all players stay in the lobby with the same invite code and settings. Great for “Play Again” buttons.
If the game server stops on its own (crashes, hits TTL, or your game logic exits cleanly), the lobby auto-heals to waiting with the same effect — no call needed. POST /me/end-match is just the explicit, host-triggered version.

Leaving

Any player can leave at any time:
What happens depends on who leaves:

Host leave behavior

What happens when the host leaves is controlled per lobby config via hostLeaveBehavior: When to use each:
  • "promote" — persistent rooms, squad play, party-based games where the lobby should survive individual churn. This is the default for every lobby config.
  • "delete" — one-shot private rooms, pickup games, host-authored sessions where the host’s presence IS the room. Pairs well with isPrivate: true invite-only lobbies.
Non-host players leaving never triggers delete — only the host walking out can collapse the lobby under "delete". Example config JSON:
When hostLeaveBehavior: "delete" fires, any game server associated with the lobby is stopped best-effort — same cleanup path as the admin delete endpoint.

Browsing Lobbies

Anyone (even without a player-id) can browse public lobbies in a config:
Returns a paginated list. Private lobbies (isPrivate: true) are excluded automatically. Query params:

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.

Heartbeat

Set heartbeat: true on a lobby config to require connected players to ping the server periodically. Players who go silent get evicted by the lobby-sweep cron.
  • heartbeat — when true, every player must send heartbeats
  • heartbeatTimeout — seconds before a player is considered stale. Any player whose last heartbeat is older than this is removed on the next sweep.
When you need it: HTTP-polling clients where the SSE connection at /me/events isn’t available. Mobile background states, low-resource IoT clients, or environments that block long-lived connections.
SSE is the better alternative. If your client can open /me/events, the stream itself is the liveness signal — see Real-time Events — and you can leave heartbeat off.

Admin Delete

Force-delete any lobby by ID. Removes all players and cancels any active matchmaking — regardless of who created the lobby.
  • No x-player-id required. This is a server-side admin operation.
  • {config} — the lobby config name (e.g. "ranked_2v2").
  • {id} — the lobby UUID to delete.
Response (200):
404 if no lobby with that ID exists in your project.
Connected SDK clients in that lobby receive a lobby_deleted SSE event and disconnect cleanly. In contrast, DELETE /{config}/me is “leave lobby” — it only removes the caller, and when the caller is the host a new host is promoted automatically. Use admin delete when you actually mean destroy.
Good uses: dashboard admin actions, anti-cheat or moderation tooling, periodic cleanup for lobbies your app determines are abandoned.

Invite Codes

inviteCodeConfig on a lobby config controls the codes PlayFlow generates when a lobby is created. Every new lobby (public and private) gets a code so friends can join by sharing it.
With the example above a lobby might get the code PLAY-K7Q2F1. Players then join with:

Error Codes

Every lobby error returns the same JSON shape — a human-readable error message, a detail (the same message), and the HTTP status:
There is no machine-readable error-code enum. Branch on the HTTP status (and, if you must, the exact error string) — do not hard-code a code field, because the API never emits one.
The messages you can expect, by status: