Docs/Reference/Stumper

Stumper

Trivia game endpoints. All under /v1/games/stumper.

Multiplayer lobby endpoints — create, join, configure, play — live in their own page: Stumper Lobbies.

You want to…Go to
Browse cached / pull new questions for solo playQuestions
Run an endless or daily challengeRuns, Challenges
Save / unsave a questionFavorites
Read the leaderboardLeaderboards
Host or join a multiplayer matchStumper Lobbies →

Categories

GET /v1/games/stumper/categories

List available trivia categories with question counts.

  • Auth: Public
  • Cache: 10 minutes

Response200 OK

JSON
{
   "categories": [
      { "name": "world-history", "num_questions": 4211 },
      { "name": "geography",     "num_questions": 3876 }
   ]
}

POST /v1/games/stumper/categorize

Suggest canonical categories matching a free-form prompt. Lets the player type natural language and get back category keys the generator understands.

  • Auth: Public
  • Rate limit: 20 / 60s

Request

JSON
{
   "prompt": "battles of world war 2"
}

Response200 OK

JSON
{
   "categories": [ "world-history", "military-history" ]
}

Questions

Both endpoints return practice questions, but they serve different flows.

POST /questions/generateGET /questions/draw
Returnsup to 20 questions in one batchone unseen question at a time
Use forpreparing a lobby, exploring a categorythe practice screen, endless mode
Generates from Anthropic?yes, on cache missyes, on cache miss
Filters out already-answeredyesyes

POST /v1/games/stumper/questions/generate

Generate (or fetch from cache) up to 20 trivia questions. Questions the caller has already answered are filtered out automatically.

  • Auth: Authenticated
  • Rate limit: 30 / 60s

Request

JSON
{
   "category": "world-history",
   "difficulty": "hard",
   "count": 10,
   "prompt": "focus on napoleonic wars",
   "model": "claude-sonnet-4-20250514"
}
FieldTypeConstraints
categorystringrequired. Category name or random.
difficultystringdefault auto. easy|medium|hard|elementary|middle_school|high_school|undergrad|masters|phd|postdoc|auto.
countinteger1–20, default 10.
promptstring|nullmax 500 chars, [\p{L}\p{N}\s.,!?'"\-:;()/].
modelstring|nulloptional override: claude-haiku-4-5-20251001, claude-sonnet-4-20250514, claude-opus-4-20250514.

Response200 OK

JSON
{
   "questions": [
      {
         "id": 18273,
         "type": "multiple_choice",
         "category": "world-history",
         "difficulty": "hard",
         "question": "Which battle marked the end of the Napoleonic Wars?",
         "payload": {
            "choices": [ "Austerlitz", "Waterloo", "Leipzig", "Borodino" ],
            "estimated_rate": 72
         },
         "is_cached": false
      }
   ]
}

POST /v1/games/stumper/questions/{id}/answer

Submit an answer to a single practice question. Records the answer, updates stats, awards XP, and returns the reveal.

  • Auth: Authenticated

Path params: id — question ID.

Request

JSON
{
   "selected_index": 1,
   "timer_duration_ms": 30000,
   "time_taken_ms": 8420
}
FieldTypeConstraints
selected_indexintegerrequired. 03, or -1 for timeout.
timer_duration_msinteger|null0–300000.
time_taken_msinteger|null0–300000.

Response200 OK

JSON
{
   "is_correct": true,
   "answer": 1,
   "explanation": "Waterloo (1815) was Napoleon's final defeat…",
   "correct_rate": 72,
   "num_served": 1204,
   "xp_gained": 18,
   "session": {
      "id": 8172,
      "player_id": 42,
      "total_points": 24100,
      "total_correct": 118,
      "total_answered": 142,
      "current_streak": 7,
      "best_streak": 19,
      "current_multiplier": 2,
      "created_at": "2026-04-18T11:20:00+00:00",
      "updated_at": "2026-04-18T12:00:00+00:00",
      "ended_at": null
   },
   "was_previously_wrong": false
}

Errors

CodeMeaning
404Question not found.

GET /v1/games/stumper/questions/draw

Draw a single practice question. Picks an unseen question matching the filters, or returns null if none available.

  • Auth: Authenticated

Query params

FieldNotes
categoriescomma-separated category names.
categorysingle category name.
difficultydefault auto.
promptnatural-language filter.
retrytrue to draw a previously-wrong question.
blendtrue to blend across categories.

Response200 OK

JSON
{
   "question": {
      "id": 18273,
      "type": "multiple_choice",
      "category": "world-history",
      "difficulty": "hard",
      "question": "…",
      "payload": { "choices": [ "…" ], "estimated_rate": 72 },
      "is_cached": true,
      "is_retry": false
   }
}

question is null when no unseen question is available.

GET /v1/games/stumper/questions

List cached trivia questions. Offline-friendly paginated catalog for admin tooling and offline play.

  • Auth: Public
  • Cache: 5 minutes

Query params

FieldNotes
categoryfilter by category.
difficultyfilter by difficulty.
limitdefault 10.
offsetdefault 0.

Response200 OK

JSON
{
   "questions": [
      {
         "id": 18273,
         "type": "multiple_choice",
         "category": "world-history",
         "difficulty": "hard",
         "question": "…",
         "answers": [ "Austerlitz", "Waterloo", "Leipzig", "Borodino" ],
         "correct_index": 1,
         "explanation": "…"
      }
   ]
}

This endpoint reveals the correct index because it's intended for admin tooling, not gameplay.


Favorites

GET /v1/games/stumper/favorites

List the caller's favorited questions with full formatted question data.

  • Auth: ROLE_REGISTERED

Response200 OK

JSON
{
   "favorites": [
      {
         "id": 314,
         "question": {
            "id": 18273,
            "type": "multiple_choice",
            "category": "world-history",
            "difficulty": "hard",
            "question": "…",
            "answers": [ "…" ],
            "correct_index": 1,
            "explanation": "…"
         },
         "created_at": "2026-04-15T09:00:00+00:00"
      }
   ]
}

POST /v1/games/stumper/favorites

Favorite a question.

  • Auth: ROLE_REGISTERED

Request

JSON
{
   "question_id": 18273
}

Response201 Created

JSON
{
   "favorite": {
      "id": 314,
      "question_id": 18273,
      "created_at": "2026-04-15T09:00:00+00:00"
   }
}

Errors

CodeMeaning
404Question not found.
409Already favorited.

DELETE /v1/games/stumper/favorites/{questionId}

Unfavorite a question.

  • Auth: ROLE_REGISTERED

Response204 No Content.

Errors

CodeMeaning
404Favorite not found.

Player state

GET /v1/games/stumper/state

Full player state in a single request. Hydrates the UI on page load with preferences, overall stats, per-category stats, seen question IDs, and any active play session.

  • Auth: Authenticated

Response200 OK

JSON
{
   "preferences": {
      "sound": true,
      "autoplay": false,
      "theme": "dark"
   },
   "stats": {
      "total_score": 132400,
      "total_answers": 1402,
      "total_correct": 1018,
      "best_streak": 47,
      "total_xp": 28500
   },
   "category_stats": {
      "world-history": {
         "xp": 4200,
         "level": 8,
         "xp_in_level": 200,
         "xp_for_next_level": 600,
         "percentage": 33.3,
         "total_answers": 221,
         "total_correct": 178,
         "best_streak": 19
      }
   },
   "seen_question_ids": [ 18273, 18274, 18275 ],
   "play_session": {
      "id": 8172,
      "player_id": 42,
      "total_points": 24100,
      "total_correct": 118,
      "total_answered": 142,
      "current_streak": 7,
      "best_streak": 19,
      "current_multiplier": 2,
      "created_at": "2026-04-18T11:20:00+00:00",
      "updated_at": "2026-04-18T12:00:00+00:00",
      "ended_at": null
   }
}

POST /v1/games/stumper/session/end

Explicitly end the active play session. Called when the player resets or starts a new session.

  • Auth: Authenticated

Response200 OK

JSON
{ "ok": true }

GET /v1/games/stumper/preferences

Get the caller's Stumper preferences. Free-form key/value map; the server does not enforce a schema.

  • Auth: Authenticated

Response200 OK

JSON
{
   "preferences": { "sound": true, "autoplay": false, "theme": "dark" }
}

PATCH /v1/games/stumper/preferences

Merge new keys into the caller's preferences. Existing keys not in the body are preserved.

  • Auth: Authenticated

Request

JSON
{ "theme": "retro", "autoplay": true }

Response200 OK

JSON
{
   "preferences": { "sound": true, "autoplay": true, "theme": "retro" }
}

Runs

Solo run modes: endless, daily_shuffle, gauntlet, and challenge-completion.

POST /v1/games/stumper/runs

Start a new run.

  • Auth: Authenticated

Request

JSON
{
   "mode": "endless",
   "challenge_id": null,
   "category": "world-history"
}
FieldTypeConstraints
modestringrequired. endless | daily_shuffle | gauntlet.
challenge_idinteger|nullrequired for challenge-backed runs.
categorystring|nulloptional category or random.

Response200 OK

JSON
{
   "id": 7201,
   "mode": "endless",
   "challenge_id": null,
   "score": 0,
   "streak": 0,
   "best_streak": 0,
   "num_correct": 0,
   "current_index": 0,
   "total_time_ms": 0,
   "category": "world-history",
   "is_finished": false,
   "created_at": "2026-04-18T12:00:00+00:00",
   "finished_at": null,
   "question": { "…": "next question with correct_index revealed server-side only at reveal time" }
}

Errors

CodeMeaning
409Challenge already completed.

GET /v1/games/stumper/runs/{id}

Get the current state of a run.

  • Auth: Authenticated

Response200 OK

JSON
{
   "run": {
      "id": 7201,
      "mode": "endless",
      "challenge_id": null,
      "score": 420,
      "streak": 3,
      "best_streak": 9,
      "num_correct": 12,
      "current_index": 12,
      "total_time_ms": 92000,
      "category": "world-history",
      "is_finished": false,
      "created_at": "2026-04-18T12:00:00+00:00",
      "finished_at": null
   }
}

Errors

CodeMeaning
404Run not found.

POST /v1/games/stumper/runs/{id}/answer

Submit an answer to the current question in a run. Returns the reveal plus the next question, or null if the run finished.

  • Auth: Authenticated

Request

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

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

Response200 OK

JSON
{
   "is_correct": true,
   "answer": 1,
   "explanation": "…",
   "correct_rate": 72,
   "num_served": 1204,
   "xp_gained": 18,
   "points_earned": 140,
   "streak": 4,
   "best_streak": 9,
   "next_question": { "…": "next question or null" },
   "run_finished": false
}

Errors

CodeMeaning
404Run not found or already finished.

POST /v1/games/stumper/runs/{id}/advance

Advance a challenge run to the next question after answering. Separated from /answer so a refresh between answering and clicking "Next" restores the reveal screen instead of skipping to the next question. Endless runs advance automatically inside /answer, so calling this on an endless run returns 409.

  • Auth: Authenticated

Path params: id — run ID.

Request — No body.

Response200 OK

JSON
{
   "run": {
      "id": 7201,
      "mode": "gauntlet",
      "challenge_id": 813,
      "score": 560,
      "streak": 4,
      "best_streak": 9,
      "num_correct": 13,
      "current_index": 13,
      "total_time_ms": 100200,
      "category": null,
      "is_finished": false,
      "created_at": "2026-04-18T12:00:00+00:00",
      "finished_at": null
   },
   "question": { "…": "next question in safe form, or null if not found" }
}

Errors

CodeMeaning
404Run or challenge not found.
409Run already finished, endless mode (auto-advances), current question not answered yet, or no more questions.

Challenges

GET /v1/games/stumper/challenges/today

Today's challenges with completion status for the current player. Accepts a timezone so "today" resolves in the player's local day.

  • Auth: Authenticated

Query params

FieldDefault
tzUTC. IANA identifier.

Response200 OK

JSON
{
   "daily_shuffle": {
      "id": 812,
      "completed": false,
      "in_progress": true,
      "run_id": 7201,
      "score": null,
      "rank": null,
      "category": "world-history",
      "categories": [ "world-history" ],
      "difficulties": [ "easy", "medium", "hard" ]
   },
   "gauntlet": {
      "id": 813,
      "completed": true,
      "in_progress": false,
      "run_id": 7150,
      "score": 2400,
      "rank": 42,
      "category": null,
      "categories": [ "world-history", "geography", "science" ],
      "difficulties": [ "easy", "medium", "hard" ]
   }
}

category is set only when every question in the challenge shares one category (always true for daily_shuffle, rare for gauntlet). rank and score are null until the player finishes the run.


Leaderboards

Three leaderboards — pick the one whose scope matches the audience you're showing it to.

EndpointScopeBest for
/leaderboardAll players, all-timeSite-wide ranking pages, profile cards
/leaderboard/challenges/{id}A single daily / gauntlet challenge"Today's challenge" leaderboard, post-run results
/leaderboard/teamsAll teams, all-timeTeam leaderboards, league standings

GET /v1/games/stumper/leaderboard

Global leaderboard. Sortable by score, accuracy, streak, or xp. Paginated, optional name search.

  • Auth: Optional (appends caller's rank if authenticated)

Query params

FieldNotes
tabscore | accuracy | streak | xp. Default score.
limitdefault 20, max 100.
pagedefault 1.
searchmin 2 chars, ILIKE on player name.

Response200 OK

JSON
{
   "entries": [
      {
         "rank": 1,
         "rank_prev": 2,
         "player_id": 42,
         "name": "anders",
         "value": 132400,
         "total_score": 132400,
         "total_xp": 28500,
         "total_answers": 1402,
         "accuracy": 72,
         "best_streak": 47,
         "level": 31,
         "is_player": true,
         "is_guest": false,
         "avatar_v": 1713456000
      }
   ],
   "total": 18402,
   "page": 1,
   "has_more": true,
   "player_rank": { "rank": 1, "value": 132400 }
}

value reflects the selected tab. player_rank is null for unauthenticated callers.

GET /v1/games/stumper/leaderboard/challenges/{challengeId}

Leaderboard for a specific daily challenge. Returns two sections: the top entries and a context window around the caller's rank.

  • Auth: Optional

Query params

FieldNotes
topdefault 5, max 100.
radiusdefault 4, max 10. Context window radius around caller.
pagedefault 1.
searchmin 2 chars.

Response200 OK

JSON
{
   "top": [
      {
         "rank": 1,
         "player_id": 42,
         "name": "anders",
         "score": 2400,
         "num_correct": 18,
         "best_streak": 12,
         "total_time_ms": 48000,
         "is_player": true
      }
   ],
   "context": [],
   "player_rank": 1,
   "total": 872,
   "page": 1,
   "has_more": true
}

GET /v1/games/stumper/leaderboard/teams

Team leaderboard. Same pagination mechanics as the global leaderboard.

  • Auth: Optional

Query params

FieldNotes
metricscore | accuracy | streak. Default score.
limitdefault 20, max 100.
pagedefault 1.

Response200 OK

Shape from TeamService::leaderboard():

JSON
{
   "entries": [ { "rank": 1, "team_id": 7, "name": "Team Trivia", "value": 48200 } ],
   "total": 42,
   "page": 1,
   "has_more": true
}

Lobbies

Multiplayer lobby endpoints — create, join, configure, play, leave — moved to a dedicated page.

Stumper Lobbies