openapi: 3.1.0
info:
  title: Classic Chess API
  version: 1.2.0
  description: Public chess archive reads plus scoped account collection automation.
servers:
  - url: https://classicchess.com
paths:
  /api/v1/players/:
    get:
      summary: Resolve player names
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, maxLength: 80 }
        - name: limit
          in: query
          schema: { type: integer, default: 10, maximum: 50 }
      responses:
        "200":
          description: MasterDB identity and public archive player matches.
  /api/v1/stats/:
    get:
      summary: Player statistics
      parameters:
        - name: player
          in: query
          required: true
          schema: { type: string, maxLength: 80 }
        - name: mode
          in: query
          schema: { type: string, enum: [summary, head_to_head], default: summary }
        - name: opponent
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Player summary or head-to-head record.
  /api/v1/games/:
    get:
      summary: Search MasterDB games
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string, maxLength: 120 }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: page_size
          in: query
          schema: { type: integer, default: 50, maximum: 100 }
      responses:
        "200":
          description: Paginated MasterDB game summaries.
  /api/v1/games/export/:
    get:
      summary: Export MasterDB games
      parameters:
        - name: q
          in: query
          schema: { type: string, maxLength: 120 }
        - name: tokens
          in: query
          schema: { type: string, description: Comma-separated game tokens, max 300. Token lists are limited to 64 KiB. }
        - name: format
          in: query
          schema: { type: string, enum: [pgn, ndjson], default: pgn }
        - name: pgnInJson
          in: query
          schema: { type: boolean, default: true }
      responses:
        "200":
          description: PGN bundle or newline-delimited JSON.
    post:
      summary: Export MasterDB games by token body
      requestBody:
        required: true
        content:
          text/plain:
            schema: { type: string, maxLength: 65536, description: Comma- or newline-separated game tokens, max 300. }
      responses:
        "200":
          description: PGN bundle or newline-delimited JSON.
  /api/v1/games/{token}/:
    get:
      summary: Get one MasterDB game
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string, maxLength: 80 }
      responses:
        "200":
          description: One game with PGN.
  /api/v1/games/{token}/pgn/:
    get:
      summary: Get one MasterDB PGN
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string, maxLength: 120 }
      responses:
        "200":
          description: Raw PGN.
  /api/v1/games/{token}/gif/:
    get:
      summary: Export one MasterDB game as an animated GIF
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string, maxLength: 120 }
        - name: orientation
          in: query
          schema: { type: string, enum: [white, black], default: white }
      responses:
        "200":
          description: Animated GIF.
        "429":
          description: GIF export rate limit exceeded.
  /api/v1/public/players/:
    get:
      summary: List public study-player archives
      parameters:
        - name: q
          in: query
          schema: { type: string, maxLength: 80 }
      responses:
        "200":
          description: Public archive players.
  /api/v1/public/games/:
    get:
      summary: Search player-backed public archive games
      parameters:
        - name: q
          in: query
          schema: { type: string, maxLength: 120 }
        - name: archive_player
          in: query
          schema: { type: string, maxLength: 80 }
        - name: since
          in: query
          schema: { type: integer, minimum: 1400, maximum: 9999 }
        - name: until
          in: query
          schema: { type: integer, minimum: 1400, maximum: 9999 }
        - name: sort
          in: query
          schema: { type: string, enum: [asc, desc], default: asc }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: page_size
          in: query
          schema: { type: integer, default: 50, maximum: 100 }
      responses:
        "200":
          description: Paginated player-backed public archive game summaries. Returned navigable URLs use the shared canonical game record.
  /api/v1/public/games/{game_slug}/:
    get:
      summary: Canonical public archive game detail
      parameters:
        - name: game_slug
          in: path
          required: true
          schema: { type: string, maxLength: 255 }
      responses:
        "200":
          description: Public game detail with archive memberships, PGN, and mainline.
        "404":
          description: Game is not in a public player or event archive.
  /api/v1/public/games/{game_slug}/pgn/:
    get:
      summary: Canonical public archive game PGN
      parameters:
        - name: game_slug
          in: path
          required: true
          schema: { type: string, maxLength: 255 }
      responses:
        "200":
          description: Sanitized public PGN as text/plain.
  /api/v1/public/games/export/:
    get:
      summary: Export player-backed public archive games
      parameters:
        - name: q
          in: query
          schema: { type: string }
        - name: archive_player
          in: query
          schema: { type: string }
        - name: since
          in: query
          schema: { type: integer, minimum: 1400, maximum: 9999 }
        - name: until
          in: query
          schema: { type: integer, minimum: 1400, maximum: 9999 }
        - name: tokens
          in: query
          schema: { type: string, description: "Compatibility export selector: comma-separated player_slug/game_slug tokens, max 300. Token lists are limited to 64 KiB; use result URLs for canonical game navigation." }
        - name: format
          in: query
          schema: { type: string, enum: [pgn, ndjson], default: pgn }
        - name: pgnInJson
          in: query
          schema: { type: boolean, default: true }
      responses:
        "200":
          description: PGN bundle or newline-delimited JSON.
    post:
      summary: Export player-backed public archive games by compatibility token body
      requestBody:
        required: true
        content:
          text/plain:
            schema: { type: string, maxLength: 65536, description: Comma- or newline-separated player_slug/game_slug tokens, max 300. }
      responses:
        "200":
          description: PGN bundle or newline-delimited JSON.
  /api/v1/annotated/books/:
    get:
      summary: List annotated books
      responses:
        "200":
          description: Annotated book metadata.
  /api/v1/annotated/books/{book_slug}/games/:
    get:
      summary: List games in an annotated book
      parameters:
        - name: book_slug
          in: path
          required: true
          schema: { type: string, maxLength: 128 }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: page_size
          in: query
          schema: { type: integer, default: 50, maximum: 100 }
      responses:
        "200":
          description: Paginated annotated games.
  /api/v1/annotated/books/{book_slug}/pgn/:
    get:
      summary: Export all games in an annotated book as PGN
      parameters:
        - name: book_slug
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Raw PGN bundle.
  /api/v1/annotated/books/{book_slug}/games/{game_slug}/:
    get:
      summary: Get one annotated game
      parameters:
        - name: book_slug
          in: path
          required: true
          schema: { type: string }
        - name: game_slug
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: One annotated game with PGN and annotation notes.
  /api/v1/opening-explorer/:
    get:
      summary: Explore an opening position
      parameters:
        - name: fen
          in: query
          schema: { type: string }
        - name: play
          in: query
          schema: { type: string, maxLength: 512, description: Comma-separated UCI moves to play from fen, max 100 moves. }
        - name: moves
          in: query
          schema: { type: integer, default: 12, maximum: 50 }
        - name: topGames
          in: query
          schema: { type: integer, default: 15, maximum: 15 }
        - name: since
          in: query
          schema: { type: integer, minimum: 1400, maximum: 9999 }
          description: Include only games from this year or later when the source supports year indexes.
        - name: until
          in: query
          schema: { type: integer, minimum: 1400, maximum: 9999 }
          description: Include only games from this year or earlier when the source supports year indexes.
        - name: source_type
          in: query
          schema: { type: string }
        - name: source_key
          in: query
          schema: { type: string }
          description: Public source key returned by the sources endpoint; the master database key is masterdb.
      responses:
        "200":
          description: Position totals, moves, and top reference games.
  /api/v1/opening-explorer/sources/:
    get:
      summary: List opening explorer sources
      responses:
        "200":
          description: Available opening explorer builds.
  /api/v1/mobile/home/:
    get:
      summary: Native mobile home payload
      responses:
        "200":
          description: Front-door counts, Game of the Day, and native app action links.
  /api/v1/mobile/android/release/:
    get:
      summary: Native Android release metadata
      responses:
        "200":
          description: Version code/name, APK URL, SHA-256 checksum, size, and release notes.
  /api/v1/account/mobile/login/:
    post:
      summary: Native mobile username/password login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username: { type: string, maxLength: 150 }
                password: { type: string }
                two_factor_code: { type: string, description: Authenticator or recovery code when required. }
                device_name: { type: string, maxLength: 80 }
      responses:
        "201":
          description: Mobile session bearer credential and account summary.
        "400":
          description: Invalid JSON request.
        "401":
          description: Invalid credentials, two_factor_required, or invalid_two_factor.
  /api/v1/account/mobile/logout/:
    post:
      summary: Revoke the current native mobile session
      security:
        - MobileSessionToken: []
      responses:
        "200":
          description: Mobile session revoked.
        "401":
          description: Missing, invalid, expired, or revoked mobile session.
  /api/v1/account/mobile/settings/:
    get:
      summary: Native mobile account settings
      security:
        - MobileSessionToken: []
      responses:
        "200":
          description: Board theme, piece set, and engine preferences from account settings.
        "401":
          description: Missing, invalid, expired, or revoked mobile session.
    post:
      summary: Update native mobile board or engine settings
      security:
        - MobileSessionToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                board_theme: { type: string }
                piece_set: { type: string }
                engine_depth: { type: integer }
                engine_lines: { type: integer, minimum: 1, maximum: 5 }
                engine_eval_bar: { type: boolean }
                engine_enabled: { type: boolean }
      responses:
        "200":
          description: Updated account settings payload.
        "400":
          description: Invalid preference or request body.
        "401":
          description: Missing, invalid, expired, or revoked mobile session.
  /api/v1/account/me/:
    get:
      summary: Authenticated account summary
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [account:read]
      responses:
        "200":
          description: Current account username, email, and email verification state.
        "401":
          description: Missing, invalid, expired, or revoked bearer token.
        "403":
          description: Token is missing the required scope.
  /api/v1/account/collections/:
    get:
      summary: List account collections
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:read]
      responses:
        "200":
          description: Account collections with game counts.
    post:
      summary: Create an account collection
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 120 }
      responses:
        "201":
          description: Created collection.
        "400":
          description: Validation error.
  /api/v1/account/collections/{collection_id}/:
    get:
      summary: List owned collection games
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:read]
      parameters:
        - in: path
          name: collection_id
          required: true
          schema: { type: integer }
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: page_size
          schema: { type: integer, minimum: 1, maximum: 100 }
        - in: query
          name: sort
          schema: { type: string }
      responses:
        "200":
          description: Paginated collection games.
        "401":
          description: Missing or invalid bearer token.
        "403":
          description: Token is missing the required scope.
        "404":
          description: Collection not found for this account.
    patch:
      summary: Rename an owned collection
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:write]
      parameters:
        - in: path
          name: collection_id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 120 }
      responses:
        "200":
          description: Updated collection.
        "400":
          description: Validation error.
        "403":
          description: Token is missing the required scope.
        "404":
          description: Collection not found for this account.
    delete:
      summary: Delete an owned collection
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:write]
      parameters:
        - in: path
          name: collection_id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Collection deleted.
        "403":
          description: Token is missing the required scope.
        "404":
          description: Collection not found for this account.
  /api/v1/account/collections/{collection_id}/items/{item_id}/:
    delete:
      summary: Remove a game from an owned collection
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:write]
      parameters:
        - in: path
          name: collection_id
          required: true
          schema: { type: integer }
        - in: path
          name: item_id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: Game removed from collection.
        "403":
          description: Token is missing the required scope.
        "404":
          description: Collection not found for this account.
  /api/v1/account/collections/import/:
    post:
      summary: Import public archive player games into an account collection
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [source, archive_player]
              properties:
                source: { type: string, enum: [public_player_games] }
                name: { type: string, maxLength: 120, description: Collection name to create or reuse when collection_id is omitted. }
                collection_id: { type: integer, description: Existing account collection ID. }
                archive_player: { type: string, maxLength: 80 }
                since: { type: integer, minimum: 1400, maximum: 9999 }
                until: { type: integer, minimum: 1400, maximum: 9999 }
      responses:
        "200":
          description: Import counts and collection metadata.
        "400":
          description: Validation error.
  /api/v1/account/imported-games/:
    get:
      summary: List or search owned imported games
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:read]
      parameters:
        - in: query
          name: q
          schema: { type: string, maxLength: 120 }
        - in: query
          name: page
          schema: { type: integer, minimum: 1 }
        - in: query
          name: page_size
          schema: { type: integer, minimum: 1, maximum: 100 }
      responses:
        "200":
          description: Paginated imported-game summaries and quota.
    post:
      summary: Import an account-owned game
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                source: { type: string, enum: [pgn, lichess, chesscom] }
                pgn: { type: string }
                remote_url: { type: string, format: uri }
                visibility: { type: string, enum: [private, public] }
      responses:
        "201":
          description: Imported game with PGN and board-ready mainline.
        "400":
          description: Invalid import, quota, provider URL, or request body.
  /api/v1/account/imported-games/{slug}/:
    get:
      summary: Owned imported-game detail
      security:
        - AccountApiToken: []
        - MobileSessionToken: []
      x-required-scopes: [library:read]
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Imported game detail with PGN and board-ready mainline.
        "404":
          description: Imported game not found for this account.
components:
  securitySchemes:
    AccountApiToken:
      type: http
      scheme: bearer
      bearerFormat: Classic Chess personal API token
    MobileSessionToken:
      type: http
      scheme: bearer
      bearerFormat: Classic Chess native mobile session token
