openapi: 3.1.0 info: title: Jackbox Game Picker API description: API for managing Jackbox Party Pack games, sessions, voting, and integrations. version: "1.0" servers: - url: http://localhost:5000 description: Local development (backend direct) - url: http://localhost:3000/api description: Docker Compose (via Vite/Nginx proxy) tags: - name: Health description: Server health check - name: Auth description: Authentication and token verification - name: Games description: Game management, filtering, and pack metadata - name: Sessions description: Session lifecycle and game tracking - name: Picker description: Weighted random game selection - name: Stats description: Aggregate statistics - name: Votes description: Real-time popularity voting - name: Webhooks description: Webhook management for external integrations components: responses: Unauthorized: description: No access token provided content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Access token required" } Forbidden: description: Invalid or expired token content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Invalid or expired token" } securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: > JWT token obtained from POST /api/auth/login. Pass as Authorization Bearer . Tokens expire after 24 hours. schemas: Error: type: object required: - error properties: error: type: string Game: type: object properties: id: type: integer pack_name: type: string title: type: string min_players: type: integer max_players: type: integer length_minutes: type: - integer - "null" has_audience: type: integer enum: [0, 1] family_friendly: type: integer enum: [0, 1] game_type: type: - string - "null" secondary_type: type: - string - "null" play_count: type: integer popularity_score: type: integer upvotes: type: integer downvotes: type: integer enabled: type: integer enum: [0, 1] favor_bias: type: integer enum: [-1, 0, 1] created_at: type: string format: date-time Session: type: object properties: id: type: integer created_at: type: string format: date-time closed_at: type: - string - "null" format: date-time is_active: type: integer enum: [0, 1] notes: type: - string - "null" games_played: type: integer SessionGame: type: object properties: id: type: integer session_id: type: integer game_id: type: integer played_at: type: string format: date-time manually_added: type: integer enum: [0, 1] status: type: string enum: [playing, played, skipped] room_code: type: - string - "null" player_count: type: - integer - "null" player_count_check_status: type: - string - "null" pack_name: type: string title: type: string game_type: type: - string - "null" min_players: type: integer max_players: type: integer popularity_score: type: integer upvotes: type: integer downvotes: type: integer Pack: type: object properties: id: type: integer name: type: string favor_bias: type: integer enum: [-1, 0, 1] created_at: type: string format: date-time PackMeta: type: object properties: name: type: string total_count: type: integer enabled_count: type: integer total_plays: type: integer Webhook: type: object properties: id: type: integer name: type: string url: type: string format: uri events: type: array items: type: string enabled: type: boolean created_at: type: string format: date-time WebhookLog: type: object properties: id: type: integer webhook_id: type: integer event_type: type: string payload: type: object response_status: type: - integer - "null" error_message: type: - string - "null" created_at: type: string format: date-time paths: /health: get: operationId: getHealth summary: Health check tags: [Health] responses: "200": description: API is running content: application/json: schema: type: object required: [status, message] properties: status: { type: string, example: ok } message: { type: string, example: "Jackbox Game Picker API is running" } /api/auth/login: post: operationId: loginWithAdminKey summary: Authenticate with admin key tags: [Auth] requestBody: required: true content: application/json: schema: type: object required: [key] properties: key: { type: string } responses: "200": description: Authentication successful content: application/json: schema: type: object required: [token, message, expiresIn] properties: token: { type: string } message: { type: string, example: "Authentication successful" } expiresIn: { type: string, example: "24h" } "400": description: Admin key is required content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Admin key is required" } "401": description: Invalid admin key content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Invalid admin key" } /api/auth/verify: post: operationId: verifyToken summary: Verify JWT token tags: [Auth] security: [{ bearerAuth: [] }] responses: "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "200": description: Token is valid content: application/json: schema: type: object required: [valid, user] properties: valid: { type: boolean, example: true } user: type: object properties: role: { type: string, example: admin } timestamp: { type: number } /api/games: get: operationId: listGames summary: List games with optional filters tags: [Games] parameters: - name: enabled in: query schema: { type: string, enum: ["true", "false"] } - name: playerCount in: query schema: { type: integer } - name: drawing in: query schema: { type: string, enum: ["only", "exclude"] } - name: length in: query schema: { type: string, enum: ["short", "medium", "long"] } - name: familyFriendly in: query schema: { type: string, enum: ["true", "false"] } - name: pack in: query schema: { type: string } responses: "200": description: Array of games ordered by pack_name, title content: application/json: schema: type: array items: { $ref: "#/components/schemas/Game" } post: operationId: createGame summary: Create a new game tags: [Games] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [pack_name, title, min_players, max_players] properties: pack_name: { type: string } title: { type: string } min_players: { type: integer } max_players: { type: integer } length_minutes: { type: integer } has_audience: { type: boolean } family_friendly: { type: boolean } game_type: { type: string } secondary_type: { type: string } responses: "201": description: Game created content: application/json: schema: { $ref: "#/components/schemas/Game" } "400": description: Missing required fields content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Missing required fields" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/games/packs: get: operationId: listPacks summary: List all packs tags: [Games] responses: "200": description: Array of packs content: application/json: schema: type: array items: { $ref: "#/components/schemas/Pack" } /api/games/meta/packs: get: operationId: listPackMeta summary: List pack metadata tags: [Games] responses: "200": description: Array of pack metadata content: application/json: schema: type: array items: { $ref: "#/components/schemas/PackMeta" } /api/games/export/csv: get: operationId: exportGamesCsv summary: Export games as CSV tags: [Games] security: [{ bearerAuth: [] }] responses: "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "200": description: CSV file download headers: Content-Disposition: schema: type: string example: 'attachment; filename="jackbox-games.csv"' content: text/csv: schema: type: string description: CSV with columns Pack Name, Title, Min Players, Max Players, Length (minutes), Audience, Family Friendly, Game Type, Secondary Type /api/games/import/csv: post: operationId: importGamesCsv summary: Import games from CSV tags: [Games] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [csvData] properties: csvData: { type: string } mode: { type: string, enum: ["append", "replace"], description: Optional; defaults to append if absent } responses: "200": description: Import successful content: application/json: schema: type: object required: [message, count, mode] properties: message: { type: string } count: { type: integer } mode: { type: string } "400": description: CSV data required content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "CSV data required" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/games/packs/{name}/favor: patch: operationId: updatePackFavor summary: Update pack favor bias tags: [Games] security: [{ bearerAuth: [] }] parameters: - name: name in: path required: true schema: { type: string } requestBody: required: true content: application/json: schema: type: object required: [favor_bias] properties: favor_bias: { type: integer, enum: [-1, 0, 1] } responses: "200": description: Pack favor bias updated content: application/json: schema: type: object required: [message, favor_bias] properties: message: { type: string, example: "Pack favor bias updated successfully" } favor_bias: { type: integer } "400": description: Invalid favor_bias content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "favor_bias must be 1 (favor), -1 (disfavor), or 0 (neutral)" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/games/packs/{name}/toggle: patch: operationId: togglePack summary: Enable or disable a pack tags: [Games] security: [{ bearerAuth: [] }] parameters: - name: name in: path required: true schema: { type: string } requestBody: required: true content: application/json: schema: type: object required: [enabled] properties: enabled: { type: boolean } responses: "200": description: Pack enabled/disabled content: application/json: schema: type: object required: [message, gamesAffected] properties: message: { type: string } gamesAffected: { type: integer } "400": description: enabled status required content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "enabled status required" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/games/{id}: get: operationId: getGame summary: Get a game by ID tags: [Games] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Game found content: application/json: schema: { $ref: "#/components/schemas/Game" } "404": description: Game not found content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Game not found" } put: operationId: updateGame summary: Update a game tags: [Games] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } requestBody: content: application/json: schema: type: object properties: pack_name: { type: string } title: { type: string } min_players: { type: integer } max_players: { type: integer } length_minutes: { type: integer } has_audience: { type: boolean } family_friendly: { type: boolean } game_type: { type: string } secondary_type: { type: string } enabled: { type: boolean } responses: "200": description: Game updated content: application/json: schema: { $ref: "#/components/schemas/Game" } "404": description: Game not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } delete: operationId: deleteGame summary: Delete a game tags: [Games] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Game deleted content: application/json: schema: type: object required: [message] properties: message: { type: string, example: "Game deleted successfully" } "404": description: Game not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/games/{id}/toggle: patch: operationId: toggleGame summary: Toggle game enabled status tags: [Games] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Game updated content: application/json: schema: { $ref: "#/components/schemas/Game" } "404": description: Game not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/games/{id}/favor: patch: operationId: updateGameFavor summary: Update game favor bias tags: [Games] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } requestBody: required: true content: application/json: schema: type: object required: [favor_bias] properties: favor_bias: { type: integer, enum: [-1, 0, 1] } responses: "200": description: Favor bias updated content: application/json: schema: type: object required: [message, favor_bias] properties: message: { type: string, example: "Favor bias updated successfully" } favor_bias: { type: integer } "400": description: Invalid favor_bias content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: Game not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions: get: operationId: listSessions summary: List all sessions tags: [Sessions] responses: "200": description: Array of sessions with games_played content: application/json: schema: type: array items: { $ref: "#/components/schemas/Session" } post: operationId: createSession summary: Create a new session tags: [Sessions] security: [{ bearerAuth: [] }] requestBody: content: application/json: schema: type: object properties: notes: { type: string } responses: "201": description: Session created content: application/json: schema: { $ref: "#/components/schemas/Session" } "400": description: Active session already exists content: application/json: schema: type: object required: [error, activeSessionId] properties: error: { type: string } activeSessionId: { type: integer } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/active: get: operationId: getActiveSession summary: Get the active session tags: [Sessions] responses: "200": description: Active session (raw object) or null with message when no session content: application/json: schema: oneOf: - $ref: "#/components/schemas/Session" - type: object required: [session, message] properties: session: { type: "null" } message: { type: string, example: "No active session" } /api/sessions/{id}: get: operationId: getSession summary: Get a session by ID tags: [Sessions] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Session found content: application/json: schema: { $ref: "#/components/schemas/Session" } "404": description: Session not found content: application/json: schema: { $ref: "#/components/schemas/Error" } delete: operationId: deleteSession summary: Delete a session tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Session deleted content: application/json: schema: type: object required: [message, sessionId] properties: message: { type: string, example: "Session deleted successfully" } sessionId: { type: integer } "404": description: Session not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "400": description: Cannot delete active session content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Cannot delete an active session. Please close it first." } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{id}/close: post: operationId: closeSession summary: Close a session tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } requestBody: content: application/json: schema: type: object properties: notes: { type: string } responses: "200": description: Session closed with games_played content: application/json: schema: { $ref: "#/components/schemas/Session" } "404": description: Session not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "400": description: Session already closed content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Session is already closed" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{id}/games: get: operationId: listSessionGames summary: List games in a session tags: [Sessions] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Array of session games content: application/json: schema: type: array items: { $ref: "#/components/schemas/SessionGame" } "404": description: Session not found content: application/json: schema: { $ref: "#/components/schemas/Error" } post: operationId: addGameToSession summary: Add a game to a session tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } requestBody: required: true content: application/json: schema: type: object required: [game_id] properties: game_id: { type: integer } manually_added: { type: boolean } room_code: { type: string } responses: "201": description: Game added to session content: application/json: schema: { $ref: "#/components/schemas/SessionGame" } "400": description: Closed session or missing game_id content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: Session or game not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{id}/votes: get: operationId: getSessionVotes summary: Get per-game vote breakdown for a session tags: [Sessions] parameters: - name: id in: path required: true schema: { type: integer } description: Session ID responses: "200": description: Per-game vote aggregates for the session content: application/json: schema: type: object required: [session_id, votes] properties: session_id: type: integer votes: type: array items: type: object properties: game_id: { type: integer } title: { type: string } pack_name: { type: string } upvotes: { type: integer } downvotes: { type: integer } net_score: { type: integer } total_votes: { type: integer } "404": description: Session not found content: application/json: schema: { $ref: "#/components/schemas/Error" } /api/sessions/{id}/chat-import: post: operationId: importSessionChat summary: Import chat log for vote processing tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } requestBody: required: true content: application/json: schema: type: object required: [chatData] properties: chatData: type: array items: type: object required: [username, message, timestamp] properties: username: { type: string } message: { type: string } timestamp: { type: string } responses: "200": description: Chat imported and processed content: application/json: schema: type: object required: [message, messagesImported, duplicatesSkipped, votesProcessed, votesByGame, debug] properties: message: { type: string } messagesImported: { type: integer } duplicatesSkipped: { type: integer } votesProcessed: { type: integer } votesByGame: { type: object } debug: { type: object } "400": description: | Two distinct cases: - "chatData must be an array" — when chatData is missing or not an array - "No games played in this session to match votes against" — when session has no games content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: Session not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{id}/export: get: operationId: exportSession summary: Export session tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } - name: format in: query schema: { type: string, enum: ["json", "txt"], default: "txt" } responses: "200": description: File download content: application/json: schema: { type: object } text/plain: schema: { type: string } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{sessionId}/games/{sessionGameId}/status: patch: operationId: updateSessionGameStatus summary: Update session game status tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: sessionId in: path required: true schema: { type: integer } - name: sessionGameId in: path required: true description: Session-game row ID (session_games.id), not games.id schema: { type: integer } requestBody: required: true content: application/json: schema: type: object required: [status] properties: status: { type: string, enum: [playing, played, skipped] } responses: "200": description: Status updated content: application/json: schema: type: object required: [message, status] properties: message: { type: string, example: "Status updated successfully" } status: { type: string } "400": description: Invalid status content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: Not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{sessionId}/games/{sessionGameId}: delete: operationId: removeGameFromSession summary: Remove game from session tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: sessionId in: path required: true schema: { type: integer } - name: sessionGameId in: path required: true description: Session-game row ID (session_games.id), not games.id schema: { type: integer } responses: "200": description: Game removed content: application/json: schema: type: object required: [message] properties: message: { type: string, example: "Game removed from session successfully" } "404": description: Not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{sessionId}/games/{sessionGameId}/room-code: patch: operationId: updateSessionGameRoomCode summary: Update room code for session game tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: sessionId in: path required: true schema: { type: integer } - name: sessionGameId in: path required: true description: Session-game row ID (session_games.id), not games.id schema: { type: integer } requestBody: required: true content: application/json: schema: type: object required: [room_code] properties: room_code: { type: string, pattern: "^[A-Z0-9]{4}$" } responses: "200": description: Room code updated content: application/json: schema: { $ref: "#/components/schemas/SessionGame" } "400": description: Missing or invalid room code format content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: Not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{sessionId}/games/{sessionGameId}/start-player-check: post: operationId: startPlayerCheck summary: Start room monitor for player count tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: sessionId in: path required: true schema: { type: integer } - name: sessionGameId in: path required: true description: Session-game row ID (session_games.id), not games.id schema: { type: integer } responses: "200": description: Room monitor started content: application/json: schema: type: object required: [message, status] properties: message: { type: string, example: "Room monitor started" } status: { type: string, example: monitoring } "400": description: Game does not have a room code content: application/json: schema: type: object required: [error] properties: error: { type: string, example: "Game does not have a room code" } "404": description: Not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{sessionId}/games/{sessionGameId}/stop-player-check: post: operationId: stopPlayerCheck summary: Stop room monitor tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: sessionId in: path required: true schema: { type: integer } - name: sessionGameId in: path required: true description: Session-game row ID (session_games.id), not games.id schema: { type: integer } responses: "200": description: Monitor stopped content: application/json: schema: type: object required: [message, status] properties: message: { type: string, example: "Room monitor and player count check stopped" } status: { type: string, example: stopped } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/sessions/{sessionId}/games/{sessionGameId}/player-count: patch: operationId: updateSessionGamePlayerCount summary: Update player count for session game tags: [Sessions] security: [{ bearerAuth: [] }] parameters: - name: sessionId in: path required: true schema: { type: integer } - name: sessionGameId in: path required: true description: Session-game row ID (session_games.id), not games.id schema: { type: integer } requestBody: required: true content: application/json: schema: type: object required: [player_count] properties: player_count: { type: integer, minimum: 0 } responses: "200": description: Player count updated content: application/json: schema: type: object required: [message, player_count] properties: message: { type: string, example: "Player count updated successfully" } player_count: { type: integer } "400": description: Missing or invalid player count content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: Not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/pick: post: operationId: pickRandomGame summary: Pick a random game with optional filters tags: [Picker] requestBody: content: application/json: schema: type: object properties: playerCount: { type: integer } drawing: { type: string } length: { type: string } familyFriendly: { type: boolean } sessionId: { type: integer } excludePlayed: { type: boolean } responses: "200": description: Random game picked content: application/json: schema: type: object required: [game, poolSize, totalEnabled] properties: game: { $ref: "#/components/schemas/Game" } poolSize: { type: integer } totalEnabled: { type: integer } "404": description: No matching games content: application/json: schema: type: object required: [error] properties: error: { type: string } suggestion: { type: string } recentlyPlayed: { type: array, items: { type: integer } } /api/stats: get: operationId: getStats summary: Get aggregate statistics tags: [Stats] responses: "200": description: Stats object content: application/json: schema: type: object required: [games, gamesEnabled, packs, sessions, activeSessions, totalGamesPlayed, mostPlayedGames, topRatedGames] properties: games: { type: object, properties: { count: { type: integer } } } gamesEnabled: { type: object, properties: { count: { type: integer } } } packs: { type: object, properties: { count: { type: integer } } } sessions: { type: object, properties: { count: { type: integer } } } activeSessions: { type: object, properties: { count: { type: integer } } } totalGamesPlayed: { type: object, properties: { count: { type: integer } } } mostPlayedGames: type: array items: type: object properties: id: { type: integer } title: { type: string } pack_name: { type: string } play_count: { type: integer } popularity_score: { type: integer } upvotes: { type: integer } downvotes: { type: integer } topRatedGames: type: array items: type: object properties: id: { type: integer } title: { type: string } pack_name: { type: string } play_count: { type: integer } popularity_score: { type: integer } upvotes: { type: integer } downvotes: { type: integer } /api/votes: get: operationId: listVotes summary: Paginated vote history with filtering tags: [Votes] parameters: - name: session_id in: query schema: { type: integer } description: Filter by session - name: game_id in: query schema: { type: integer } description: Filter by game - name: username in: query schema: { type: string } description: Filter by voter - name: vote_type in: query schema: { type: string, enum: [up, down] } description: Filter by direction - name: page in: query schema: { type: integer, default: 1 } description: Page number - name: limit in: query schema: { type: integer, default: 50, maximum: 100 } description: Results per page (max 100) responses: "200": description: Paginated vote records content: application/json: schema: type: object required: [votes, pagination] properties: votes: type: array items: type: object properties: id: { type: integer } session_id: { type: integer } game_id: { type: integer } game_title: { type: string } pack_name: { type: string } username: { type: string } vote_type: { type: string, enum: [up, down] } timestamp: { type: string, format: date-time } created_at: { type: string, format: date-time } pagination: type: object properties: page: { type: integer } limit: { type: integer } total: { type: integer } total_pages: { type: integer } "400": description: Invalid filter parameter content: application/json: schema: { $ref: "#/components/schemas/Error" } /api/votes/live: post: operationId: recordLiveVote summary: Record a live vote (up/down) tags: [Votes] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [username, vote, timestamp] properties: username: { type: string } vote: { type: string, enum: [up, down] } timestamp: { type: string, format: date-time, description: ISO 8601 } responses: "200": description: Vote recorded content: application/json: schema: type: object required: [success, message, session, game, vote] properties: success: { type: boolean, example: true } message: { type: string, example: "Vote recorded successfully" } session: type: object properties: id: { type: integer } games_played: { type: integer } game: type: object properties: id: { type: integer } title: { type: string } upvotes: { type: integer } downvotes: { type: integer } popularity_score: { type: integer } vote: type: object properties: username: { type: string } type: { type: string } timestamp: { type: string } "400": description: Missing fields, invalid vote, or invalid timestamp content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: No active session, no games, or no matching game (vote timestamp doesn't match) content: application/json: schema: type: object required: [error] properties: error: { type: string } debug: type: object description: Present when vote timestamp doesn't match any game properties: voteTimestamp: { type: string } sessionGames: type: array items: type: object properties: title: { type: string } played_at: { type: string } "409": description: Duplicate vote within 1 second content: application/json: schema: type: object required: [error] properties: error: { type: string } message: { type: string } timeSinceLastVote: { type: number } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/webhooks: get: operationId: listWebhooks summary: List all webhooks tags: [Webhooks] security: [{ bearerAuth: [] }] responses: "200": description: Array of webhooks content: application/json: schema: type: array items: { $ref: "#/components/schemas/Webhook" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: operationId: createWebhook summary: Create a webhook tags: [Webhooks] security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [name, url, secret, events] properties: name: { type: string } url: { type: string, format: uri } secret: { type: string } events: type: array items: { type: string } responses: "201": description: Webhook created content: application/json: schema: allOf: - { $ref: "#/components/schemas/Webhook" } - type: object properties: message: { type: string, example: "Webhook created successfully" } "400": description: Invalid input content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/webhooks/{id}: get: operationId: getWebhook summary: Get a webhook by ID tags: [Webhooks] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Webhook found content: application/json: schema: { $ref: "#/components/schemas/Webhook" } "404": description: Webhook not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } patch: operationId: updateWebhook summary: Update a webhook tags: [Webhooks] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } requestBody: content: application/json: schema: type: object properties: name: { type: string } url: { type: string, format: uri } secret: { type: string } events: type: array items: { type: string } enabled: { type: boolean } responses: "200": description: Webhook updated content: application/json: schema: allOf: - { $ref: "#/components/schemas/Webhook" } - type: object properties: message: { type: string, example: "Webhook updated successfully" } "400": description: No fields, invalid URL, or events not array content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: Webhook not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } delete: operationId: deleteWebhook summary: Delete a webhook tags: [Webhooks] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Webhook deleted content: application/json: schema: type: object required: [message, webhookId] properties: message: { type: string, example: "Webhook deleted successfully" } webhookId: { type: integer } "404": description: Webhook not found content: application/json: schema: { $ref: "#/components/schemas/Error" } /api/webhooks/test/{id}: post: operationId: testWebhook summary: Send test webhook tags: [Webhooks] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } responses: "200": description: Test webhook sent content: application/json: schema: type: object required: [message, note] properties: message: { type: string, example: "Test webhook sent" } note: { type: string, example: "Check webhook_logs table for delivery status" } "404": description: Webhook not found content: application/json: schema: { $ref: "#/components/schemas/Error" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } /api/webhooks/{id}/logs: get: operationId: listWebhookLogs summary: List webhook delivery logs tags: [Webhooks] security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: { type: integer } - name: limit in: query schema: { type: integer, default: 50 } responses: "200": description: Array of webhook logs content: application/json: schema: type: array items: { $ref: "#/components/schemas/WebhookLog" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" }