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

# Lobby Lifecycle

> The complete lifecycle of a PlayFlow lobby — from creation to cleanup. Creating, joining, leaving, host actions, and every edge case.

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.

```text theme={null}
waiting ─────→ in_queue ─────→ matched ─────→ in_game ─────→ waiting
    │             │                  │              ↑         (rematch-ready)
    │             │                  │              │
    │             │                  └──→ match_found (if matchConfirmation enabled)
    │             │                         │
    │             │                         └──→ in_game (all confirm)
    │             │                         └──→ waiting  (decline / timeout)
    │             │
    │             └──→ waiting (timeout / cancel / no opponents)
    │
    └──→ starting ─────→ in_game (host started game directly)
              │
              └──→ waiting (server create failed)
```

| Status | Meaning |
| :- | :- |
| `waiting` | Open lobby, accepting players. The default resting state. |
| `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 `POST /me/confirm-match` (only if `matchConfirmation.enabled`) |
| `in_game` | Game server running, players connected |

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

```bash theme={null}
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 |

> [!WARNING]
>
> 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 → `DeleteLobby` → `CreateLobby`) with one request.

```bash theme={null}
POST /v3/lobbies/{config}
x-player-id: host_player
Content-Type: application/json

{
  "name": "Friday Night CTF",
  "maxPlayers": 8,
  "forceFresh": true
}
```

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

> [tab] By Invite Code
>
> ```bash theme={null}
> 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.

> [tab] By Lobby ID
>
> ```bash theme={null}
> 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` | `Player is already in a lobby` | Player is in a different lobby in the same config |

> [!TIP]
>
> 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`:

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

{
  "state": {
    "ready": true,
    "character": "wizard",
    "mmr": 1450
  }
}
```

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

```bash theme={null}
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

```bash theme={null}
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.

> [!WARNING]
>
> 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:

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

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.

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

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

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.

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

```bash theme={null}
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 (default) |
| Last player leaves | Lobby is deleted, returns `{"status": "lobby_deleted"}` |
| Player leaves during matchmaking | Matchmaking auto-cancels, lobby returns to `waiting` |

### Host leave behavior

What happens when the host leaves is controlled per lobby config via `hostLeaveBehavior`:

| Value | Behavior |
| :- | :- |
| `"promote"` (default) | The next player in the lobby becomes the new host. Remaining players stay put with the same invite code, settings, and state. |
| `"delete"` | The entire lobby is destroyed. Every remaining player receives a `lobby_deleted` SSE event on `/me/events` and must create or join a new lobby. |

**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:

```json theme={null}
{
  "id": "private-squad",
  "name": "Private Squad Room",
  "enabled": true,
  "timeout": 300,
  "hostLeaveBehavior": "delete",
  "inviteCodeConfig": {
    "type": "alphanumeric",
    "length": 6,
    "prefix": "SQD-"
  },
  "serverSettings": { "serverSize": "small" }
}
```

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

```bash theme={null}
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`

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

```json theme={null}
{
  "heartbeat": true,
  "heartbeatTimeout": 30
}
```

* `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.

> [!TIP]
>
> SSE is the better alternative. If your client can open `/me/events`, the stream itself is the liveness signal — see [Real-time Events](https://docs.playflowcloud.com/lobbies/real-time.md) — 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.

```bash theme={null}
curl -X DELETE https://api.computeflow.cloud/api/v3/lobbies/{config}/{id} \
  -H "api-key: pf_your_server_key"
```

* 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):**

```json theme={null}
{ "status": "lobby_deleted", "id": "a3f1…-e8c2" }
```

**404** if no lobby with that ID exists in your project.

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

```json theme={null}
{
  "inviteCodeConfig": {
    "type": "alphanumeric",
    "length": 6,
    "prefix": "PLAY-"
  }
}
```

| Field | Values | Notes |
| :- | :- | :- |
| `type` | `numeric`, `alphanumeric`, `alphabetic` | Character set for the generated suffix |
| `length` | 1–16 | Length of the generated suffix (excluding prefix) |
| `prefix` | any string | Optional. Prepended verbatim to the generated code. |

With the example above a lobby might get the code `PLAY-K7Q2F1`. Players then join with:

```bash theme={null}
POST /v3/lobbies/{config}/join
{ "code": "PLAY-K7Q2F1" }
```

***

## Error Codes

Every lobby error returns the same JSON shape — a human-readable `error` message, a `detail` (the same message), and the HTTP `status`:

```json theme={null}
{
  "error": "Lobby is full",
  "detail": "Lobby is full",
  "status": 400
}
```

> [!WARNING]
>
> 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:

| HTTP | `error` message | Meaning |
| :- | :- | :- |
| `400` | `Lobby is full` | Capacity exceeded (`currentPlayers >= maxPlayers`) |
| `400` | `Lobby is not accepting players` | In-game with no late-join |
| `400` | `Invalid action for current lobby state` | E.g., host trying to self-kick |
| `403` | `Only the host can perform this action` | Non-host tried a host-only action |
| `403` | `This lobby is private; join with its invite code` | Tried to join a private lobby by ID |
| `404` | `Lobby not found` | Lobby doesn't exist |
| `404` | `Player not found in lobby` | Player isn't in the lobby |
| `409` | `Player is already in a lobby` | Player is already in another lobby in this config |
| `409` | `Game is already running` | Started a game that's already in progress |
| `409` | `Invalid lobby state transition` | Wrong status for this operation |


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