Stumper Lobbies
Multiplayer endpoints under /v1/games/stumper/lobbies. Live state is delivered over Mercure SSE on topic https://stumper.gg/lobby/{code} — clients fetch the initial snapshot via GET /lobbies/{code} and then subscribe.
All {code} path params are the 6-character public join code. Endpoints are grouped by the lobby phase they belong to.
The non-lobby Stumper endpoints (categories, questions, runs, leaderboards) live in /docs/reference/stumper.
Create & join
GET /v1/games/stumper/lobbies
List currently-joinable public lobbies (status waiting or in_progress), newest first.
- Auth: Public
Response — 200 OK
{
"lobbies": [
{
"id": 812,
"code": "A3F9X2",
"host_name": "anders",
"category": null,
"difficulty": null,
"prompt": null,
"selected_categories": [ "world-history" ],
"blend": false,
"review": true,
"timer": 20,
"num_questions": 10,
"is_endless": false,
"status": "waiting",
"visibility": "public",
"mode": "coop",
"max_players": 8,
"current_index": 0,
"created_at": "2026-04-18T12:00:00+00:00",
"started_at": null,
"finished_at": null,
"current_question_started_at_ms": null,
"server_now_ms": 1713456000000,
"players": [],
"current_question": null,
"answered_player_ids": [],
"host_disconnect": null
}
]
}
POST /v1/games/stumper/lobbies
Create a new lobby. The caller becomes the host.
- Auth: Authenticated (guest or registered)
Request
{
"hostName": "anders",
"category": "world-history",
"timer": 20,
"numQuestions": 10,
"visibility": "public",
"mode": "coop",
"maxPlayers": 8,
"difficulty": "hard",
"prompt": "napoleonic wars"
}
| Field | Type | Constraints |
|---|---|---|
hostName | string | required, max 64. |
category | string|null | category name or random. |
timer | integer|null | 10 | 15 | 20 | 30 | 45 | 60. |
numQuestions | integer|null | 1–100. null for endless. |
visibility | string | public (browser) or private (code-only). Default public. |
mode | string | coop (tentative picks + lock) or versus (immediate). Default coop. |
maxPlayers | integer | 2–16. Default 8. |
difficulty | string|null | max 32. |
prompt | string|null | max 256, [\p{L}\p{N}\s.,!?'"\-:;()/]. |
Response — 201 Created
{
"lobby": { "…": "full lobby object" },
"player": { "id": 42, "name": "anders", "is_host": true }
}
Publishes updated lobby list to SSE subscribers when visibility=public.
GET /v1/games/stumper/lobbies/{code}
Get a lobby's full state. Used for the initial page load before subscribing to the Mercure stream.
- Auth: Public
Response — 200 OK
{
"lobby": { "…": "full lobby object" }
}
Errors
| Code | Meaning |
|---|---|
404 | Lobby not found. |
GET /v1/games/stumper/lobbies/{code}/status
Alias for GET /v1/games/stumper/lobbies/{code}. Fallback when EventSource is unavailable or for state recovery after reconnect.
- Auth: Public
- Response / errors: same as
GET /lobbies/{code}.
POST /v1/games/stumper/lobbies/{code}/join
Join a lobby as a new player. Lobby must be in waiting and not full.
- Auth: Public
Request
{ "name": "Mira" }
| Field | Constraints |
|---|---|
name | 1–64 chars, unique within the lobby. |
Response — 201 Created
{
"player": { "id": 43, "name": "Mira", "is_host": false }
}
Errors
| Code | Meaning |
|---|---|
400 | Not accepting players, lobby full, or name taken. |
404 | Lobby not found. |
Settings & control
PATCH /v1/games/stumper/lobbies/{code}/settings
Update lobby preferences while in waiting. Host only.
- Auth: Host
Request — all fields optional:
{
"difficulty": "hard",
"prompt": "napoleonic wars",
"selected_categories": [ "world-history" ],
"timer": 20,
"blend": true,
"review": true,
"num_questions": 10,
"visibility": "public",
"mode": "coop"
}
Response — 204 No Content. Publishes settings change to subscribers.
Errors
| Code | Meaning |
|---|---|
403 | Caller is not the host. |
404 | Lobby not found. |
POST /v1/games/stumper/lobbies/{code}/start
Start the round. Transitions waiting → in_progress and generates the question set. Host only.
- Auth: Host
Response — 200 OK
{ "lobby": { "…": "full lobby, status=in_progress, current_question populated" } }
Errors
| Code | Meaning |
|---|---|
400 | Already started or not enough players. |
403 | Caller is not the host. |
404 | Lobby not found. |
POST /v1/games/stumper/lobbies/{code}/token
Mint a Mercure subscriber JWT scoped to this lobby's topic. The JWT is returned as a cookie (mercureAuthorization, HttpOnly, SameSite=Lax, path /.well-known/mercure) so EventSource can pick it up automatically.
- Auth: Player in lobby
Response — 204 No Content with the mercureAuthorization cookie set (24-hour TTL).
Errors
| Code | Meaning |
|---|---|
403 | Caller is not a member of the lobby. |
404 | Lobby not found. |
Gameplay
The answer flow differs by lobby mode. Versus lobbies submit through /answer; co-op lobbies use /pick + /lock so the host can synchronize a group reveal.
POST /v1/games/stumper/lobbies/{code}/answer
Submit an answer in a versus lobby. Use /pick + /lock in co-op lobbies instead.
- Auth: Player in lobby
Request
{ "selected_index": 1, "time_taken_ms": 8200 }
selected_index is 0–3 or -1 for timeout. time_taken_ms is 0–600000.
Response — 200 OK with reveal result (shape from LobbyManager::answer). Publishes the answer to subscribers.
Errors
| Code | Meaning |
|---|---|
400 | Co-op lobby (use /pick + /lock), game not in progress, no more questions, or already answered. |
404 | Lobby not found or caller not a player. |
POST /v1/games/stumper/lobbies/{code}/pick
Co-op: tentative pick. Upserts the caller's current selection for the current question without committing. The avatar row reflects tentative picks in real time.
- Auth: Player in lobby
Request
{ "selected_index": 1 }
selected_index is 0–3.
Response — 204 No Content. Publishes tentative pick to subscribers.
Errors
| Code | Meaning |
|---|---|
400 | Versus lobby (use /answer) or game not in progress. |
404 | Lobby not found or caller not a player. |
POST /v1/games/stumper/lobbies/{code}/lock
Co-op: host locks in everyone's tentative picks as real answers and triggers the reveal.
- Auth: Host, co-op only
Response — 200 OK
{ "lobby": { "…": "full lobby with reveal state" } }
Errors
| Code | Meaning |
|---|---|
400 | Not a co-op lobby, game not in progress, or no more questions. |
403 | Caller is not the host. |
404 | Lobby not found. |
POST /v1/games/stumper/lobbies/{code}/next
Advance to the next question. Host only. Enforces a server-side minimum timer to prevent early reveals.
- Auth: Host
Response — 200 OK
{ "lobby": { "…": "current_index incremented, new question populated" } }
Errors
| Code | Meaning |
|---|---|
403 | Caller is not the host. |
404 | Lobby not found. |
POST /v1/games/stumper/lobbies/{code}/reveal
Trigger the reveal of the current question. Internal coordination endpoint, typically called by clients once the timer elapses.
- Auth: Public
Response — 200 OK { "ok": true }. Publishes reveal snapshot to subscribers.
Errors
| Code | Meaning |
|---|---|
404 | Lobby not found. |
POST /v1/games/stumper/lobbies/{code}/heartbeat
Players post roughly every 30 seconds while connected so the server can detect host disconnects.
- Auth: Player in lobby
Response — 200 OK { "ok": true }. If the host was marked stale and now resumes, a host_disconnect cleared event is published.
Errors
| Code | Meaning |
|---|---|
404 | Lobby not found or caller not a player. |
Cleanup
POST /v1/games/stumper/lobbies/{code}/leave
Current player leaves the lobby. If the host leaves, the lobby ends for everyone.
- Auth: Player in lobby
Response — 204 No Content. Publishes player-removal event (or lobby-ended if host).
POST /v1/games/stumper/lobbies/{code}/end
Host ends the lobby early. Transitions to finished and notifies subscribers. The lobby becomes read-only.
- Auth: Host
Response — 200 OK
{ "lobby": { "…": "status=finished" } }
Errors
| Code | Meaning |
|---|---|
403 | Caller is not the host. |
404 | Lobby not found. |
DELETE /v1/games/stumper/lobbies/{code}/players/{playerId}
Host kicks another player from the lobby. The kicked player sees the change on their next Mercure snapshot and the SPA navigates them out of the lobby.
- Auth: Host
Path params
| Field | Constraints |
|---|---|
code | 6-char lobby code. |
playerId | integer. Player ID to remove. |
Response — 204 No Content. Publishes a player-removal event to subscribers.
Errors
| Code | Meaning |
|---|---|
403 | Caller is not the host. |
404 | Lobby not found or player not in lobby. |