Docs/Reference/Stumper Lobbies

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

Response200 OK

JSON
{
   "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

JSON
{
   "hostName": "anders",
   "category": "world-history",
   "timer": 20,
   "numQuestions": 10,
   "visibility": "public",
   "mode": "coop",
   "maxPlayers": 8,
   "difficulty": "hard",
   "prompt": "napoleonic wars"
}
FieldTypeConstraints
hostNamestringrequired, max 64.
categorystring|nullcategory name or random.
timerinteger|null10 | 15 | 20 | 30 | 45 | 60.
numQuestionsinteger|null1–100. null for endless.
visibilitystringpublic (browser) or private (code-only). Default public.
modestringcoop (tentative picks + lock) or versus (immediate). Default coop.
maxPlayersinteger2–16. Default 8.
difficultystring|nullmax 32.
promptstring|nullmax 256, [\p{L}\p{N}\s.,!?'"\-:;()/].

Response201 Created

JSON
{
   "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

Response200 OK

JSON
{
   "lobby": { "…": "full lobby object" }
}

Errors

CodeMeaning
404Lobby 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

JSON
{ "name": "Mira" }
FieldConstraints
name1–64 chars, unique within the lobby.

Response201 Created

JSON
{
   "player": { "id": 43, "name": "Mira", "is_host": false }
}

Errors

CodeMeaning
400Not accepting players, lobby full, or name taken.
404Lobby 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:

JSON
{
   "difficulty": "hard",
   "prompt": "napoleonic wars",
   "selected_categories": [ "world-history" ],
   "timer": 20,
   "blend": true,
   "review": true,
   "num_questions": 10,
   "visibility": "public",
   "mode": "coop"
}

Response204 No Content. Publishes settings change to subscribers.

Errors

CodeMeaning
403Caller is not the host.
404Lobby not found.

POST /v1/games/stumper/lobbies/{code}/start

Start the round. Transitions waitingin_progress and generates the question set. Host only.

  • Auth: Host

Response200 OK

JSON
{ "lobby": { "…": "full lobby, status=in_progress, current_question populated" } }

Errors

CodeMeaning
400Already started or not enough players.
403Caller is not the host.
404Lobby 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

Response204 No Content with the mercureAuthorization cookie set (24-hour TTL).

Errors

CodeMeaning
403Caller is not a member of the lobby.
404Lobby 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

JSON
{ "selected_index": 1, "time_taken_ms": 8200 }

selected_index is 03 or -1 for timeout. time_taken_ms is 0600000.

Response200 OK with reveal result (shape from LobbyManager::answer). Publishes the answer to subscribers.

Errors

CodeMeaning
400Co-op lobby (use /pick + /lock), game not in progress, no more questions, or already answered.
404Lobby 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

JSON
{ "selected_index": 1 }

selected_index is 03.

Response204 No Content. Publishes tentative pick to subscribers.

Errors

CodeMeaning
400Versus lobby (use /answer) or game not in progress.
404Lobby 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

Response200 OK

JSON
{ "lobby": { "…": "full lobby with reveal state" } }

Errors

CodeMeaning
400Not a co-op lobby, game not in progress, or no more questions.
403Caller is not the host.
404Lobby 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

Response200 OK

JSON
{ "lobby": { "…": "current_index incremented, new question populated" } }

Errors

CodeMeaning
403Caller is not the host.
404Lobby 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

Response200 OK { "ok": true }. Publishes reveal snapshot to subscribers.

Errors

CodeMeaning
404Lobby 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

Response200 OK { "ok": true }. If the host was marked stale and now resumes, a host_disconnect cleared event is published.

Errors

CodeMeaning
404Lobby 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

Response204 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

Response200 OK

JSON
{ "lobby": { "…": "status=finished" } }

Errors

CodeMeaning
403Caller is not the host.
404Lobby 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

FieldConstraints
code6-char lobby code.
playerIdinteger. Player ID to remove.

Response204 No Content. Publishes a player-removal event to subscribers.

Errors

CodeMeaning
403Caller is not the host.
404Lobby not found or player not in lobby.