# 03 — API Contract > **Purpose:** REST and WebSocket endpoint reference with exact Zod schemas. Concatenate with 00-project-overview.md and 99-current-task.md. > **Last updated:** 2026-05-15 > **Depends on:** 00-project-overview.md, 02-data-model.md --- ## REST Endpoints ### Health ``` GET /api/v1/health ``` **Response:** `{ "status": "ok" }` **Auth:** None (public) --- ### Game — Start Session ``` POST /api/v1/game/start ``` **Request body** (GameRequestSchema): ```typescript { source_language: SupportedLanguageCode, // "en" | "it" | "de" | "es" | "fr" target_language: SupportedLanguageCode, pos: SupportedPos, // "noun" | "verb" | "adjective" | "adverb" difficulty: DifficultyLevel, // "easy" | "intermediate" | "hard" rounds: GameRounds // "3" | "10" (string enum, converted to number in service) } ``` **Validation rules:** - `source_language` !== `target_language` - Both languages in `SUPPORTED_LANGUAGE_CODES` - `pos` in `SUPPORTED_POS` - `difficulty` in `DIFFICULTY_LEVELS` - `rounds` in `GAME_ROUNDS` **Response** (GameSessionSchema): ```typescript { sessionId: string, // UUID questions: GameQuestion[] } ``` **GameQuestionSchema:** ```typescript { questionId: string, // UUID prompt: string, // Word in source language gloss: string | null, // Definition (falls back to English if target lang gloss missing) options: AnswerOption[] // 4 items, shuffled } ``` **AnswerOptionSchema:** ```typescript { optionId: number, // 0–3 text: string // Translation in target language } ``` **Note:** The correct answer is NOT included in the response. The frontend only sees `optionId` and `text`. The server stores `questionId → correctOptionId` mapping in the GameSessionStore. **Auth:** Required (session middleware) --- ### Game — Submit Answer ``` POST /api/v1/game/answer ``` **Request body** (AnswerSubmissionSchema): ```typescript { sessionId: string, // UUID questionId: string, // UUID selectedOptionId: number // 0–3 } ``` **Response** (AnswerResultSchema): ```typescript { questionId: string, isCorrect: boolean, correctOptionId: number, // 0–3 selectedOptionId: number // 0–3 } ``` **Error cases:** - Session not found → 404 NotFoundError - Question not in session → 404 NotFoundError - Invalid optionId → 400 ValidationError **Auth:** Required --- ### Lobby — Create ``` POST /api/v1/lobbies ``` **Request body:** None (host's auth session determines host_id) **Response:** ```typescript { id: string, // UUID code: string, // Human-readable room code (e.g. "WOLF-42") host_id: string, status: "waiting", max_players: number, settings: object | null, created_at: string } ``` **Auth:** Required --- ### Lobby — Join ``` POST /api/v1/lobbies/:code/join ``` **Path param:** `code` — room code (e.g. "WOLF-42") **Response:** Same as create (the lobby object) **Error cases:** - Lobby not found → 404 - Lobby full → 400 - Already joined → 200 (idempotent) **Auth:** Required --- ### Auth ``` ALL /api/auth/* — Better Auth handlers (public) ``` Better Auth mounts its own router at `/api/auth/*`. Handles: - `/api/auth/signin/social` — initiate social login - `/api/auth/callback/:provider` — OAuth callback - `/api/auth/signout` — clear session - `/api/auth/session` — get current session **Auth:** Mixed (some public, some require valid session) --- ## WebSocket Protocol All WS messages are JSON objects with a `type` field. The `type` is a discriminated union — the router validates the payload against the schema for that type. ### Connection 1. Client opens WebSocket to `wss://api.lilastudy.com/ws` 2. Server validates Better Auth session from cookie on upgrade 3. Connection established ### Client → Server Messages #### `lobby:join` ```typescript { type: "lobby:join", payload: { code: string // Room code (e.g. "WOLF-42") } } ``` #### `lobby:leave` ```typescript { type: "lobby:leave", payload: { code: string } } ``` #### `lobby:start` ```typescript { type: "lobby:start", payload: { code: string } } ``` Only the host can send this. Triggers game start. #### `game:answer` ```typescript { type: "game:answer", payload: { code: string, questionId: string, optionId: number // 0–3 } } ``` Must be sent within the 15-second server timer. --- ### Server → Client Messages #### `lobby:state` ```typescript { type: "lobby:state", payload: { code: string, players: { id: string, display_name: string, is_host: boolean }[], status: "waiting" | "in_progress" | "finished", settings: object | null } } ``` Broadcast to all players in the lobby on any membership change. #### `game:question` ```typescript { type: "game:question", payload: { questionId: string, prompt: string, gloss: string | null, options: { optionId: number, text: string }[], timeLimit: number // seconds (15) } } ``` Broadcast when the game starts or a new round begins. #### `game:answer_result` ```typescript { type: "game:answer_result", payload: { questionId: string, results: { playerId: string, displayName: string, isCorrect: boolean, selectedOptionId: number, score: number }[] } } ``` Broadcast after all players answer or timer expires. #### `game:finished` ```typescript { type: "game:finished", payload: { finalScores: { playerId: string, displayName: string, score: number }[], winner: { playerId: string, displayName: string } | null // null for ties } } ``` Broadcast after all rounds complete. --- ## Zod Schema Locations All schemas live in `packages/shared/src/schemas/`: | Schema | File | Used by | | ---------------------- | ---------- | --------------------------------------- | | GameRequestSchema | `game.ts` | API controller, frontend GameSetup | | GameSessionSchema | `game.ts` | API service, frontend quiz flow | | GameQuestionSchema | `game.ts` | API service, frontend QuestionCard | | AnswerOptionSchema | `game.ts` | API service, frontend OptionButton | | AnswerSubmissionSchema | `game.ts` | API controller, frontend submit handler | | AnswerResultSchema | `game.ts` | API controller, frontend ScoreScreen | | LobbyCreateSchema | `lobby.ts` | API controller | | LobbyJoinSchema | `lobby.ts` | API controller | | LobbyStateSchema | `lobby.ts` | WS handler, frontend lobby UI | | WebSocketMessageSchema | `lobby.ts` | WS router (discriminated union) | **Rule:** Never duplicate these schemas. Import from `packages/shared` in both API and frontend. --- ## Error Responses All errors follow this shape: ```typescript { error: string, // Human-readable message statusCode: number // HTTP status } ``` **Common status codes:** - 400 — ValidationError (bad input, schema mismatch) - 401 — Unauthorized (no valid session) - 404 — NotFoundError (session, question, or lobby not found) - 500 — Unknown error (logged, generic message to client)