openapi: 3.0.3 info: title: Galaxy Game Lobby Service Public REST API version: v1 description: | This specification documents the public authenticated REST contract of `galaxy/lobby` served on `LOBBY_PUBLIC_HTTP_ADDR` (default `:8094`). This port is reached exclusively through `Edge Gateway`. Gateway verifies the authenticated session and injects the `X-User-ID` header before forwarding every request. `Lobby` derives the acting user identity from `X-User-ID` only and must never accept identity claims from request bodies. Scope: - game lifecycle management (create, update, get, list) - enrollment management (open, close, ready-to-start) - start lifecycle (start, pause, resume, cancel, retry-start) - application flow for public games - invite flow for private games - membership operations - user-facing lists (my games, my applications, my invitations) This specification intentionally does not describe: - the internal trusted REST contract (see `api/internal-openapi.yaml`) - Redis Stream event contracts (see `README.md`) - notification intent contracts (see `../notification/README.md`) Transport rules: - request bodies are strict JSON only; unknown fields are rejected - all authenticated routes require `X-User-ID` injected by `Edge Gateway` - error responses use `{ "error": { "code", "message" } }` - stable error codes are `invalid_request`, `conflict`, `subject_not_found`, `eligibility_denied`, `name_taken`, `race_name_pending_window_expired`, `race_name_registration_quota_exceeded`, `forbidden`, `internal_error`, and `service_unavailable` - `eligibility_denied`, `name_taken`, `race_name_pending_window_expired`, and `race_name_registration_quota_exceeded` are returned as `422` servers: - url: http://localhost:8094 description: Default local public listener for Game Lobby Service. tags: - name: Games description: Game record CRUD and lifecycle queries. - name: Enrollment description: Enrollment management commands. - name: Lifecycle description: Start, pause, resume, cancel, and retry-start commands. - name: Applications description: Application flow for public games. - name: Invites description: Invite flow for private games. - name: Memberships description: Membership roster operations. - name: MyLists description: Authenticated-user personal list queries. - name: RaceNames description: Race Name Directory user-facing operations. - name: Probes description: Health and readiness probes. paths: /healthz: get: tags: - Probes operationId: publicHealthz summary: Public listener health probe responses: "200": description: Service is alive. content: application/json: schema: $ref: "#/components/schemas/ProbeResponse" examples: ok: value: status: ok /readyz: get: tags: - Probes operationId: publicReadyz summary: Public listener readiness probe responses: "200": description: Service is ready to serve traffic. content: application/json: schema: $ref: "#/components/schemas/ProbeResponse" examples: ready: value: status: ready /api/v1/lobby/games: post: tags: - Games operationId: createGame summary: Create a new game record in draft status description: | Creates a new game record in `draft` status. Authorization: - `game_type=public`: requires system-admin role enforced upstream by `Admin Service`; public games created on the internal port only in normal operation - `game_type=private`: requires the acting user's eligibility snapshot from `User Service` to carry `can_create_private_game=true` parameters: - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateGameRequest" responses: "201": description: Game record created in draft status. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "422": $ref: "#/components/responses/DomainPreconditionError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" get: tags: - Games operationId: listGames summary: List public games with deterministic pagination description: | Returns a paginated list of public games with status in `enrollment_open`, `ready_to_start`, `running`, or `finished`. Games in `draft` or `cancelled` status are excluded from the public list. Authenticated users also see private games where they hold an active membership. Default order: `enrollment_open` and `ready_to_start` first, then `running`, then `finished` (most recent first within each group). parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/PageSize" - $ref: "#/components/parameters/PageToken" responses: "200": description: One deterministic page of game summaries. content: application/json: schema: $ref: "#/components/schemas/GameListResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}: get: tags: - Games operationId: getGame summary: Get one game record description: | Returns the full game record for the requested `game_id`. Visibility rules: - private `draft` games: visible only to the owner - private non-draft games: visible to the owner and users with an active membership or a non-expired invite - public `draft` games: visible only to system administrators - public non-draft games: visible to any authenticated user parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Full game record. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" patch: tags: - Games operationId: updateGame summary: Update mutable fields of a game record description: | Partially updates a game record. Only fields present in the request body are modified; absent fields retain their current values. Editable in `draft` status: all fields in the request schema. Editable in `enrollment_open` status: `description` only. All fields are immutable in all other statuses. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateGameRequest" responses: "200": description: Updated game record. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/open-enrollment: post: tags: - Enrollment operationId: openEnrollment summary: Transition a draft game to enrollment_open description: | Transitions the game from `draft` to `enrollment_open`. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated game record with status enrollment_open. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/ready-to-start: post: tags: - Enrollment operationId: manualReadyToStart summary: Manually close enrollment and transition to ready_to_start description: | Manually closes enrollment and transitions the game from `enrollment_open` to `ready_to_start`. Pre-condition: `approved_count >= min_players`. Side effects: all invites in `created` status are transitioned to `expired`; `lobby.invite.expired` notification intents are published for each expired invite. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated game record with status ready_to_start. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/start: post: tags: - Lifecycle operationId: startGame summary: Initiate the game start sequence description: | Transitions the game from `ready_to_start` to `starting` and publishes a start job to `Runtime Manager`. The final outcome (`running`, `paused`, or `start_failed`) is determined asynchronously by the `Runtime Manager` result consumer. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated game record with status starting. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/pause: post: tags: - Lifecycle operationId: pauseGame summary: Apply a platform-level pause to a running game description: | Transitions the game from `running` to `paused`. This is a platform-level pause distinct from `Game Master` runtime failure states. The engine container may remain alive. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated game record with status paused. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/resume: post: tags: - Lifecycle operationId: resumeGame summary: Resume a paused game description: | Transitions the game from `paused` to `running`. A synchronous `Game Master` liveness check is performed before the transition. If `Game Master` is unreachable, the game remains `paused` and `503 service_unavailable` is returned. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated game record with status running. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/cancel: post: tags: - Lifecycle operationId: cancelGame summary: Cancel a game that has not yet started running description: | Cancels the game. Allowed source statuses: `draft`, `enrollment_open`, `ready_to_start`, `start_failed`. Not allowed from `starting`, `running`, or `paused`. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated game record with status cancelled. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/retry-start: post: tags: - Lifecycle operationId: retryStart summary: Retry a failed start attempt description: | Transitions the game from `start_failed` back to `ready_to_start`, enabling a new start attempt. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated game record with status ready_to_start. content: application/json: schema: $ref: "#/components/schemas/GameRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/applications: post: tags: - Applications operationId: submitApplication summary: Submit a join application for a public game description: | Creates a new application in `submitted` status for a public game. Pre-conditions checked synchronously: - game status is `enrollment_open` and game type is `public` - acting user has no existing non-rejected application to the same game - `User Service` eligibility confirms `can_join_game=true` - roster capacity allows additional applicants - Race Name Directory confirms `race_name` is available for the acting user On success, `lobby.application.submitted` notification intent is published to the configured admin email list. Authorization: any authenticated user. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SubmitApplicationRequest" responses: "201": description: Application created in submitted status. content: application/json: schema: $ref: "#/components/schemas/ApplicationRecord" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "422": $ref: "#/components/responses/DomainPreconditionError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/applications/{application_id}/approve: post: tags: - Applications operationId: approveApplication summary: Approve a submitted application description: | Approves a submitted application, reserves the race name, and creates an active membership for the applicant. Pre-conditions: game is `enrollment_open`; application is `submitted`; roster capacity allows additional approved participants. On success, `lobby.membership.approved` notification intent is published to the applicant. Authorization: system administrator. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/ApplicationIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Active membership created for the approved applicant. content: application/json: schema: $ref: "#/components/schemas/MembershipRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/applications/{application_id}/reject: post: tags: - Applications operationId: rejectApplication summary: Reject a submitted application description: | Rejects a submitted application and releases any pending race name reservation held for the applicant. On success, `lobby.membership.rejected` notification intent is published to the applicant. Authorization: system administrator. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/ApplicationIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Application record with status rejected. content: application/json: schema: $ref: "#/components/schemas/ApplicationRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/invites: post: tags: - Invites operationId: createInvite summary: Create an invite for a private game description: | Creates a new invite in `created` status for the specified invitee. Pre-conditions: game is `enrollment_open` and `private`; the invitee has no active invite or active membership in the game; roster capacity allows additional participants. `expires_at` is set to `enrollment_ends_at` of the game. On success, `lobby.invite.created` notification intent is published to the invitee. Authorization: private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateInviteRequest" responses: "201": description: Invite record created in created status. content: application/json: schema: $ref: "#/components/schemas/InviteRecord" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/invites/{invite_id}/redeem: post: tags: - Invites operationId: redeemInvite summary: Redeem an invite and join a private game description: | Redeems a `created` invite, reserves the chosen race name, and creates an active membership immediately without a separate owner-approval step. Pre-conditions: invite status is `created`; game is `enrollment_open`; roster capacity allows additional participants; Race Name Directory confirms `race_name` is available for the acting user. On success, `lobby.invite.redeemed` notification intent is published to the private-game owner. Authorization: the invited user (invitee_user_id must match X-User-ID). parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/InviteIDPath" - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RedeemInviteRequest" responses: "200": description: Active membership created for the redeeming user. content: application/json: schema: $ref: "#/components/schemas/MembershipRecord" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "422": $ref: "#/components/responses/DomainPreconditionError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/invites/{invite_id}/decline: post: tags: - Invites operationId: declineInvite summary: Decline a received invite description: | Transitions a `created` invite to `declined`. No notification is published in v1. Declined users may receive a new invite from the owner while enrollment is open. Authorization: the invited user (invitee_user_id must match X-User-ID). parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/InviteIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Invite record with status declined. content: application/json: schema: $ref: "#/components/schemas/InviteRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/invites/{invite_id}/revoke: post: tags: - Invites operationId: revokeInvite summary: Revoke a sent invite description: | Transitions a `created` invite to `revoked`. No notification is published in v1. Authorization: private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/InviteIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Invite record with status revoked. content: application/json: schema: $ref: "#/components/schemas/InviteRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/memberships: get: tags: - Memberships operationId: listMemberships summary: List memberships of a game description: | Returns a paginated list of memberships for the game. Authorization: system administrator, game owner, or active member. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/PageSize" - $ref: "#/components/parameters/PageToken" responses: "200": description: One deterministic page of membership records. content: application/json: schema: $ref: "#/components/schemas/MembershipListResponse" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/memberships/{membership_id}/remove: post: tags: - Memberships operationId: removeMember summary: Remove a member from a game description: | Removes an active member. Before game start: drops the membership and releases the race name reservation. After game start: marks membership `removed`; `Game Master` must deactivate the player slot; race name reservation is retained until the game finishes. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/MembershipIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated membership record with status removed. content: application/json: schema: $ref: "#/components/schemas/MembershipRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/games/{game_id}/memberships/{membership_id}/block: post: tags: - Memberships operationId: blockMember summary: Apply a platform-level block to a member description: | Blocks an active member. The engine slot is retained but the member cannot send commands through `Game Master`. Race name reservation is preserved. Authorization: system administrator or private-game owner. parameters: - $ref: "#/components/parameters/GameIDPath" - $ref: "#/components/parameters/MembershipIDPath" - $ref: "#/components/parameters/XUserID" responses: "200": description: Updated membership record with status blocked. content: application/json: schema: $ref: "#/components/schemas/MembershipRecord" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/my/games: get: tags: - MyLists operationId: listMyGames summary: List active games for the authenticated user description: | Returns games where the authenticated user holds an active membership and the game status is `running` or `paused`. Response includes the denormalized runtime snapshot for each game. parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/PageSize" - $ref: "#/components/parameters/PageToken" responses: "200": description: One page of active game records including runtime snapshot. content: application/json: schema: $ref: "#/components/schemas/GameListResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/my/applications: get: tags: - MyLists operationId: listMyApplications summary: List pending applications for the authenticated user description: | Returns applications submitted by the authenticated user with status `submitted`. Each item includes game name and type for display. parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/PageSize" - $ref: "#/components/parameters/PageToken" responses: "200": description: One page of submitted application items. content: application/json: schema: $ref: "#/components/schemas/MyApplicationListResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/my/invites: get: tags: - MyLists operationId: listMyInvites summary: List open invites addressed to the authenticated user description: | Returns invites addressed to the authenticated user with status `created`. Each item includes game name, inviter name, and `expires_at`. parameters: - $ref: "#/components/parameters/XUserID" - $ref: "#/components/parameters/PageSize" - $ref: "#/components/parameters/PageToken" responses: "200": description: One page of open invite items. content: application/json: schema: $ref: "#/components/schemas/MyInviteListResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/my/race-names: get: tags: - RaceNames operationId: listMyRaceNames summary: List the acting user's race-name directory entries description: | Returns the acting user's view of the Race Name Directory across all three levels of binding: permanent registered names, `pending_registration` entries waiting for the 30-day window to elapse, and active per-game reservations. Each reservation carries the current `game_status` of its hosting game so the UI can render it next to the game state. The endpoint reads only the `user_registered` and `user_reservations` indexes; it never scans the full directory. The response is exclusively scoped to the caller. There is no `?user_id=` parameter; admin-side cross-user reads are not exposed by this route. parameters: - $ref: "#/components/parameters/XUserID" responses: "200": description: Snapshot of the acting user's race-name bindings. content: application/json: schema: $ref: "#/components/schemas/MyRaceNamesResponse" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/lobby/race-names/register: post: tags: - RaceNames operationId: registerRaceName summary: Convert a pending race-name registration into a permanent one description: | Converts the caller's `pending_registration` for `(source_game_id, race_name)` into a permanent registered race name. The pending entry must still be inside its 30-day window, the caller must not carry an active `permanent_block`, and the caller's `max_registered_race_names` allowance from the User Service eligibility snapshot must permit the new registration (a value of `0` denotes the unlimited lifetime tariff). The call is idempotent: a repeated request with the same body returns the previously registered record without consuming any additional quota slot. The notification intent `lobby.race_name.registered` is emitted on every successful return; consumers deduplicate using the stable idempotency key `lobby.race_name.registered::`. parameters: - $ref: "#/components/parameters/XUserID" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegisterRaceNameRequest" responses: "200": description: Race name successfully registered. content: application/json: schema: $ref: "#/components/schemas/RegisteredRaceName" "400": $ref: "#/components/responses/InvalidRequestError" "403": $ref: "#/components/responses/ForbiddenError" "404": $ref: "#/components/responses/NotFoundError" "422": $ref: "#/components/responses/DomainPreconditionError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" components: parameters: XUserID: name: X-User-ID in: header required: true description: | Authenticated platform user identifier injected by `Edge Gateway`. `Lobby` derives the acting user identity exclusively from this header. schema: type: string GameIDPath: name: game_id in: path required: true description: Opaque stable game identifier. schema: type: string ApplicationIDPath: name: application_id in: path required: true description: Opaque stable application identifier. schema: type: string InviteIDPath: name: invite_id in: path required: true description: Opaque stable invite identifier. schema: type: string MembershipIDPath: name: membership_id in: path required: true description: Opaque stable membership identifier. schema: type: string PageSize: name: page_size in: query required: false description: | Maximum number of items to return. Default is `50`; maximum is `200`. schema: type: integer minimum: 1 maximum: 200 default: 50 PageToken: name: page_token in: query required: false description: Opaque continuation token returned as `next_page_token` in a previous response. schema: type: string schemas: GameRecord: type: object additionalProperties: false required: - game_id - game_name - game_type - owner_user_id - status - min_players - max_players - start_gap_hours - start_gap_players - enrollment_ends_at - turn_schedule - target_engine_version - created_at - updated_at - current_turn - runtime_status - engine_health_summary properties: game_id: type: string description: Opaque stable game identifier in game-* form. game_name: type: string description: Human-readable game name; mutable in draft status. description: type: string description: Optional game description; mutable in draft and enrollment_open. game_type: type: string enum: - public - private description: Game visibility and enrollment model. owner_user_id: type: string description: Platform user identifier of the private-game owner; empty for public games. status: type: string enum: - draft - enrollment_open - ready_to_start - starting - start_failed - running - paused - finished - cancelled description: Current platform-level lifecycle status. min_players: type: integer description: Minimum approved participants required to proceed to start. max_players: type: integer description: Target roster size that activates the gap window. start_gap_hours: type: integer description: Hours of gap window after max_players is reached. start_gap_players: type: integer description: Additional participants admitted during the gap window. enrollment_ends_at: type: integer format: int64 description: UTC Unix seconds; deadline for automatic enrollment close. turn_schedule: type: string description: Five-field cron expression for scheduled turn generation; passed to Game Master at registration. target_engine_version: type: string description: Semver of the game engine to launch; passed to Game Master at registration. created_at: type: integer format: int64 description: UTC Unix milliseconds; record creation timestamp. updated_at: type: integer format: int64 description: UTC Unix milliseconds; last mutation timestamp. started_at: type: integer format: int64 description: UTC Unix milliseconds; set when status becomes running. finished_at: type: integer format: int64 description: UTC Unix milliseconds; set when status becomes finished. current_turn: type: integer description: Denormalized from Game Master; zero until the game is running. runtime_status: type: string description: Denormalized from Game Master; empty until the game is running. engine_health_summary: type: string description: Denormalized from Game Master; empty until the game is running. runtime_binding: $ref: "#/components/schemas/RuntimeBinding" RuntimeBinding: type: object additionalProperties: false description: | Runtime binding metadata persisted on the game record after a successful container start. Absent before the start sequence completes. required: - container_id - engine_endpoint - runtime_job_id - bound_at properties: container_id: type: string description: Engine container identifier assigned by Runtime Manager. engine_endpoint: type: string description: Network address Game Master uses to reach the engine container. runtime_job_id: type: string description: | Source `runtime:job_results` Redis Stream message id (in `-` form) that produced this binding. bound_at: type: integer format: int64 description: UTC Unix milliseconds when the binding was persisted. ApplicationRecord: type: object additionalProperties: false required: - application_id - game_id - applicant_user_id - race_name - status - created_at properties: application_id: type: string description: Opaque stable application identifier. game_id: type: string description: Identifier of the game this application belongs to. applicant_user_id: type: string description: Platform user identifier of the applicant. race_name: type: string description: Desired in-game name submitted with the application. status: type: string enum: - submitted - approved - rejected description: Current application lifecycle status. created_at: type: integer format: int64 description: UTC Unix milliseconds; application submission timestamp. decided_at: type: integer format: int64 description: UTC Unix milliseconds; set when application is approved or rejected. InviteRecord: type: object additionalProperties: false required: - invite_id - game_id - inviter_user_id - invitee_user_id - status - created_at - expires_at properties: invite_id: type: string description: Opaque stable invite identifier. game_id: type: string description: Identifier of the game this invite belongs to. inviter_user_id: type: string description: Platform user identifier of the game owner who created the invite. invitee_user_id: type: string description: Platform user identifier of the invited user. race_name: type: string description: In-game name chosen by the invitee at redeem time; absent until the invite is redeemed. status: type: string enum: - created - redeemed - declined - revoked - expired description: Current invite lifecycle status. created_at: type: integer format: int64 description: UTC Unix milliseconds; invite creation timestamp. expires_at: type: integer format: int64 description: UTC Unix milliseconds; equals enrollment_ends_at of the game at creation time. decided_at: type: integer format: int64 description: UTC Unix milliseconds; set when invite is redeemed, declined, revoked, or expired. MembershipRecord: type: object additionalProperties: false required: - membership_id - game_id - user_id - race_name - status - joined_at properties: membership_id: type: string description: Opaque stable membership identifier. game_id: type: string description: Identifier of the game this membership belongs to. user_id: type: string description: Platform user identifier of the member. race_name: type: string description: Confirmed in-game name; reserved in Race Name Directory. status: type: string enum: - active - removed - blocked description: Current membership status. joined_at: type: integer format: int64 description: UTC Unix milliseconds; membership activation timestamp. removed_at: type: integer format: int64 description: UTC Unix milliseconds; set when membership is removed or blocked. MyApplicationItem: type: object additionalProperties: false required: - application_id - game_id - applicant_user_id - race_name - status - created_at - game_name - game_type properties: application_id: type: string game_id: type: string applicant_user_id: type: string race_name: type: string status: type: string enum: - submitted - approved - rejected created_at: type: integer format: int64 decided_at: type: integer format: int64 game_name: type: string description: Human-readable game name for display purposes. game_type: type: string enum: - public - private description: Game type for display purposes. MyInviteItem: type: object additionalProperties: false required: - invite_id - game_id - inviter_user_id - invitee_user_id - status - created_at - expires_at - game_name - inviter_name properties: invite_id: type: string game_id: type: string inviter_user_id: type: string invitee_user_id: type: string race_name: type: string status: type: string enum: - created - redeemed - declined - revoked - expired created_at: type: integer format: int64 expires_at: type: integer format: int64 decided_at: type: integer format: int64 game_name: type: string description: Human-readable game name for display purposes. inviter_name: type: string description: Owner's race name if already a member of the game; otherwise the owner's user_id. CreateGameRequest: type: object additionalProperties: false required: - game_name - game_type - min_players - max_players - start_gap_hours - start_gap_players - enrollment_ends_at - turn_schedule - target_engine_version properties: game_name: type: string description: Human-readable game name; must be non-empty after trim. description: type: string description: Optional game description. game_type: type: string enum: - public - private description: Game visibility and enrollment model. min_players: type: integer minimum: 1 description: Minimum approved participants required to proceed to start; must be <= max_players. max_players: type: integer minimum: 1 description: Target roster size that activates the gap window; must be >= min_players. start_gap_hours: type: integer minimum: 0 description: Hours of gap window after max_players is reached. start_gap_players: type: integer minimum: 0 description: Additional participants admitted during the gap window. enrollment_ends_at: type: integer format: int64 description: UTC Unix seconds; deadline for automatic enrollment close; must be a positive integer. turn_schedule: type: string description: Valid five-field cron expression for scheduled turn generation. target_engine_version: type: string description: Non-empty semver string of the game engine to launch. UpdateGameRequest: type: object additionalProperties: false description: | Partial update of a game record. Only fields present in the request body are modified. `game_name`, `min_players`, `max_players`, `start_gap_hours`, `start_gap_players`, `enrollment_ends_at`, `turn_schedule`, and `target_engine_version` are mutable in `draft` status only. `description` is additionally mutable in `enrollment_open` status. properties: game_name: type: string description: type: string min_players: type: integer minimum: 1 max_players: type: integer minimum: 1 start_gap_hours: type: integer minimum: 0 start_gap_players: type: integer minimum: 0 enrollment_ends_at: type: integer format: int64 turn_schedule: type: string target_engine_version: type: string SubmitApplicationRequest: type: object additionalProperties: false required: - race_name properties: race_name: type: string description: Desired in-game name; must be available in the Race Name Directory. CreateInviteRequest: type: object additionalProperties: false required: - invitee_user_id properties: invitee_user_id: type: string description: Platform user identifier of the user to invite. RedeemInviteRequest: type: object additionalProperties: false required: - race_name properties: race_name: type: string description: Desired in-game name; must be available in the Race Name Directory. RegisterRaceNameRequest: type: object additionalProperties: false required: - race_name - source_game_id properties: race_name: type: string description: | Original-casing race name to register. Must match the canonical key of an existing `pending_registration` owned by the caller in `source_game_id`. source_game_id: type: string description: | Identifier of the finished game whose capable finish produced the pending registration to convert. RegisteredRaceName: type: object additionalProperties: false required: - canonical_key - race_name - source_game_id - registered_at_ms properties: canonical_key: type: string description: | Race Name Directory canonical key derived from the policy (lowercase + frozen confusable-pair map). race_name: type: string description: Original-casing display value owned by the caller. source_game_id: type: string description: | Game whose capable finish produced the pending registration converted by this call. registered_at_ms: type: integer format: int64 description: | UTC Unix milliseconds timestamp recorded by the directory on the original commit. Idempotent retries return the same value. PendingRaceName: type: object additionalProperties: false required: - canonical_key - race_name - source_game_id - eligible_until_ms properties: canonical_key: type: string description: | Race Name Directory canonical key derived from the policy (lowercase + frozen confusable-pair map). race_name: type: string description: Original-casing display value held by the caller. source_game_id: type: string description: | Game whose capable finish produced this pending entry. Use this value as `source_game_id` when calling `lobby.race_name.register`. reserved_at_ms: type: integer format: int64 description: | UTC Unix milliseconds timestamp of the original `Reserve` call that became this pending entry. eligible_until_ms: type: integer format: int64 description: | UTC Unix milliseconds deadline for converting the pending entry into a registered race name. After this moment the pending-registration expiration worker releases it. RaceNameReservation: type: object additionalProperties: false required: - canonical_key - race_name - game_id - game_status properties: canonical_key: type: string description: | Race Name Directory canonical key derived from the policy (lowercase + frozen confusable-pair map). race_name: type: string description: Original-casing display value held by the caller. game_id: type: string description: Game hosting the reservation. reserved_at_ms: type: integer format: int64 description: | UTC Unix milliseconds timestamp of the `Reserve` call. game_status: type: string description: | Current `game.Status` of the hosting game. Empty when the game record cannot be loaded (defensive only — this should not occur in normal operation). MyRaceNamesResponse: type: object additionalProperties: false required: - registered - pending - reservations properties: registered: type: array items: $ref: "#/components/schemas/RegisteredRaceName" pending: type: array items: $ref: "#/components/schemas/PendingRaceName" reservations: type: array items: $ref: "#/components/schemas/RaceNameReservation" GameListResponse: type: object additionalProperties: false required: - items properties: items: type: array items: $ref: "#/components/schemas/GameRecord" next_page_token: type: string description: Opaque continuation token; absent when no further pages exist. MembershipListResponse: type: object additionalProperties: false required: - items properties: items: type: array items: $ref: "#/components/schemas/MembershipRecord" next_page_token: type: string description: Opaque continuation token; absent when no further pages exist. MyApplicationListResponse: type: object additionalProperties: false required: - items properties: items: type: array items: $ref: "#/components/schemas/MyApplicationItem" next_page_token: type: string description: Opaque continuation token; absent when no further pages exist. MyInviteListResponse: type: object additionalProperties: false required: - items properties: items: type: array items: $ref: "#/components/schemas/MyInviteItem" next_page_token: type: string description: Opaque continuation token; absent when no further pages exist. ProbeResponse: type: object additionalProperties: false required: - status properties: status: type: string description: Stable probe outcome string. ErrorResponse: type: object additionalProperties: false required: - error properties: error: $ref: "#/components/schemas/ErrorBody" ErrorBody: type: object additionalProperties: false required: - code - message properties: code: type: string description: Stable internal API error code. message: type: string description: Human-readable trusted error message. responses: InvalidRequestError: description: Request validation failed. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: invalidRequest: value: error: code: invalid_request message: request is invalid ForbiddenError: description: Caller is not authorized for this operation on this resource. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: forbidden: value: error: code: forbidden message: access denied NotFoundError: description: The requested game, application, invite, or membership does not exist. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: notFound: value: error: code: subject_not_found message: resource not found ConflictError: description: The requested state transition is not allowed from the current status. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: conflict: value: error: code: conflict message: operation not allowed in current status DomainPreconditionError: description: | A domain-level precondition was not met. Stable codes returned under this response: - `eligibility_denied` — user not eligible per User Service - `name_taken` — race_name is already reserved by another user - `race_name_pending_window_expired` — the 30-day pending registration window has lapsed - `race_name_registration_quota_exceeded` — caller exhausted their tariff `max_registered_race_names` allowance content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: eligibilityDenied: value: error: code: eligibility_denied message: user is not eligible to join games nameTaken: value: error: code: name_taken message: race name is already taken raceNamePendingWindowExpired: value: error: code: race_name_pending_window_expired message: pending race-name registration window has expired raceNameRegistrationQuotaExceeded: value: error: code: race_name_registration_quota_exceeded message: race name registration quota exceeded InternalError: description: Unexpected internal service error. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: internal: value: error: code: internal_error message: internal server error ServiceUnavailableError: description: An upstream dependency is unavailable. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: unavailable: value: error: code: service_unavailable message: service is unavailable