openapi: 3.1.0
info:
  version: 1.0.0
  title: Stratastax API
  contact:
    name: Contact our support
    email: info@buijs.dev
    url: https://buijs.dev/
  license:
    name: Apache-2.0
    url: https://github.com/buijs-dev/stratastax-contract/blob/main/LICENSE
  description: Match tracking API for the Stratastax engine platform.
servers:
  - url: https://stratastax.dev/rest
security:
  - bearerAuth: []
tags:
  - name: matches
    description: Match tracking operations.
  - name: matchEdits
    description: Match edit proposal and approval workflow.
  - name: games
    description: Game catalogue.
  - name: expansions
    description: Game expansion management.
  - name: playsets
    description: Playset configuration management.
  - name: ghostPlayers
    description: Ghost player management.
  - name: canonicalPlayers
    description: Canonical player profile.
  - name: trustedEditors
    description: Trusted editor grant management.
  - name: claimRequests
    description: Ghost player claim request workflow.
  - name: claimEdges
    description: Active ghost player claim edges.
  - name: privacySettings
    description: Privacy configuration per data type.
  - name: groups
    description: Group management for privileged access control.
  - name: communities
    description: Community management.
  - name: memberships
    description: Community membership management.
  - name: membershipRequests
    description: Community membership request workflow.
  - name: users
    description: The current user and finding other users.
paths:
  /api/v1/matches/count:
    get:
      summary: Count matches
      description: |
        Counts the matches that match the filter; soft-deleted matches are not counted. Filterable on `id`, `gameId`, `gameName`, `playsetId`, `playsetName`, `communityId`, `communityName`, `recorderId`, `recorderName`, `playedAt`, `finalizedAt`, `editRequestsEnabled`, `lastEditedAt`, `lastEditedById`, `createdAt`, `expansionsId`, `expansionsName`.
      operationId: countMatches
      security:
        - bearerAuth:
            - stx-engine:match-read
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/query_filter'
      responses:
        '200':
          $ref: '#/components/responses/count'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/matches/{matchId}:
    parameters:
      - $ref: '#/components/parameters/path_match_id'
    get:
      summary: Get match
      operationId: getMatch
      security:
        - bearerAuth:
            - stx-engine:match-read
      tags:
        - matches
      responses:
        '200':
          $ref: '#/components/responses/match'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update match
      description: |
        Updates match fields. While the match is DRAFT, all fields including participants may be changed here; a participant that is left out of `participants` is deleted, together with its loadout. Once FINALIZED, only playedAt and editRequestsEnabled remain directly editable; use POST /matches/{matchId}/edits for anything else.
      operationId: updateMatch
      security:
        - bearerAuth:
            - stx-engine:match-update
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_match'
      responses:
        '200':
          $ref: '#/components/responses/match'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Delete match
      description: |
        Only allowed while the match is DRAFT. The match is soft-deleted together with its participants: it is no longer listed or counted, and its participants no longer show up in anyone's history.
      operationId: deleteMatch
      security:
        - bearerAuth:
            - stx-engine:match-delete
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/matches/{matchId}/finalize:
    parameters:
      - $ref: '#/components/parameters/path_match_id'
    post:
      summary: Finalize a match
      description: |
        Transitions a match from DRAFT to FINALIZED. From this point on, changes to participants, scores, loadouts, playset or expansions must go through POST /matches/{matchId}/edits. This transition is one-way.
      operationId: finalizeMatch
      security:
        - bearerAuth:
            - stx-engine:match-finalize
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '200':
          $ref: '#/components/responses/match'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/matches/{matchId}/edits:
    parameters:
      - $ref: '#/components/parameters/path_match_id'
    get:
      summary: List edits for a match
      description: |
        Filterable on `id`, `proposedById`, `proposedByName`, `decidedById`, `decidedByName`, `status`, `createdAt`; sortable on `id`, `status`, `createdAt`.
      operationId: listMatchEdits
      security:
        - bearerAuth:
            - stx-engine:match-edit-read
      tags:
        - matchEdits
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/match_edit_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Propose a match edit
      description: |
        Proposes a change to a finalized match whose edit requests are enabled: new results or loadouts of its participants, another playset (or none), other expansions of its game (or none). An edit by a trusted editor of the recorder is approved at once.
      operationId: createMatchEdit
      security:
        - bearerAuth:
            - stx-engine:match-edit-create
      tags:
        - matchEdits
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_match_edit'
      responses:
        '201':
          $ref: '#/components/responses/created_match_edit'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/matches/{matchId}/edits/{editId}:
    parameters:
      - $ref: '#/components/parameters/path_match_id'
      - $ref: '#/components/parameters/path_edit_id'
    get:
      summary: Get match edit
      operationId: getMatchEdit
      security:
        - bearerAuth:
            - stx-engine:match-edit-read
      tags:
        - matchEdits
      responses:
        '200':
          $ref: '#/components/responses/match_edit'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Decide on a match edit (approve or reject)
      description: |
        Approves or rejects a pending edit; only the match's recorder decides. On approval the proposed changes are written to the match, and a dispute on a changed participant is cleared. A decided edit cannot be decided again (409).
      operationId: decideMatchEdit
      security:
        - bearerAuth:
            - stx-engine:match-edit-decide
      tags:
        - matchEdits
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/decide_match_edit'
      responses:
        '200':
          $ref: '#/components/responses/match_edit'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/matches/{matchId}/participants/{matchParticipantId}/dispute:
    parameters:
      - $ref: '#/components/parameters/path_match_id'
      - $ref: '#/components/parameters/path_match_participant_id'
    post:
      summary: Dispute a result
      description: |
        Records that the claimed player of a participant disagrees with their recorded result, with the reason; the recorder sees it in the match. Only a user whose canonical player has an active claim edge to the participant's ghost player may dispute it, and only in a finalized match. A new dispute replaces an earlier one. The dispute is cleared when it is withdrawn or when an approved match edit changes the participant; the way to have the result corrected is a match edit (POST /matches/{matchId}/edits).
      operationId: disputeMatchParticipant
      security:
        - bearerAuth:
            - stx-engine:match-participant-dispute
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/dispute_participant'
      responses:
        '200':
          $ref: '#/components/responses/match'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Withdraw a dispute
      description: Withdraws the dispute of the caller on the participant.
      operationId: withdrawMatchParticipantDispute
      security:
        - bearerAuth:
            - stx-engine:match-participant-dispute
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '200':
          $ref: '#/components/responses/match'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/matches:
    get:
      summary: List matches
      description: |
        Soft-deleted matches are never listed. Filterable on `id`, `gameId`, `gameName`, `playsetId`, `playsetName`, `communityId`, `communityName`, `recorderId`, `recorderName`, `playedAt`, `finalizedAt`, `editRequestsEnabled`, `lastEditedAt`, `lastEditedById`, `createdAt`, `expansionsId`, `expansionsName`; sortable on `id`, `playedAt`, `editRequestsEnabled`, `createdAt`.
      operationId: listMatches
      security:
        - bearerAuth:
            - stx-engine:match-read
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/match_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create match
      description: |
        Records a match as a DRAFT. A ghost player takes part at most once; the playset and the expansions must be of the game.
      operationId: createMatch
      security:
        - bearerAuth:
            - stx-engine:match-create
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_match'
      responses:
        '201':
          $ref: '#/components/responses/created_match'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/matches/batch:
    post:
      summary: Create multiple matches in one request
      operationId: createMatchBatch
      security:
        - bearerAuth:
            - stx-engine:match-create
      tags:
        - matches
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_match_batch'
      responses:
        '207':
          $ref: '#/components/responses/match_batch_result'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/games/suggestions:
    get:
      summary: Get game suggestions
      operationId: getGameSuggestions
      security:
        - bearerAuth:
            - stx-engine:game-read
      tags:
        - games
      parameters:
        - $ref: '#/components/parameters/query_query'
      responses:
        '200':
          $ref: '#/components/responses/game_ref_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/games/{gameId}:
    parameters:
      - $ref: '#/components/parameters/path_game_id'
    get:
      summary: Get game
      operationId: getGame
      security:
        - bearerAuth:
            - stx-engine:game-read
      tags:
        - games
      responses:
        '200':
          $ref: '#/components/responses/game'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update game
      description: |
        Changes a game of the catalogue. Requires the `stx-engine:game-update` role, which only the game catalog service has: it updates a game when a published candidate is approved again with new values.
      operationId: updateGame
      security:
        - bearerAuth:
            - stx-engine:game-update
      tags:
        - games
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_game'
      responses:
        '200':
          $ref: '#/components/responses/game'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/games/{gameId}/expansions:
    parameters:
      - $ref: '#/components/parameters/path_game_id'
    get:
      summary: List expansions for a game
      description: |
        Filterable on `id`, `name`, `description`, `sourceRef`, `createdAt`, `updatedAt`; sortable on `id`, `name`, `description`, `createdAt`, `updatedAt`.
      operationId: listExpansions
      security:
        - bearerAuth:
            - stx-engine:expansion-read
      tags:
        - expansions
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/expansion_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create expansion
      description: |
        Adds an expansion to a game. Requires the `stx-engine:expansion-create` role; the game catalog service publishes approved candidates through this operation.
      operationId: createExpansion
      security:
        - bearerAuth:
            - stx-engine:expansion-create
      tags:
        - expansions
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_expansion'
      responses:
        '201':
          $ref: '#/components/responses/created_expansion'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/games/{gameId}/expansions/{expansionId}:
    parameters:
      - $ref: '#/components/parameters/path_game_id'
      - $ref: '#/components/parameters/path_expansion_id'
    get:
      summary: Get expansion
      operationId: getExpansion
      security:
        - bearerAuth:
            - stx-engine:expansion-read
      tags:
        - expansions
      responses:
        '200':
          $ref: '#/components/responses/expansion'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update expansion
      description: |
        Changes an expansion of a game. Requires the `stx-engine:expansion-update` role, which only the game catalog service has: it updates an expansion when a published candidate is approved again with new values.
      operationId: updateExpansion
      security:
        - bearerAuth:
            - stx-engine:expansion-update
      tags:
        - expansions
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_expansion'
      responses:
        '200':
          $ref: '#/components/responses/expansion'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/games:
    get:
      summary: List games
      description: |
        Filterable on `id`, `name`, `description`, `minPlayers`, `maxPlayers`, `tags`, `scoreType`, `winCondition`, `sourceRef`, `createdAt`, `updatedAt`, `expansionsId`, `expansionsName`; sortable on `id`, `name`, `createdAt`, `updatedAt`.
      operationId: listGames
      security:
        - bearerAuth:
            - stx-engine:game-read
      tags:
        - games
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/game_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create game
      description: |
        Adds a game to the catalogue. Requires the `stx-engine:game-create` role; the game catalog service publishes approved candidates through this operation.
      operationId: createGame
      security:
        - bearerAuth:
            - stx-engine:game-create
      tags:
        - games
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_game'
      responses:
        '201':
          $ref: '#/components/responses/created_game'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/playsets:
    get:
      summary: List playsets
      description: |
        Soft-deleted playsets are never listed. Filterable on `id`, `name`, `variants`, `scenario`, `gameId`, `gameName`, `ownerId`, `ownerName`, `expansionsId`, `expansionsName`, `createdAt`, `updatedAt`; sortable on `id`, `name`, `scenario`, `createdAt`, `updatedAt`.
      operationId: listPlaysets
      security:
        - bearerAuth:
            - stx-engine:playset-read
      tags:
        - playsets
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/playset_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create playset
      operationId: createPlayset
      security:
        - bearerAuth:
            - stx-engine:playset-create
      tags:
        - playsets
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_playset'
      responses:
        '201':
          $ref: '#/components/responses/created_playset'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/playsets/{playsetId}:
    parameters:
      - $ref: '#/components/parameters/path_playset_id'
    get:
      summary: Get playset
      operationId: getPlayset
      security:
        - bearerAuth:
            - stx-engine:playset-read
      tags:
        - playsets
      responses:
        '200':
          $ref: '#/components/responses/playset'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update playset
      operationId: updatePlayset
      security:
        - bearerAuth:
            - stx-engine:playset-update
      tags:
        - playsets
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_playset'
      responses:
        '200':
          $ref: '#/components/responses/playset'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Delete playset
      description: |
        Soft-deletes the playset: matches keep referring to it, but it is no longer listed and cannot be chosen for a match or a match edit.
      operationId: deletePlayset
      security:
        - bearerAuth:
            - stx-engine:playset-delete
      tags:
        - playsets
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/ghost-players/count:
    get:
      summary: Count ghost players
      description: |
        Counts the ghost players that match the filter; deleted ghost players are not counted. Filterable on `id`, `name`, `isSelf`, `ownerId`, `ownerName`, `createdAt`, `updatedAt`.
      operationId: countGhostPlayers
      security:
        - bearerAuth:
            - stx-engine:ghost-player-read
      tags:
        - ghostPlayers
      parameters:
        - $ref: '#/components/parameters/query_filter'
      responses:
        '200':
          $ref: '#/components/responses/count'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/ghost-players/suggestions:
    get:
      summary: Get ghost player suggestions
      description: |
        Ghost players of the caller whose display name matches the query; deleted ghost players are never suggested.
      operationId: getGhostPlayerSuggestions
      security:
        - bearerAuth:
            - stx-engine:ghost-player-read
      tags:
        - ghostPlayers
      parameters:
        - $ref: '#/components/parameters/query_query'
      responses:
        '200':
          $ref: '#/components/responses/ghost_player_ref_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/ghost-players/{ghostPlayerId}:
    parameters:
      - $ref: '#/components/parameters/path_ghost_player_id'
    get:
      summary: Get ghost player
      operationId: getGhostPlayer
      security:
        - bearerAuth:
            - stx-engine:ghost-player-read
      tags:
        - ghostPlayers
      responses:
        '200':
          $ref: '#/components/responses/ghost_player'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update ghost player
      operationId: updateGhostPlayer
      security:
        - bearerAuth:
            - stx-engine:ghost-player-update
      tags:
        - ghostPlayers
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_ghost_player'
      responses:
        '200':
          $ref: '#/components/responses/ghost_player'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Delete ghost player
      description: |
        Soft-deletes the ghost player and revokes its claim edges. It stays in the matches it played, but is no longer listed or suggested and cannot be used in new matches or claim requests. The ghost player that represents the caller (`isSelf`) cannot be deleted (409).
      operationId: deleteGhostPlayer
      security:
        - bearerAuth:
            - stx-engine:ghost-player-delete
      tags:
        - ghostPlayers
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/ghost-players:
    get:
      summary: List ghost players
      description: |
        Deleted ghost players are never listed. Filterable on `id`, `name`, `isSelf`, `ownerId`, `ownerName`, `createdAt`, `updatedAt`; sortable on `id`, `name`, `createdAt`, `updatedAt`.
      operationId: listGhostPlayers
      security:
        - bearerAuth:
            - stx-engine:ghost-player-read
      tags:
        - ghostPlayers
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/ghost_player_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create ghost player
      operationId: createGhostPlayer
      security:
        - bearerAuth:
            - stx-engine:ghost-player-create
      tags:
        - ghostPlayers
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_ghost_player'
      responses:
        '201':
          $ref: '#/components/responses/created_ghost_player'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/canonical-players/{canonicalPlayerId}:
    parameters:
      - $ref: '#/components/parameters/path_canonical_player_id'
    get:
      summary: Get canonical player
      operationId: getCanonicalPlayer
      security:
        - bearerAuth:
            - stx-engine:canonical-player-read
      tags:
        - canonicalPlayers
      responses:
        '200':
          $ref: '#/components/responses/canonical_player'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/trusted-editors:
    get:
      summary: List trusted editors
      description: |
        Filterable on `id`, `grantorId`, `grantorName`, `trusteeId`, `trusteeName`, `revokedAt`, `createdAt`; sortable on `id`, `revokedAt`, `createdAt`.
      operationId: listTrustedEditors
      security:
        - bearerAuth:
            - stx-engine:trusted-editor-read
      tags:
        - trustedEditors
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/trusted_editor_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Grant trusted editor access
      operationId: createTrustedEditor
      security:
        - bearerAuth:
            - stx-engine:trusted-editor-create
      tags:
        - trustedEditors
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_trusted_editor'
      responses:
        '201':
          $ref: '#/components/responses/created_trusted_editor'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/trusted-editors/{trustedEditorId}:
    parameters:
      - $ref: '#/components/parameters/path_trusted_editor_id'
    get:
      summary: Get trusted editor grant
      operationId: getTrustedEditor
      security:
        - bearerAuth:
            - stx-engine:trusted-editor-read
      tags:
        - trustedEditors
      responses:
        '200':
          $ref: '#/components/responses/trusted_editor'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Revoke trusted editor access
      description: |
        Revokes the grant; it stays as history with revokedAt and revokedBy.
      operationId: revokeTrustedEditor
      security:
        - bearerAuth:
            - stx-engine:trusted-editor-revoke
      tags:
        - trustedEditors
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/claim-requests:
    get:
      summary: List claim requests
      description: |
        Filterable on `id`, `canonicalPlayerId`, `canonicalPlayerName`, `decidedById`, `decidedByName`, `status`, `ghostPlayersId`, `ghostPlayersName`, `createdAt`; sortable on `id`, `status`, `createdAt`.
      operationId: listClaimRequests
      security:
        - bearerAuth:
            - stx-engine:claim-request-read
      tags:
        - claimRequests
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/claim_request_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create a claim request
      description: |
        Proposes to link ghost players to the caller's canonical player. A deleted ghost player is unknown (400 NOT_FOUND); a ghost player with an active claim edge cannot be claimed (409).
      operationId: createClaimRequest
      security:
        - bearerAuth:
            - stx-engine:claim-request-create
      tags:
        - claimRequests
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_claim_request'
      responses:
        '201':
          $ref: '#/components/responses/created_claim_request'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/claim-requests/{claimRequestId}:
    parameters:
      - $ref: '#/components/parameters/path_claim_request_id'
    get:
      summary: Get claim request
      operationId: getClaimRequest
      security:
        - bearerAuth:
            - stx-engine:claim-request-read
      tags:
        - claimRequests
      responses:
        '200':
          $ref: '#/components/responses/claim_request'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Decide on a claim request (approve or reject)
      description: |
        Approves or rejects a pending claim request; on approval a claim edge is created per ghost player. A decided request cannot be decided again (409).
      operationId: decideClaimRequest
      security:
        - bearerAuth:
            - stx-engine:claim-request-decide
      tags:
        - claimRequests
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/decide_claim_request'
      responses:
        '200':
          $ref: '#/components/responses/claim_request'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/claim-edges:
    get:
      summary: List claim edges
      description: |
        Filterable on `id`, `canonicalPlayerId`, `canonicalPlayerName`, `ghostPlayerId`, `ghostPlayerName`, `claimRequestId`, `revokedAt`, `revokedById`, `revokedByName`, `createdAt`; sortable on `id`, `revokedAt`, `createdAt`.
      operationId: listClaimEdges
      security:
        - bearerAuth:
            - stx-engine:claim-edge-read
      tags:
        - claimEdges
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/claim_edge_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/claim-edges/{claimEdgeId}:
    parameters:
      - $ref: '#/components/parameters/path_claim_edge_id'
    get:
      summary: Get claim edge
      operationId: getClaimEdge
      security:
        - bearerAuth:
            - stx-engine:claim-edge-read
      tags:
        - claimEdges
      responses:
        '200':
          $ref: '#/components/responses/claim_edge'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Revoke claim edge
      description: |
        Revokes the claim edge; it stays as history with revokedAt and revokedBy. The claim edge of a user's own ghost player (`isSelf`) cannot be revoked (409).
      operationId: revokeClaimEdge
      security:
        - bearerAuth:
            - stx-engine:claim-edge-revoke
      tags:
        - claimEdges
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/privacy-settings:
    get:
      summary: List the privacy settings of the authenticated user
      description: |
        Every user has exactly one setting per data type, created PRIVATE together with the user; they are changed with updatePrivacySetting and never created or deleted through the API. Filterable on `id`, `dataType`, `level`, `groupsId`, `groupsName`, `createdAt`, `updatedAt`; sortable on `id`, `dataType`, `level`, `createdAt`, `updatedAt`.
      operationId: listPrivacySettings
      security:
        - bearerAuth:
            - stx-engine:privacy-setting-read
      tags:
        - privacySettings
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/privacy_setting_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/privacy-settings/{privacySettingId}:
    parameters:
      - $ref: '#/components/parameters/path_privacy_setting_id'
    get:
      summary: Get privacy setting
      operationId: getPrivacySetting
      security:
        - bearerAuth:
            - stx-engine:privacy-setting-read
      tags:
        - privacySettings
      responses:
        '200':
          $ref: '#/components/responses/privacy_setting'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update privacy setting
      description: |
        Changes the level of a privacy setting and, for PRIVILEGED_ACCESS, the groups (owned by the caller) that get access.
      operationId: updatePrivacySetting
      security:
        - bearerAuth:
            - stx-engine:privacy-setting-update
      tags:
        - privacySettings
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_privacy_setting'
      responses:
        '200':
          $ref: '#/components/responses/privacy_setting'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/groups:
    get:
      summary: List groups owned by the authenticated user
      description: |
        Soft-deleted groups are never listed. Filterable on `id`, `name`, `membersId`, `membersName`, `createdAt`, `updatedAt`; sortable on `id`, `name`, `createdAt`, `updatedAt`.
      operationId: listGroups
      security:
        - bearerAuth:
            - stx-engine:group-read
      tags:
        - groups
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/group_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create group
      operationId: createGroup
      security:
        - bearerAuth:
            - stx-engine:group-create
      tags:
        - groups
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_group'
      responses:
        '201':
          $ref: '#/components/responses/created_group'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/groups/{groupId}:
    parameters:
      - $ref: '#/components/parameters/path_group_id'
    get:
      summary: Get group
      operationId: getGroup
      security:
        - bearerAuth:
            - stx-engine:group-read
      tags:
        - groups
      responses:
        '200':
          $ref: '#/components/responses/group'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update group
      operationId: updateGroup
      security:
        - bearerAuth:
            - stx-engine:group-update
      tags:
        - groups
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_group'
      responses:
        '200':
          $ref: '#/components/responses/group'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Delete group
      description: |
        Soft-deletes the group: it no longer grants privileged access or community admin rights.
      operationId: deleteGroup
      security:
        - bearerAuth:
            - stx-engine:group-delete
      tags:
        - groups
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/groups/{groupId}/members:
    parameters:
      - $ref: '#/components/parameters/path_group_id'
    get:
      summary: List members of a group
      description: |
        Filterable on `id`, `name`; sortable on `name`.
      operationId: listGroupMembers
      security:
        - bearerAuth:
            - stx-engine:group-read
      tags:
        - groups
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/group_member_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Add member to group
      operationId: addGroupMember
      security:
        - bearerAuth:
            - stx-engine:group-member-add
      tags:
        - groups
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/add_group_member'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/groups/{groupId}/members/{userId}:
    parameters:
      - $ref: '#/components/parameters/path_group_id'
      - $ref: '#/components/parameters/path_user_id'
    delete:
      summary: Remove member from group
      operationId: removeGroupMember
      security:
        - bearerAuth:
            - stx-engine:group-member-remove
      tags:
        - groups
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/communities:
    get:
      summary: List communities
      description: |
        Soft-deleted communities are never listed. Filterable on `id`, `name`, `description`, `joinPolicy`, `ownerId`, `ownerName`, `adminGroupId`, `adminGroupName`, `createdAt`, `updatedAt`; sortable on `id`, `name`, `createdAt`, `updatedAt`.
      operationId: listCommunities
      security:
        - bearerAuth:
            - stx-engine:community-read
      tags:
        - communities
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/community_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Create community
      operationId: createCommunity
      security:
        - bearerAuth:
            - stx-engine:community-create
      tags:
        - communities
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/create_community'
      responses:
        '201':
          $ref: '#/components/responses/created_community'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/communities/{communityId}:
    parameters:
      - $ref: '#/components/parameters/path_community_id'
    get:
      summary: Get community
      operationId: getCommunity
      security:
        - bearerAuth:
            - stx-engine:community-read
      tags:
        - communities
      responses:
        '200':
          $ref: '#/components/responses/community'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Update community
      operationId: updateCommunity
      security:
        - bearerAuth:
            - stx-engine:community-update
      tags:
        - communities
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/update_community'
      responses:
        '200':
          $ref: '#/components/responses/community'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    delete:
      summary: Delete community
      description: |
        Soft-deletes the community: its matches keep referring to it, but it is no longer listed and cannot be joined.
      operationId: deleteCommunity
      security:
        - bearerAuth:
            - stx-engine:community-delete
      tags:
        - communities
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/communities/{communityId}/memberships:
    parameters:
      - $ref: '#/components/parameters/path_community_id'
    get:
      summary: List memberships in a community
      description: |
        Filterable on `id`, `userId`, `userName`, `joinedAt`; sortable on `id`, `joinedAt`.
      operationId: listMemberships
      security:
        - bearerAuth:
            - stx-engine:membership-read
      tags:
        - memberships
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/membership_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Join a community directly (OPEN join policy)
      operationId: joinCommunity
      security:
        - bearerAuth:
            - stx-engine:community-join
      tags:
        - memberships
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      responses:
        '201':
          $ref: '#/components/responses/created_membership'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/communities/{communityId}/memberships/{userId}:
    parameters:
      - $ref: '#/components/parameters/path_community_id'
      - $ref: '#/components/parameters/path_user_id'
    delete:
      summary: Leave or remove a member from a community
      operationId: removeMembership
      security:
        - bearerAuth:
            - stx-engine:membership-delete
      tags:
        - memberships
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      responses:
        '204':
          $ref: '#/components/responses/no_content'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/communities/{communityId}/membership-requests:
    parameters:
      - $ref: '#/components/parameters/path_community_id'
    get:
      summary: List membership requests for a community
      description: |
        Filterable on `id`, `proposedById`, `proposedByName`, `decidedById`, `decidedByName`, `status`, `createdAt`; sortable on `id`, `status`, `createdAt`.
      operationId: listMembershipRequests
      security:
        - bearerAuth:
            - stx-engine:membership-request-read
      tags:
        - membershipRequests
      parameters:
        - $ref: '#/components/parameters/query_limit'
        - $ref: '#/components/parameters/query_cursor'
        - $ref: '#/components/parameters/query_filter'
        - $ref: '#/components/parameters/query_sort'
      responses:
        '200':
          $ref: '#/components/responses/membership_request_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    post:
      summary: Submit a membership request (REQUIRES_APPROVAL join policy)
      description: |
        Asks to join a community with the REQUIRES_APPROVAL join policy. A user has at most one pending request per community (409).
      operationId: createMembershipRequest
      security:
        - bearerAuth:
            - stx-engine:membership-request-create
      tags:
        - membershipRequests
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
      responses:
        '201':
          $ref: '#/components/responses/created_membership_request'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/communities/{communityId}/membership-requests/{membershipRequestId}:
    parameters:
      - $ref: '#/components/parameters/path_community_id'
      - $ref: '#/components/parameters/path_membership_request_id'
    get:
      summary: Get membership request
      operationId: getMembershipRequest
      security:
        - bearerAuth:
            - stx-engine:membership-request-read
      tags:
        - membershipRequests
      responses:
        '200':
          $ref: '#/components/responses/membership_request'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
    patch:
      summary: Decide on a membership request (approve or reject)
      description: |
        Approves or rejects a pending request; the community's owner or a member of its admin group decides. On approval the user becomes a member. A decided request cannot be decided again (409).
      operationId: decideMembershipRequest
      security:
        - bearerAuth:
            - stx-engine:membership-request-decide
      tags:
        - membershipRequests
      parameters:
        - $ref: '#/components/parameters/header_idempotency_key'
        - $ref: '#/components/parameters/header_if_match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/decide_membership_request'
      responses:
        '200':
          $ref: '#/components/responses/membership_request'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '404':
          $ref: '#/components/responses/not_found_error'
        '409':
          $ref: '#/components/responses/conflict_error'
        '412':
          $ref: '#/components/responses/precondition_failed_error'
        '422':
          $ref: '#/components/responses/unprocessable_content_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/users/me:
    get:
      summary: Get the current user
      description: |
        The authenticated user, with their canonical player and the ghost player that represents themselves when they record a match.
      operationId: getCurrentUser
      security:
        - bearerAuth:
            - stx-engine:user-read
      tags:
        - users
      responses:
        '200':
          $ref: '#/components/responses/current_user'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
  /api/v1/users/suggestions:
    get:
      summary: Get user suggestions
      description: |
        Users whose display name matches the query, e.g. to add one to a group or to grant them trusted editor rights.
      operationId: getUserSuggestions
      security:
        - bearerAuth:
            - stx-engine:user-read
      tags:
        - users
      parameters:
        - $ref: '#/components/parameters/query_query'
      responses:
        '200':
          $ref: '#/components/responses/user_ref_list'
        '400':
          $ref: '#/components/responses/validation_error'
        '401':
          $ref: '#/components/responses/unauthorized_error'
        '403':
          $ref: '#/components/responses/forbidden_error'
        '500':
          $ref: '#/components/responses/internal_server_error'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    query_filter:
      name: filter
      in: query
      required: false
      description: |
        RSQL filter expression evaluated against this resource's filterable fields. Combine multiple conditions with `;` (AND) or `,` (OR), e.g. `gameId==3fa85f64-...;playedAt=gt=2026-01-01T00:00:00Z`. The set of filterable fields is documented per endpoint in the API reference; filtering on a field that isn't in that whitelist returns 400 Bad Request.
      schema:
        type: string
    path_match_id:
      name: matchId
      in: path
      required: true
      description: Public match identifier
      schema:
        type: string
        format: uuid
    header_idempotency_key:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Client-generated UUIDv7, required on every mutating request (POST, PUT, PATCH, DELETE), that makes the request safe to retry. A repeated request with the same key and the same payload returns the stored response (marked with the `Idempotent-Replayed: true` header) instead of executing again; the same key with a different payload is refused with 422. Keys older than the retention window (30 days) are rejected. A key that is not a UUID is refused with 400.
      schema:
        type: string
        examples:
          - 0192f100-0000-7000-8000-0000000000a5
    header_if_match:
      name: If-Match
      in: header
      required: true
      description: |
        ETag of the resource being modified, for optimistic concurrency control: the request is refused with 412 when the resource changed since. Obtain it from the ETag response header of a previous GET or mutation.
      schema:
        type: string
    query_limit:
      name: limit
      in: query
      required: false
      description: Maximum number of items to return
      schema:
        type: integer
        minimum: 1
        maximum: 100
    query_cursor:
      name: cursor
      in: query
      required: false
      description: |
        Opaque cursor from a previous response's nextCursor. Omit to fetch the first page. Clients must treat this value as opaque and only pass back what was previously returned.
      schema:
        type: string
    query_sort:
      name: sort
      in: query
      required: false
      description: |
        Sort order. Format: `field:direction`. Direction is ASC or DESC (default ASC if omitted). 
        Use a comma-separated list for multi-column sorting; priority follows the parameter order.
      schema:
        type: string
        pattern: ^([a-zA-Z0-9_]+(:(ASC|DESC))?)(,([a-zA-Z0-9_]+(:(ASC|DESC))?))*$
      examples:
        singleColumn:
          summary: Sort by a single column
          value: playedAt:DESC
        multiColumn:
          summary: Sort by multiple columns
          value: playedAt:DESC,status:ASC
        implicitDirection:
          summary: Omitted direction defaults to ASC
          value: status,playedAt:DESC
    path_edit_id:
      name: editId
      in: path
      required: true
      description: Public match edit identifier
      schema:
        type: string
        format: uuid
    path_match_participant_id:
      name: matchParticipantId
      in: path
      required: true
      description: Public match participant identifier
      schema:
        type: string
        format: uuid
    query_query:
      name: query
      in: query
      required: true
      schema:
        type: string
        minLength: 1
      description: Free-text search term for suggestion matching
    path_game_id:
      name: gameId
      in: path
      required: true
      description: Public game identifier
      schema:
        type: string
        format: uuid
    path_expansion_id:
      name: expansionId
      in: path
      required: true
      description: Public expansion identifier
      schema:
        type: string
        format: uuid
    path_playset_id:
      name: playsetId
      in: path
      required: true
      description: Public playset identifier
      schema:
        type: string
        format: uuid
    path_ghost_player_id:
      name: ghostPlayerId
      in: path
      required: true
      description: Public ghost player identifier
      schema:
        type: string
        format: uuid
    path_canonical_player_id:
      name: canonicalPlayerId
      in: path
      required: true
      description: Public canonical player identifier
      schema:
        type: string
        format: uuid
    path_trusted_editor_id:
      name: trustedEditorId
      in: path
      required: true
      description: Public trusted editor identifier
      schema:
        type: string
        format: uuid
    path_claim_request_id:
      name: claimRequestId
      in: path
      required: true
      description: Public claim request identifier
      schema:
        type: string
        format: uuid
    path_claim_edge_id:
      name: claimEdgeId
      in: path
      required: true
      description: Public claim edge identifier
      schema:
        type: string
        format: uuid
    path_privacy_setting_id:
      name: privacySettingId
      in: path
      required: true
      description: Public privacy setting identifier
      schema:
        type: string
        format: uuid
    path_group_id:
      name: groupId
      in: path
      required: true
      description: Public group identifier
      schema:
        type: string
        format: uuid
    path_user_id:
      name: userId
      in: path
      required: true
      description: Public user identifier
      schema:
        type: string
        format: uuid
    path_community_id:
      name: communityId
      in: path
      required: true
      description: Public community identifier
      schema:
        type: string
        format: uuid
    path_membership_request_id:
      name: membershipRequestId
      in: path
      required: true
      description: Public membership request identifier
      schema:
        type: string
        format: uuid
  schemas:
    count:
      type: object
      required:
        - count
      properties:
        count:
          type: integer
          format: int64
          minimum: 0
          description: Total number of records matching the given filter criteria
    error_type:
      type: string
      description: |
        Canonical error type identifier (URI). The values are the problem types of stratastax-http's problem-core; stratastax-codegen fails the build of a service when the two disagree.
      enum:
        - https://errors.stratastax.dev/authentication
        - https://errors.stratastax.dev/authorization
        - https://errors.stratastax.dev/validation
        - https://errors.stratastax.dev/not-found
        - https://errors.stratastax.dev/conflict
        - https://errors.stratastax.dev/precondition-failed
        - https://errors.stratastax.dev/internal
    problem_detail:
      type: object
      required:
        - type
        - title
        - status
        - detail
      properties:
        type:
          $ref: '#/components/schemas/error_type'
        title:
          type: string
          description: Short, human-readable summary
        status:
          type: integer
          format: int32
          description: HTTP status code
        detail:
          type: string
          description: Human-readable explanation
        instance:
          type: string
          format: uri
          description: URI of the request that caused the error
    validation_error:
      type: object
      required:
        - field
        - code
        - message
      properties:
        field:
          type: string
          description: Field or property that failed validation
        code:
          type: string
          description: |
            Machine-readable validation error code, e.g. REQUIRED, VALUE_TOO_LONG, INVALID_FORMAT, OUT_OF_RANGE. Canonical list is maintained per resource in the documentation.
        message:
          type: string
          description: Human readable validation message
        rejectedValue:
          description: The actual value that failed validation. Omitted if the value is sensitive.
    validation_problem_detail:
      allOf:
        - $ref: '#/components/schemas/problem_detail'
        - type: object
          properties:
            errors:
              type: array
              items:
                $ref: '#/components/schemas/validation_error'
    game_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    playset_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    community_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    user_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    match_status:
      type: string
      description: |
        DRAFT matches are freely mutable directly via PATCH. Once a match is finalized it becomes immutable except through the match-edit proposal and approval workflow.
      enum:
        - DRAFT
        - FINALIZED
    date_time:
      description: date-time (ISO 8601)
      type: string
      format: date-time
    expansion_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    ghost_player_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    score:
      type: number
      description: |
        Score of a participant: points (SCORE), seconds (TIME) or turns (TURNS), with at most two decimals; a value with more decimals is refused with 400.
      minimum: -99999999.99
      maximum: 99999999.99
    rank:
      type: integer
      format: int32
      description: Position in the match, 1 for the winner; tied participants share a rank
      minimum: 1
    outcome:
      type: string
      enum:
        - WIN
        - LOSS
    loadout:
      type: object
      required:
        - id
        - attributes
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        attributes:
          type: object
          additionalProperties: true
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    dispute_reason:
      type: string
      description: Why the player disagrees with the recorded result
      minLength: 1
      maxLength: 1000
    match_participant:
      type: object
      required:
        - id
        - ghostPlayer
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        ghostPlayer:
          $ref: '#/components/schemas/ghost_player_ref'
        score:
          $ref: '#/components/schemas/score'
        rank:
          $ref: '#/components/schemas/rank'
        outcome:
          $ref: '#/components/schemas/outcome'
        loadout:
          $ref: '#/components/schemas/loadout'
        disputeReason:
          $ref: '#/components/schemas/dispute_reason'
        disputedAt:
          $ref: '#/components/schemas/date_time'
        disputedBy:
          $ref: '#/components/schemas/user_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    match:
      type: object
      required:
        - id
        - game
        - recorder
        - status
        - playedAt
        - editRequestsEnabled
        - createdAt
        - participants
      properties:
        id:
          type: string
          format: uuid
        game:
          $ref: '#/components/schemas/game_ref'
        playset:
          $ref: '#/components/schemas/playset_ref'
        community:
          $ref: '#/components/schemas/community_ref'
        recorder:
          $ref: '#/components/schemas/user_ref'
        status:
          $ref: '#/components/schemas/match_status'
        playedAt:
          $ref: '#/components/schemas/date_time'
        editRequestsEnabled:
          type: boolean
        finalizedAt:
          $ref: '#/components/schemas/date_time'
          description: When the match was finalized; absent while it is a DRAFT.
        lastEditedAt:
          $ref: '#/components/schemas/date_time'
        lastEditedBy:
          $ref: '#/components/schemas/user_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
        expansions:
          type: array
          items:
            $ref: '#/components/schemas/expansion_ref'
        participants:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/match_participant'
    match_participant_input:
      type: object
      description: A participant of a match; a ghost player takes part at most once.
      required:
        - ghostPlayerId
      properties:
        ghostPlayerId:
          type: string
          format: uuid
        score:
          $ref: '#/components/schemas/score'
        rank:
          $ref: '#/components/schemas/rank'
        outcome:
          $ref: '#/components/schemas/outcome'
        loadoutAttributes:
          type: object
          additionalProperties: true
    update_match:
      type: object
      description: |
        Update match fields; an absent field keeps its value, and only playsetId and communityId accept null (to clear them). While the match has status DRAFT, all fields below may be updated directly. Once the match is FINALIZED, only playedAt and editRequestsEnabled remain directly editable; attempting to change playsetId, communityId, expansions or participants of a finalized match returns 409 Conflict; use POST /matches/{matchId}/edits instead.
      properties:
        playsetId:
          type:
            - string
            - 'null'
          format: uuid
        communityId:
          type:
            - string
            - 'null'
          format: uuid
        playedAt:
          $ref: '#/components/schemas/date_time'
        editRequestsEnabled:
          type: boolean
        expansions:
          type: array
          uniqueItems: true
          items:
            type: string
            format: uuid
          description: |
            Replaces the complete expansion collection; an empty list removes all expansions.

            If omitted, the existing expansions remain unchanged.
        participants:
          type: array
          minItems: 1
          description: |
            Replaces the complete participant collection.

            If omitted, the existing participants remain unchanged.
            If provided, the complete participant collection is replaced
            by the supplied collection: a participant that is no longer in it
            is deleted, together with its loadout.

            Each participant must be fully specified. Partial participant
            updates are not supported while the match is in DRAFT status.
          items:
            $ref: '#/components/schemas/match_participant_input'
    paged_metadata:
      type: object
      required:
        - limit
      properties:
        limit:
          type: integer
          format: int32
          description: Maximum number of items returned in this page
        prevCursor:
          type:
            - string
            - 'null'
          description: |
            Opaque cursor to pass as the `cursor` query parameter to fetch the previous page. Null when there is no previous results. Clients must not parse or construct this value.
        nextCursor:
          type:
            - string
            - 'null'
          description: |
            Opaque cursor to pass as the `cursor` query parameter to fetch the next page. Null when there are no more results. Clients must not parse or construct this value.
    approval_status:
      type: string
      enum:
        - PENDING
        - APPROVED
        - REJECTED
    match_edit_participant_change:
      type: object
      description: |
        A proposed change to one participant's result. Only the fields flagged as set are written when the edit is applied; a field that is set but absent is cleared.
      required:
        - matchParticipantId
        - scoreSet
        - rankSet
        - outcomeSet
      properties:
        matchParticipantId:
          type: string
          format: uuid
        score:
          $ref: '#/components/schemas/score'
        rank:
          $ref: '#/components/schemas/rank'
        outcome:
          $ref: '#/components/schemas/outcome'
        scoreSet:
          type: boolean
        rankSet:
          type: boolean
        outcomeSet:
          type: boolean
    match_edit_loadout_change:
      type: object
      description: |
        New loadout attributes of one participant; they replace the current ones. A participant appears at most once per edit (400 DUPLICATE_CHANGE).
      required:
        - matchParticipantId
        - attributes
      properties:
        matchParticipantId:
          type: string
          format: uuid
        attributes:
          type: object
          additionalProperties: true
    match_edit_playset_change:
      type: object
      description: The playset the edit gives the match; absent playset means the edit removes it.
      properties:
        playset:
          $ref: '#/components/schemas/playset_ref'
    match_edit_expansion_change:
      type: object
      description: The expansions the edit gives the match; they replace the current ones, none removes them all.
      required:
        - expansions
      properties:
        expansions:
          type: array
          items:
            $ref: '#/components/schemas/expansion_ref'
    match_edit:
      type: object
      description: |
        A proposed change to a finalized match. Only the parts that are present are changed when the edit is approved.
      required:
        - id
        - matchId
        - proposedBy
        - status
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        matchId:
          type: string
          format: uuid
        proposedBy:
          $ref: '#/components/schemas/user_ref'
        status:
          $ref: '#/components/schemas/approval_status'
        decidedBy:
          $ref: '#/components/schemas/user_ref'
        decidedAt:
          $ref: '#/components/schemas/date_time'
        participantChanges:
          type: array
          items:
            $ref: '#/components/schemas/match_edit_participant_change'
        loadoutChanges:
          type: array
          items:
            $ref: '#/components/schemas/match_edit_loadout_change'
        playsetChange:
          $ref: '#/components/schemas/match_edit_playset_change'
        expansionChange:
          $ref: '#/components/schemas/match_edit_expansion_change'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    match_edit_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Match edits in this page
              items:
                $ref: '#/components/schemas/match_edit'
    match_edit_participant_change_input:
      type: object
      description: |
        A proposed change to one participant's result. A property that is absent is left unchanged; a property that is present replaces the current value, and null clears it. A participant appears at most once per edit (400 DUPLICATE_CHANGE).
      required:
        - matchParticipantId
      properties:
        matchParticipantId:
          type: string
          format: uuid
        score:
          anyOf:
            - $ref: '#/components/schemas/score'
            - type: 'null'
        rank:
          anyOf:
            - $ref: '#/components/schemas/rank'
            - type: 'null'
        outcome:
          anyOf:
            - $ref: '#/components/schemas/outcome'
            - type: 'null'
    match_edit_playset_change_input:
      type: object
      description: |
        Replaces the playset of the match with a playset of the match's game. Without `playsetId`, or with `null`, the edit removes the playset.
      properties:
        playsetId:
          type:
            - string
            - 'null'
          format: uuid
    match_edit_expansion_change_input:
      type: object
      description: |
        Replaces the expansions of the match with these expansions of the match's game; an empty list removes them all.
      required:
        - expansionIds
      properties:
        expansionIds:
          type: array
          uniqueItems: true
          items:
            type: string
            format: uuid
    create_match_edit:
      type: object
      description: |
        A proposed change to a finalized match: at least one of the parts. A part that is absent leaves that aspect of the match as it is.
      minProperties: 1
      properties:
        participantChanges:
          type: array
          items:
            $ref: '#/components/schemas/match_edit_participant_change_input'
        loadoutChanges:
          type: array
          items:
            $ref: '#/components/schemas/match_edit_loadout_change'
        playsetChange:
          $ref: '#/components/schemas/match_edit_playset_change_input'
        expansionChange:
          $ref: '#/components/schemas/match_edit_expansion_change_input'
    decision:
      type: string
      description: Decision on a pending request
      enum:
        - APPROVED
        - REJECTED
    decide_match_edit:
      type: object
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/decision'
    dispute_participant:
      type: object
      required:
        - reason
      properties:
        reason:
          $ref: '#/components/schemas/dispute_reason'
    match_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Matches in this page
              items:
                $ref: '#/components/schemas/match'
    create_match:
      type: object
      required:
        - gameId
        - playedAt
        - participants
      properties:
        gameId:
          type: string
          format: uuid
        playedAt:
          $ref: '#/components/schemas/date_time'
        participants:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/match_participant_input'
        expansions:
          type: array
          description: Expansions of the game that were played
          uniqueItems: true
          items:
            type: string
            format: uuid
        playsetId:
          type: string
          format: uuid
        communityId:
          type: string
          format: uuid
        editRequestsEnabled:
          type: boolean
          default: true
    create_match_batch:
      type: array
      description: Create multiple matches in a single request.
      minItems: 1
      maxItems: 50
      items:
        $ref: '#/components/schemas/create_match'
    match_batch_result_item:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - CREATED
            - FAILED
        match:
          $ref: '#/components/schemas/match'
        error:
          $ref: '#/components/schemas/problem_detail'
    match_batch_result:
      type: object
      required:
        - results
      properties:
        results:
          type: array
          items:
            $ref: '#/components/schemas/match_batch_result_item'
    game_display_name:
      type: string
      description: Name of a game or expansion (`game.display_name`, `candidate.display_name`)
      minLength: 1
      maxLength: 255
    description:
      type: string
      description: Free text
      maxLength: 10000
    player_count:
      type: integer
      format: int32
      description: Number of players; the maximum is never below the minimum
      minimum: 1
    image_url:
      type: string
      format: uri
      description: |
        At most 500 characters (the column it is stored in); a longer URL is refused with 400. Not declared as `maxLength`: the services read a `uri` as a URI type, which length validation does not apply to, so they check the length themselves.
    tag:
      type: string
      minLength: 1
      maxLength: 50
    tag_list:
      type: array
      maxItems: 20
      uniqueItems: true
      items:
        $ref: '#/components/schemas/tag'
    score_type:
      type: string
      description: |
        How a game is won: by score (SCORE), time (TIME) or number of turns (TURNS), each with a win condition, or by winning and losing (WIN_LOSS), which never has a win condition. The engine (`chk_game_win_condition`) and the game catalog (`chk_candidate_win_condition`) enforce the same rule.
      enum:
        - SCORE
        - TIME
        - TURNS
        - WIN_LOSS
    win_condition:
      type: string
      description: Whether the highest or the lowest score wins; required unless the score type is WIN_LOSS, absent then
      enum:
        - HIGHER_WINS
        - LOWER_WINS
    source_ref:
      type: string
      description: |
        Where a game or expansion came from, as `<source>:<id>`, e.g. `bgg:199792`. Unique per game and per expansion: creating a second one with the same source is refused with 409.
      maxLength: 64
      pattern: ^[a-z]+:[0-9]+$
      examples:
        - bgg:199792
    game:
      type: object
      required:
        - id
        - displayName
        - description
        - minPlayers
        - maxPlayers
        - scoreType
        - tags
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        displayName:
          $ref: '#/components/schemas/game_display_name'
        description:
          $ref: '#/components/schemas/description'
        minPlayers:
          $ref: '#/components/schemas/player_count'
        maxPlayers:
          $ref: '#/components/schemas/player_count'
        imageUrl:
          $ref: '#/components/schemas/image_url'
        tags:
          $ref: '#/components/schemas/tag_list'
        scoreType:
          $ref: '#/components/schemas/score_type'
        winCondition:
          $ref: '#/components/schemas/win_condition'
        expansions:
          type: array
          items:
            $ref: '#/components/schemas/expansion_ref'
        sourceRef:
          $ref: '#/components/schemas/source_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    update_game:
      type: object
      description: |
        Every field is optional; an absent field keeps its value. Only the fields that can be empty (imageUrl, winCondition) accept null, which clears them. After the update the win condition is set unless the score type is WIN_LOSS, and absent then.
      properties:
        displayName:
          $ref: '#/components/schemas/game_display_name'
        description:
          $ref: '#/components/schemas/description'
        minPlayers:
          $ref: '#/components/schemas/player_count'
        maxPlayers:
          $ref: '#/components/schemas/player_count'
        imageUrl:
          anyOf:
            - $ref: '#/components/schemas/image_url'
            - type: 'null'
        tags:
          $ref: '#/components/schemas/tag_list'
        scoreType:
          $ref: '#/components/schemas/score_type'
        winCondition:
          anyOf:
            - $ref: '#/components/schemas/win_condition'
            - type: 'null'
    expansion:
      type: object
      required:
        - id
        - game
        - displayName
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        game:
          $ref: '#/components/schemas/game_ref'
        displayName:
          $ref: '#/components/schemas/game_display_name'
        description:
          $ref: '#/components/schemas/description'
        imageUrl:
          $ref: '#/components/schemas/image_url'
        sourceRef:
          $ref: '#/components/schemas/source_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    expansion_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Expansions in this page
              items:
                $ref: '#/components/schemas/expansion'
    create_expansion:
      type: object
      required:
        - displayName
      properties:
        displayName:
          $ref: '#/components/schemas/game_display_name'
        description:
          $ref: '#/components/schemas/description'
        imageUrl:
          $ref: '#/components/schemas/image_url'
        sourceRef:
          $ref: '#/components/schemas/source_ref'
    update_expansion:
      type: object
      description: |
        Every field is optional; an absent field keeps its value. Only the fields that can be empty (description, imageUrl) accept null, which clears them.
      properties:
        displayName:
          $ref: '#/components/schemas/game_display_name'
        description:
          anyOf:
            - $ref: '#/components/schemas/description'
            - type: 'null'
        imageUrl:
          anyOf:
            - $ref: '#/components/schemas/image_url'
            - type: 'null'
    game_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Games in this page
              items:
                $ref: '#/components/schemas/game'
    create_game:
      type: object
      description: |
        A new game. The win condition is required unless the score type is WIN_LOSS, and absent then.
      required:
        - displayName
        - description
        - minPlayers
        - maxPlayers
        - scoreType
        - tags
      properties:
        displayName:
          $ref: '#/components/schemas/game_display_name'
        description:
          $ref: '#/components/schemas/description'
        minPlayers:
          $ref: '#/components/schemas/player_count'
        maxPlayers:
          $ref: '#/components/schemas/player_count'
        imageUrl:
          $ref: '#/components/schemas/image_url'
        tags:
          $ref: '#/components/schemas/tag_list'
        scoreType:
          $ref: '#/components/schemas/score_type'
        winCondition:
          $ref: '#/components/schemas/win_condition'
        sourceRef:
          $ref: '#/components/schemas/source_ref'
    playset_display_name:
      type: string
      minLength: 1
      maxLength: 100
    variant_list:
      type: array
      description: Rule variants that are played
      maxItems: 20
      uniqueItems: true
      items:
        type: string
        minLength: 1
        maxLength: 100
    scenario:
      type: string
      minLength: 1
      maxLength: 200
    playset:
      type: object
      required:
        - id
        - owner
        - game
        - variants
        - expansions
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        owner:
          $ref: '#/components/schemas/user_ref'
        game:
          $ref: '#/components/schemas/game_ref'
        displayName:
          $ref: '#/components/schemas/playset_display_name'
        variants:
          $ref: '#/components/schemas/variant_list'
        scenario:
          $ref: '#/components/schemas/scenario'
        expansions:
          type: array
          items:
            $ref: '#/components/schemas/expansion_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
        deletedAt:
          $ref: '#/components/schemas/date_time'
    playset_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Playsets in this page
              items:
                $ref: '#/components/schemas/playset'
    create_playset:
      type: object
      required:
        - gameId
        - variants
      properties:
        gameId:
          type: string
          format: uuid
        displayName:
          $ref: '#/components/schemas/playset_display_name'
        variants:
          $ref: '#/components/schemas/variant_list'
        scenario:
          $ref: '#/components/schemas/scenario'
        expansions:
          type: array
          description: Expansions of the game
          uniqueItems: true
          items:
            type: string
            format: uuid
    update_playset:
      type: object
      description: |
        Every field is optional; an absent field keeps its value. Only displayName and scenario accept null, which clears them.
      properties:
        displayName:
          anyOf:
            - $ref: '#/components/schemas/playset_display_name'
            - type: 'null'
        scenario:
          anyOf:
            - $ref: '#/components/schemas/scenario'
            - type: 'null'
        variants:
          $ref: '#/components/schemas/variant_list'
        expansions:
          type: array
          description: Replaces the expansions; an empty list removes them all
          uniqueItems: true
          items:
            type: string
            format: uuid
    display_name:
      type: string
      minLength: 1
      maxLength: 100
    ghost_player:
      type: object
      required:
        - id
        - owner
        - displayName
        - isSelf
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        owner:
          $ref: '#/components/schemas/user_ref'
        displayName:
          $ref: '#/components/schemas/display_name'
        isSelf:
          type: boolean
          description: |
            The ghost player that represents the owner themselves in the matches they record. It is claimed by the owner's canonical player and can be neither deleted nor unclaimed.
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    update_ghost_player:
      type: object
      properties:
        displayName:
          $ref: '#/components/schemas/display_name'
    ghost_player_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Ghost players in this page
              items:
                $ref: '#/components/schemas/ghost_player'
    create_ghost_player:
      type: object
      required:
        - displayName
      properties:
        displayName:
          $ref: '#/components/schemas/display_name'
    canonical_player:
      type: object
      required:
        - id
        - displayName
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        displayName:
          $ref: '#/components/schemas/display_name'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    trusted_editor:
      type: object
      required:
        - id
        - grantor
        - trustee
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        grantor:
          $ref: '#/components/schemas/user_ref'
        trustee:
          $ref: '#/components/schemas/user_ref'
        revokedAt:
          $ref: '#/components/schemas/date_time'
        revokedBy:
          $ref: '#/components/schemas/user_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    trusted_editor_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Trusted editors in this page
              items:
                $ref: '#/components/schemas/trusted_editor'
    user_id:
      type: string
      format: uuid
      description: Unique identifier for a user
      examples:
        - 123e4567-e89b-12d3-a456-426614174000
    create_trusted_editor:
      type: object
      required:
        - trusteeUserId
      properties:
        trusteeUserId:
          $ref: '#/components/schemas/user_id'
    canonical_player_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    claim_request:
      type: object
      required:
        - id
        - canonicalPlayer
        - status
        - ghostPlayers
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        canonicalPlayer:
          $ref: '#/components/schemas/canonical_player_ref'
        status:
          $ref: '#/components/schemas/approval_status'
        decidedBy:
          $ref: '#/components/schemas/user_ref'
        decidedAt:
          $ref: '#/components/schemas/date_time'
        ghostPlayers:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/ghost_player_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    claim_request_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Claim requests in this page
              items:
                $ref: '#/components/schemas/claim_request'
    create_claim_request:
      type: object
      required:
        - ghostPlayerIds
      properties:
        ghostPlayerIds:
          type: array
          description: Ghost players to claim for the caller's canonical player; deleted ghost players cannot be claimed
          minItems: 1
          maxItems: 100
          uniqueItems: true
          items:
            type: string
            format: uuid
    decide_claim_request:
      type: object
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/decision'
    claim_edge:
      type: object
      required:
        - id
        - canonicalPlayer
        - ghostPlayer
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        canonicalPlayer:
          $ref: '#/components/schemas/canonical_player_ref'
        ghostPlayer:
          $ref: '#/components/schemas/ghost_player_ref'
        createdViaClaimRequestId:
          type: string
          format: uuid
        revokedAt:
          $ref: '#/components/schemas/date_time'
        revokedBy:
          $ref: '#/components/schemas/user_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    claim_edge_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Claim edges in this page
              items:
                $ref: '#/components/schemas/claim_edge'
    privacy_data_type:
      type: string
      description: The kind of data a privacy setting is about
      enum:
        - PROFILE
        - MATCH_HISTORY
        - MATCH_DETAILS
    visibility:
      type: string
      enum:
        - PRIVATE
        - PUBLIC
        - PRIVILEGED_ACCESS
    group_ref:
      type: object
      required:
        - id
        - displayName
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
    privacy_setting:
      description: |
        Who may see one kind of data of a user. Every user has exactly one setting per data type, created PRIVATE together with the user.
      type: object
      required:
        - id
        - user
        - dataType
        - level
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        user:
          $ref: '#/components/schemas/user_ref'
        dataType:
          $ref: '#/components/schemas/privacy_data_type'
        level:
          $ref: '#/components/schemas/visibility'
        groups:
          type: array
          description: Only meaningful when level is PRIVILEGED_ACCESS
          items:
            $ref: '#/components/schemas/group_ref'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    privacy_setting_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Privacy settings in this page
              items:
                $ref: '#/components/schemas/privacy_setting'
    update_privacy_setting:
      type: object
      description: Every field is optional; an absent field keeps its value.
      properties:
        level:
          $ref: '#/components/schemas/visibility'
        groupIds:
          type: array
          description: |
            Groups of the caller that get access; replaces the current groups. Only meaningful when level is PRIVILEGED_ACCESS.
          uniqueItems: true
          items:
            type: string
            format: uuid
    group:
      type: object
      required:
        - id
        - owner
        - displayName
        - memberCount
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        owner:
          $ref: '#/components/schemas/user_ref'
        displayName:
          $ref: '#/components/schemas/display_name'
        memberCount:
          type: integer
          format: int32
          minimum: 0
          description: Number of members; list them with listGroupMembers
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
        deletedAt:
          $ref: '#/components/schemas/date_time'
    group_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Groups in this page
              items:
                $ref: '#/components/schemas/group'
    create_group:
      type: object
      required:
        - displayName
      properties:
        displayName:
          $ref: '#/components/schemas/display_name'
    update_group:
      type: object
      properties:
        displayName:
          $ref: '#/components/schemas/display_name'
    group_member_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Members in this page
              items:
                $ref: '#/components/schemas/user_ref'
    add_group_member:
      type: object
      required:
        - userId
      properties:
        userId:
          $ref: '#/components/schemas/user_id'
    join_policy:
      type: string
      enum:
        - OPEN
        - REQUIRES_APPROVAL
    community:
      type: object
      required:
        - id
        - owner
        - displayName
        - joinPolicy
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        owner:
          $ref: '#/components/schemas/user_ref'
        adminGroup:
          $ref: '#/components/schemas/group_ref'
        displayName:
          $ref: '#/components/schemas/display_name'
        description:
          $ref: '#/components/schemas/description'
        joinPolicy:
          $ref: '#/components/schemas/join_policy'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
        deletedAt:
          $ref: '#/components/schemas/date_time'
    community_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Communities in this page
              items:
                $ref: '#/components/schemas/community'
    create_community:
      type: object
      required:
        - displayName
        - joinPolicy
      properties:
        displayName:
          $ref: '#/components/schemas/display_name'
        description:
          $ref: '#/components/schemas/description'
        joinPolicy:
          $ref: '#/components/schemas/join_policy'
        adminGroupId:
          type: string
          format: uuid
          description: A group owned by the caller, whose members administer the community
    update_community:
      type: object
      description: |
        Every field is optional; an absent field keeps its value. Only description and adminGroupId accept null, which clears them.
      properties:
        displayName:
          $ref: '#/components/schemas/display_name'
        description:
          anyOf:
            - $ref: '#/components/schemas/description'
            - type: 'null'
        joinPolicy:
          $ref: '#/components/schemas/join_policy'
        adminGroupId:
          type:
            - string
            - 'null'
          format: uuid
    membership:
      type: object
      required:
        - id
        - community
        - user
        - joinedAt
      properties:
        id:
          type: string
          format: uuid
        community:
          $ref: '#/components/schemas/community_ref'
        user:
          $ref: '#/components/schemas/user_ref'
        joinedAt:
          $ref: '#/components/schemas/date_time'
    membership_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Memberships in this page
              items:
                $ref: '#/components/schemas/membership'
    membership_request:
      type: object
      required:
        - id
        - community
        - proposedBy
        - status
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        community:
          $ref: '#/components/schemas/community_ref'
        proposedBy:
          $ref: '#/components/schemas/user_ref'
        status:
          $ref: '#/components/schemas/approval_status'
        decidedBy:
          $ref: '#/components/schemas/user_ref'
        decidedAt:
          $ref: '#/components/schemas/date_time'
        createdAt:
          $ref: '#/components/schemas/date_time'
        updatedAt:
          $ref: '#/components/schemas/date_time'
    membership_request_list:
      type: object
      allOf:
        - $ref: '#/components/schemas/paged_metadata'
        - type: object
          required:
            - items
          properties:
            items:
              type: array
              description: Membership requests in this page
              items:
                $ref: '#/components/schemas/membership_request'
    decide_membership_request:
      type: object
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/decision'
    current_user:
      type: object
      description: |
        The authenticated user: the canonical player that is their identity in Stratastax, and the ghost player that represents themselves in the matches they record.
      required:
        - id
        - displayName
        - canonicalPlayer
        - selfGhostPlayer
      properties:
        id:
          type: string
          format: uuid
        displayName:
          type: string
        canonicalPlayer:
          $ref: '#/components/schemas/canonical_player_ref'
        selfGhostPlayer:
          $ref: '#/components/schemas/ghost_player_ref'
  responses:
    count:
      description: Total number of matching records
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/count'
          examples:
            count:
              value:
                count: 42
    validation_error:
      description: Request validation failed
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/validation_problem_detail'
          examples:
            bad_request:
              value:
                type: https://errors.stratastax.dev/validation
                title: Bad Request
                status: 400
                detail: Received invalid request data
                errors:
                  - field: displayName
                    code: REQUIRED
                    message: missing required field
            duplicate:
              value:
                type: https://errors.stratastax.dev/validation
                title: Bad Request
                status: 400
                detail: Received invalid request data
                errors:
                  - field: participants[2].ghostPlayerId
                    code: DUPLICATE_GHOST_PLAYER
                    message: the ghost player already takes part in this match
            filter_field_not_allowed:
              value:
                type: https://errors.stratastax.dev/validation
                title: Bad Request
                status: 400
                detail: Request validation failed
                errors:
                  - field: query
                    code: INVALID_SEARCH
                    message: '''nonsense'' is not a filterable field'
    unauthorized_error:
      description: Authentication required or token invalid
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            authentication:
              value:
                type: https://errors.stratastax.dev/authentication
                title: Unauthorized
                status: 401
                detail: Authentication required
    forbidden_error:
      description: Insufficient permissions
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            forbidden:
              value:
                type: https://errors.stratastax.dev/authorization
                title: Access denied
                status: 403
                detail: Insufficient permissions
    internal_server_error:
      description: Unexpected server error
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            internal:
              value:
                type: https://errors.stratastax.dev/internal
                title: Oops! Something went wrong.
                status: 500
                detail: Try again later.
    match:
      description: Match retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/match'
    not_found_error:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            not_found:
              value:
                type: https://errors.stratastax.dev/not-found
                title: Resource not found
                status: 404
                detail: The requested resource could not be found
    no_content:
      description: Done; there is no content to return
      headers:
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
    conflict_error:
      description: |
        The requested mutation is not allowed in the resource's current state, or it conflicts with existing data (a unique value that is already taken)
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            already_exists:
              value:
                type: https://errors.stratastax.dev/conflict
                title: Already exists
                status: 409
                detail: A game with source bgg:199792 already exists.
            self_ghost_player:
              value:
                type: https://errors.stratastax.dev/conflict
                title: Self ghost player
                status: 409
                detail: The ghost player that represents yourself cannot be deleted or unclaimed.
            finalized_match:
              value:
                type: https://errors.stratastax.dev/conflict
                title: Match is finalized
                status: 409
                detail: This match is FINALIZED; propose a change via POST /matches/{matchId}/edits instead.
    precondition_failed_error:
      description: The If-Match header did not match the current resource's ETag
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/problem_detail'
          examples:
            precondition_failed:
              value:
                type: https://errors.stratastax.dev/precondition-failed
                title: Resource has changed
                status: 412
                detail: The resource was modified since you last fetched it. Reload and try again.
    unprocessable_content_error:
      description: The Idempotency-Key was already used for a request with a different payload
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/validation_problem_detail'
          examples:
            idempotency_key_reused:
              value:
                type: https://errors.stratastax.dev/validation
                title: Unprocessable Content
                status: 422
                detail: Idempotency key reused for a different request
                errors:
                  - field: Idempotency-Key
                    code: IDEMPOTENCY_KEY_REUSED
                    message: 0192f100-0000-7000-8000-0000000000a5 was already used for a different request
    match_edit_list:
      description: Match edits retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/match_edit_list'
    created_match_edit:
      description: Match edit created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/match_edit'
    match_edit:
      description: Match edit retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/match_edit'
    match_list:
      description: List of matches
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/match_list'
    created_match:
      description: Match created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/match'
    match_batch_result:
      description: Batch match creation result (some items may have failed independently)
      headers:
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/match_batch_result'
    game_ref_list:
      description: List of game references retrieved successfully
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: '#/components/schemas/game_ref'
    game:
      description: Game retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/game'
    expansion_list:
      description: Expansions retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/expansion_list'
    created_expansion:
      description: Expansion created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/expansion'
    expansion:
      description: Expansion retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/expansion'
    game_list:
      description: List of games
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/game_list'
    created_game:
      description: Game created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/game'
    playset_list:
      description: Playsets retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/playset_list'
    created_playset:
      description: Playset created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/playset'
    playset:
      description: Playset retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/playset'
    ghost_player_ref_list:
      description: List of ghost player references retrieved successfully
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: '#/components/schemas/ghost_player_ref'
    ghost_player:
      description: Ghost player retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ghost_player'
    ghost_player_list:
      description: Ghost players retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ghost_player_list'
    created_ghost_player:
      description: Ghost player created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ghost_player'
    canonical_player:
      description: Canonical player retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/canonical_player'
    trusted_editor_list:
      description: Trusted editors retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/trusted_editor_list'
    created_trusted_editor:
      description: Trusted editor created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/trusted_editor'
    trusted_editor:
      description: Trusted editor retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/trusted_editor'
    claim_request_list:
      description: Claim requests retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/claim_request_list'
    created_claim_request:
      description: Claim request created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/claim_request'
    claim_request:
      description: Claim request retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/claim_request'
    claim_edge_list:
      description: Claim edges retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/claim_edge_list'
    claim_edge:
      description: Claim edge retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/claim_edge'
    privacy_setting_list:
      description: Privacy settings retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/privacy_setting_list'
    privacy_setting:
      description: Privacy setting retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/privacy_setting'
    group_list:
      description: Groups retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/group_list'
    created_group:
      description: Group created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/group'
    group:
      description: Group retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/group'
    group_member_list:
      description: Group members retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/group_member_list'
    community_list:
      description: Communities retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/community_list'
    created_community:
      description: Community created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/community'
    community:
      description: Community retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/community'
    membership_list:
      description: Memberships retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/membership_list'
    created_membership:
      description: Membership created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/membership'
    membership_request_list:
      description: Membership requests retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/membership_request_list'
    created_membership_request:
      description: Membership request created, or the stored response of an earlier request with the same Idempotency-Key
      headers:
        Location:
          $ref: '#/components/headers/location'
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/membership_request'
    membership_request:
      description: Membership request retrieved or changed successfully
      headers:
        ETag:
          $ref: '#/components/headers/etag'
        Idempotent-Replayed:
          $ref: '#/components/headers/idempotent_replayed'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/membership_request'
    current_user:
      description: Current user retrieved successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/current_user'
    user_ref_list:
      description: List of user references retrieved successfully
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: '#/components/schemas/user_ref'
  headers:
    etag:
      description: Current version of the resource; send it as If-Match to change or delete it
      schema:
        type: string
    idempotent_replayed:
      description: |
        Present (`true`) when the response was replayed from an earlier request with the same Idempotency-Key, instead of being executed again
      schema:
        type: string
        enum:
          - 'true'
    location:
      description: URL of the created resource
      schema:
        type: string
        format: uri-reference
