openapi: 3.0.3
info:
  title: Flexslot API
  version: 1.0.0
  description: API documentation for Flexslot decks application
paths:
  /api/public/v1/_probe/partner:
    get:
      operationId: _probe_partner_retrieve
      description: >-
        Returns the calling partner's slug, scopes, game scopes, and rate-limit
        tier.
      summary: Partner probe (HMAC or API key)
      tags:
        - Probes
      security:
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  caller:
                    type: string
                  scopes:
                    type: array
                    items:
                      type: string
                  game_scopes:
                    type: array
                    items:
                      type: string
                  tier:
                    type: string
          description: ''
  /api/public/v1/_probe/public:
    get:
      operationId: _probe_public_retrieve
      description: >-
        Echo the caller. Useful for partners verifying that anonymous access
        works as expected.
      summary: Anonymous probe
      tags:
        - Probes
      security:
        - {}
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  caller:
                    type: string
                  scopes:
                    type: array
                    items:
                      type: string
          description: ''
  /api/public/v1/_probe/user:
    get:
      operationId: _probe_user_retrieve
      description: >-
        Returns the calling user's principal id, scopes, rate-limit tier, and a
        human-readable identity (`user.id` / `username`). `user.id` is the bare
        Firebase id and matches the `author.id` on that user's public decks;
        `user.username` is the handle shown on the consent screen the user
        approved. This doubles as the userinfo endpoint for OAuth2 Bearer
        callers — no extra scope is required. Email is intentionally NOT
        returned: it is not consistently shown to the user before consent (the
        screen shows username, falling back to email only when username is
        empty, and the skip-consent re-authorization path shows neither), so
        exposing it to every bearer caller would leak PII a `decks:read`-only
        partner never had the user approve.
      summary: User PAT probe
      tags:
        - Probes
      security:
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                required:
                  - caller
                  - user
                  - scopes
                  - tier
                properties:
                  caller:
                    type: string
                  user:
                    type: object
                    required:
                      - id
                      - username
                    properties:
                      id:
                        type: string
                      username:
                        type: string
                  scopes:
                    type: array
                    items:
                      type: string
                  tier:
                    type: string
          description: ''
  /api/public/v1/games/{game}/cards/resolve:
    post:
      operationId: games_cards_resolve
      description: >-
        Resolves a list of partner-supplied card identifiers to canonical
        Flexslot cards. Used by partners to validate references before
        submitting deck imports. Strict mode (``?strict=true`` query string OR
        ``"strict": true`` body flag) returns 422 if any identifiers are
        unresolved.
      summary: Resolve card identifiers
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: query
          name: strict
          schema:
            type: boolean
          description: >-
            When true, return 422 FLEXSLOT_CARD_RESOLUTION_FAILED if any
            submitted identifier cannot be resolved.
      tags:
        - Cards
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CardResolveRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CardResolveRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CardResolveRequestRequest'
        required: true
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CardResolveResponse'
          description: ''
  /api/public/v1/games/{game}/decks:
    get:
      operationId: games_decks_list
      description: >-
        Returns the visible decks for a game. Anonymous callers see only Public
        decks. Authenticated callers also see their own private/unlisted decks.
        Archived decks are excluded.


        The ``featured_cards``, ``section_counts`` and ``colors`` fields are
        only present when ``include=summary`` is passed; the default payload
        omits them to stay lean. ``expanded_cards`` is never returned on the
        list — fetch the detail endpoint with ``expand=cards`` for the full
        inventory.
      summary: List public decks for a game
      parameters:
        - in: query
          name: authorId
          schema:
            type: string
          description: Filter by deck author (firebase user id).
        - in: query
          name: createdAfter
          schema:
            type: string
            format: date-time
          description: ISO 8601 lower bound on creation timestamp (inclusive).
        - in: query
          name: createdBefore
          schema:
            type: string
            format: date-time
          description: ISO 8601 upper bound on creation timestamp (inclusive).
        - in: query
          name: editedAfter
          schema:
            type: string
            format: date-time
          description: ISO 8601 lower bound on last-edit timestamp (inclusive).
        - in: query
          name: editedBefore
          schema:
            type: string
            format: date-time
          description: ISO 8601 upper bound on last-edit timestamp (inclusive).
        - in: query
          name: formatId
          schema:
            type: string
            format: uuid
          description: Filter by format id.
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: query
          name: hasGuide
          schema:
            type: boolean
          description: Filter to decks that do/don't have in-deck strategy notes.
        - in: query
          name: hasSideboardGuide
          schema:
            type: boolean
          description: Filter to decks that do/don't have an attached sideboard guide.
        - in: query
          name: hasVideo
          schema:
            type: boolean
          description: Filter to decks that do/don't link a YouTube video.
        - in: query
          name: include
          schema:
            type: string
            enum:
              - summary
          description: >-
            Set to ``summary`` to add ``featured_cards``, ``section_counts`` and
            ``colors`` to each list item — enough to render a rich deck card
            without a per-deck detail fetch. Omitted by default to keep the list
            lean.
        - in: query
          name: isLegal
          schema:
            type: boolean
          description: Filter by format-legality flag.
        - in: query
          name: limit
          schema:
            type: integer
          description: Page size (max 100, default 20).
        - in: query
          name: order
          schema:
            type: string
            enum:
              - asc
              - desc
          description: Sort direction. Defaults to ``desc``.
        - in: query
          name: page
          schema:
            type: integer
          description: 1-indexed page number.
        - in: query
          name: q
          schema:
            type: string
          description: Substring filter on deck name (case-insensitive).
        - in: query
          name: sort
          schema:
            type: string
            enum:
              - createdAt
              - editedAt
              - likes
              - trending
              - views
          description: Sort key. Defaults to ``editedAt``.
      tags:
        - Decks
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckSummaryItem'
          description: ''
    post:
      operationId: games_decks_create
      description: >-
        Creates a deck owned by the authenticated user (PAT) or the partner's
        ``created_by`` user (partner-only HMAC). Provenance fields are optional
        but, when provided, the combination of ``source_provider`` +
        ``source_external_id`` + ``source_external_version`` must be unique per
        user -- duplicates return ``409 FLEXSLOT_VALIDATION_ERROR``.
      summary: Create a deck
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
      tags:
        - Decks
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicDeckCreateRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PublicDeckCreateRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PublicDeckCreateRequestRequest'
        required: true
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckWriteResponse'
          description: ''
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
        '409':
          description: No response body
  /api/public/v1/games/{game}/decks/{id}:
    get:
      operationId: games_decks_retrieve
      description: >-
        Returns a deck with author, format, featured cards, and notes. Pass
        ``expand=cards`` to also receive the per-section card inventory under
        ``expanded_cards``. Anonymous callers receive 404 for non-Public decks
        (existence is not leaked).
      summary: Get a single deck
      parameters:
        - in: query
          name: expand
          schema:
            type: string
            enum:
              - cards
          description: Optional. Pass ``cards`` to include the deck's full card inventory.
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Decks
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckDetail'
          description: ''
    patch:
      operationId: games_decks_update
      description: >-
        Partial update. Only the deck owner may patch their own deck; non-owners
        receive ``404 FLEXSLOT_NOT_FOUND`` so existence is not leaked. Each
        successful save increments the version counter and emits a new history
        record.
      summary: Update a deck (partial)
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Decks
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchedPublicDeckUpdateRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PatchedPublicDeckUpdateRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PatchedPublicDeckUpdateRequestRequest'
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckWriteResponse'
          description: ''
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
    delete:
      operationId: games_decks_delete
      description: >-
        Deletes a deck. Owner-only -- non-owners and unknown ids alike receive
        ``404 FLEXSLOT_NOT_FOUND``. Returns 204 on success.
      summary: Delete a deck
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Decks
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '204':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
  /api/public/v1/games/{game}/decks/{id}/guide:
    get:
      operationId: games_decks_guide_retrieve
      description: >-
        Returns the strategy guide attached to a deck, or ``null`` if the deck
        has no guide. Premium guides are truncated to a 200-char preview for
        non-owner callers without content access; the ``is_truncated`` flag
        signals the response shape.
      summary: Get a deck's strategy guide
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Deck Guides
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                nullable: true
                allOf:
                  - $ref: '#/components/schemas/PublicDeckGuideOutput'
          description: >-
            The guide, or ``null`` when the deck has no attached guide.
            Consumers MUST null-check before accessing fields.
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
    put:
      operationId: games_decks_guide_upsert
      description: >-
        Owner-only create-or-replace. First call against a guideless deck
        returns 201; subsequent calls overwrite and return 200. Non-owners
        receive ``404 FLEXSLOT_NOT_FOUND`` so existence is not leaked.
        Pure-partner HMAC callers are rejected because the write contract
        requires per-user attribution to the deck owner.
      summary: Create or replace a deck's strategy guide
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Deck Guides
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicDeckGuideUpsertRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PublicDeckGuideUpsertRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PublicDeckGuideUpsertRequestRequest'
        required: true
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckGuideOutput'
          description: ''
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckGuideOutput'
          description: ''
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
    delete:
      operationId: games_decks_guide_delete
      description: >-
        Owner-only delete. Non-owners and missing-guide cases both return ``404
        FLEXSLOT_NOT_FOUND`` so existence is not leaked. Returns 204 on success.
      summary: Delete a deck's strategy guide
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Deck Guides
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '204':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
  /api/public/v1/games/{game}/decks/{id}/like:
    post:
      operationId: games_decks_like
      description: >-
        Idempotent toggle: first call likes, second call unlikes. Requires
        per-user attribution (PAT or partner-on-behalf-of-user) -- pure partner
        HMAC callers receive ``401 FLEXSLOT_AUTHENTICATION_REQUIRED`` because
        there is no user to attribute the like to.
      summary: Toggle the caller's like on a deck
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Decks
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckLikeResponse'
          description: ''
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
  /api/public/v1/games/{game}/decks/{id}/rollback:
    post:
      operationId: games_decks_rollback
      description: >-
        Restores the deck's field state from the historical version at the given
        1-based ``version_number`` (matching the order returned by ``GET
        /decks/<id>/versions``). The rollback is a forward-saving event -- the
        historical chain GROWS by one record. Owners only; non-owners and
        out-of-range versions alike return ``404 FLEXSLOT_NOT_FOUND``.
      summary: Roll a deck back to a historical version
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Decks
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicDeckRollbackRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PublicDeckRollbackRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PublicDeckRollbackRequestRequest'
        required: true
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckWriteResponse'
          description: ''
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
  /api/public/v1/games/{game}/decks/{id}/versions:
    get:
      operationId: games_decks_versions_list
      description: >-
        Returns the deck's full edit history as ``HistoricalDeck`` records,
        oldest first. Position 1 is the create event; the final entry is the
        most recent revision. Visibility rules of the underlying deck are
        respected — anonymous callers receive 404 for non-Public decks.
      summary: List historical versions of a deck
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
        - in: query
          name: limit
          schema:
            type: integer
        - in: query
          name: page
          schema:
            type: integer
      tags:
        - Decks
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckVersion'
          description: ''
  /api/public/v1/games/{game}/decks/{id}/versions/{n}:
    get:
      operationId: games_decks_versions_retrieve
      description: >-
        Returns the nth-oldest historical record. Position 1 is the create
        event. Out-of-range ``n`` returns 404 NOT_FOUND. Visibility rules of the
        underlying deck are respected.
      summary: Get a single historical version of a deck
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: 'n'
          schema:
            type: integer
          required: true
      tags:
        - Decks
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckVersion'
          description: ''
  /api/public/v1/games/{game}/decks/{id}/views:
    post:
      operationId: games_decks_view_increment
      description: >-
        Increments ``Deck.pageviews`` with a per-visitor 1-hour dedupe window.
        The visitor key is the caller's ``firebase_id`` if authenticated,
        otherwise the request IP. Returns 200 in both the increment and the
        dedupe-skip cases -- no information is leaked about whether the counter
        actually advanced.
      summary: Register a pageview for a deck
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Decks
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckViewIncrementResponse'
          description: ''
        '404':
          description: No response body
  /api/public/v1/games/{game}/decks/export/text:
    post:
      operationId: games_decks_export_text
      description: >-
        Renders a deck as an MTG Arena / Moxfield-compatible text list. Pass
        ``deck_id`` to export an existing Flexslot deck (visibility / IDOR rules
        apply) OR ``deck_payload`` to render an inline deck supplied by the
        partner. Inline-payload requests surface any card-resolution warnings
        via the ``X-Flexslot-Export-Warnings`` response header. Responses are
        cached for 24h keyed by ``(deck_id, deck.updated_at)`` -- inline-payload
        renders bypass the cache.
      summary: Export a deck as plain text
      parameters:
        - in: path
          name: game
          schema:
            type: string
          description: Game slug (e.g. ``magic-the-gathering``).
          required: true
      tags:
        - Exports
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicDeckExportRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PublicDeckExportRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PublicDeckExportRequestRequest'
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: string
                description: Plain-text deck list.
          description: ''
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
        '429':
          description: No response body
  /api/public/v1/games/{game}/decks/my:
    get:
      operationId: games_decks_my_retrieve
      description: >-
        Returns the authenticated user's decks for a game across all
        visibilities (Public, Private, Unlisted), excluding archived. Requires
        PAT auth — partner-only callers receive 401 because there is no user to
        scope to.


        The ``featured_cards``, ``section_counts`` and ``colors`` fields are
        only present when ``include=summary`` is passed — same opt-in and same
        shape as ``/decks``, and the enrichment runs for the caller's private
        decks too. ``expanded_cards`` is never returned on the list — fetch the
        detail endpoint with ``expand=cards`` for the full inventory.
      summary: List the caller's own decks
      parameters:
        - in: query
          name: authorId
          schema:
            type: string
          description: Filter by deck author (firebase user id).
        - in: query
          name: createdAfter
          schema:
            type: string
            format: date-time
          description: ISO 8601 lower bound on creation timestamp (inclusive).
        - in: query
          name: createdBefore
          schema:
            type: string
            format: date-time
          description: ISO 8601 upper bound on creation timestamp (inclusive).
        - in: query
          name: editedAfter
          schema:
            type: string
            format: date-time
          description: ISO 8601 lower bound on last-edit timestamp (inclusive).
        - in: query
          name: editedBefore
          schema:
            type: string
            format: date-time
          description: ISO 8601 upper bound on last-edit timestamp (inclusive).
        - in: query
          name: formatId
          schema:
            type: string
            format: uuid
          description: Filter by format id.
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: query
          name: hasGuide
          schema:
            type: boolean
          description: Filter to decks that do/don't have in-deck strategy notes.
        - in: query
          name: hasSideboardGuide
          schema:
            type: boolean
          description: Filter to decks that do/don't have an attached sideboard guide.
        - in: query
          name: hasVideo
          schema:
            type: boolean
          description: Filter to decks that do/don't link a YouTube video.
        - in: query
          name: include
          schema:
            type: string
            enum:
              - summary
          description: >-
            Set to ``summary`` to add ``featured_cards``, ``section_counts`` and
            ``colors`` to each list item — enough to render a rich deck card
            without a per-deck detail fetch. Omitted by default to keep the list
            lean.
        - in: query
          name: isLegal
          schema:
            type: boolean
          description: Filter by format-legality flag.
        - in: query
          name: limit
          schema:
            type: integer
          description: Page size (max 100, default 20).
        - in: query
          name: order
          schema:
            type: string
            enum:
              - asc
              - desc
          description: Sort direction. Defaults to ``desc``.
        - in: query
          name: page
          schema:
            type: integer
          description: 1-indexed page number.
        - in: query
          name: q
          schema:
            type: string
          description: Substring filter on deck name (case-insensitive).
        - in: query
          name: sort
          schema:
            type: string
            enum:
              - createdAt
              - editedAt
              - likes
              - trending
              - views
          description: Sort key. Defaults to ``editedAt``.
      tags:
        - Decks
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicDeckSummaryItem'
          description: ''
  /api/public/v1/games/{game}/sideboard-guides:
    get:
      operationId: games_sideboard_guides_list
      description: >-
        Returns the visible sideboard guides for a game. Anonymous callers are
        rejected with 401 (the endpoint requires the ``sideboards:read`` scope).
        Authenticated callers see public guides plus their own private guides.
        Archived guides are excluded.


        The ``featured_cards``, ``colors`` and ``matchup_count`` fields are only
        present when ``include=summary`` is passed; the default payload omits
        them to stay lean.
      summary: List public sideboard guides for a game
      parameters:
        - in: query
          name: archetypeId
          schema:
            type: string
          description: >-
            Case-insensitive substring filter on related matchup
            ``opponent_archetype``.
        - in: query
          name: authorId
          schema:
            type: string
          description: Filter by guide author (firebase user id).
        - in: query
          name: createdAfter
          schema:
            type: string
            format: date-time
          description: ISO 8601 lower bound on creation timestamp (inclusive).
        - in: query
          name: createdBefore
          schema:
            type: string
            format: date-time
          description: ISO 8601 upper bound on creation timestamp (inclusive).
        - in: query
          name: deckId
          schema:
            type: string
            format: uuid
          description: Filter to guides whose m2m membership includes this deck.
        - in: query
          name: editedAfter
          schema:
            type: string
            format: date-time
          description: ISO 8601 lower bound on last-edit timestamp (inclusive).
        - in: query
          name: editedBefore
          schema:
            type: string
            format: date-time
          description: ISO 8601 upper bound on last-edit timestamp (inclusive).
        - in: query
          name: format
          schema:
            type: string
          description: >-
            Case-insensitive substring filter on the guide's plain ``format``
            string. Guides have no format FK, so this is a string filter named
            ``format`` — a deliberate divergence from the deck list's
            ``formatId``.
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: query
          name: hasVideo
          schema:
            type: boolean
          description: Filter to guides that do/don't link a YouTube video.
        - in: query
          name: include
          schema:
            type: string
            enum:
              - summary
          description: >-
            Set to ``summary`` to add ``featured_cards``, ``colors`` and
            ``matchup_count`` to each list item — enough to render a rich guide
            card without a per-guide detail fetch. Omitted by default to keep
            the list lean.
        - in: query
          name: limit
          schema:
            type: integer
          description: Page size (max 100, default 20).
        - in: query
          name: order
          schema:
            type: string
            enum:
              - asc
              - desc
          description: Sort direction. Defaults to ``desc``.
        - in: query
          name: page
          schema:
            type: integer
          description: 1-indexed page number.
        - in: query
          name: q
          schema:
            type: string
          description: Substring filter on guide name (case-insensitive).
        - in: query
          name: sort
          schema:
            type: string
            enum:
              - createdAt
              - editedAt
              - likes
              - views
          description: Sort key. Defaults to ``editedAt``.
      tags:
        - Sideboard Guides
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSideboardGuideSummaryItem'
          description: ''
    post:
      operationId: games_sideboard_guides_create
      description: >-
        Creates a sideboard guide for the authenticated user. The caller must
        own the deck referenced by ``deck_id``; otherwise the request is
        rejected with ``403 FLEXSLOT_NOT_DECK_OWNER``. Nested ``matchups`` and
        ``attached_deck_ids`` arrays are optional. The primary deck attachment
        is auto-created by the post_save signal.
      summary: Create a sideboard guide
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
      tags:
        - Sideboard Guides
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicSideboardGuideCreateRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PublicSideboardGuideCreateRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/PublicSideboardGuideCreateRequestRequest'
        required: true
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSideboardGuideDetail'
          description: ''
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
  /api/public/v1/games/{game}/sideboard-guides/{id}:
    get:
      operationId: games_sideboard_guides_retrieve
      description: >-
        Returns a sideboard guide with nested matchups and the m2m deck
        membership. Non-owners receive 404 for non-Public guides (existence is
        not leaked).
      summary: Get a single sideboard guide
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Sideboard Guides
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSideboardGuideDetail'
          description: ''
    patch:
      operationId: games_sideboard_guides_update
      description: >-
        Partial update. Only the guide owner may patch their own guide;
        non-owners receive ``404 FLEXSLOT_NOT_FOUND``. Omitting a field leaves
        it untouched.
      summary: Update a sideboard guide (partial)
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Sideboard Guides
      requestBody:
        content:
          application/json:
            schema:
              $ref: >-
                #/components/schemas/PatchedPublicSideboardGuideUpdateRequestRequest
          application/x-www-form-urlencoded:
            schema:
              $ref: >-
                #/components/schemas/PatchedPublicSideboardGuideUpdateRequestRequest
          multipart/form-data:
            schema:
              $ref: >-
                #/components/schemas/PatchedPublicSideboardGuideUpdateRequestRequest
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSideboardGuideDetail'
          description: ''
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
    delete:
      operationId: games_sideboard_guides_delete
      description: >-
        Deletes a sideboard guide. Owner-only -- non-owners and unknown ids
        alike receive ``404 FLEXSLOT_NOT_FOUND``. Cascade removes matchups +
        attachment rows.
      summary: Delete a sideboard guide
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Sideboard Guides
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '204':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
  /api/public/v1/games/{game}/sideboard-guides/{id}/decks:
    post:
      operationId: games_sideboard_guides_attach_deck
      description: >-
        Attaches a deck (other than the primary) to the guide via the
        ``SideboardGuideDeckAttachment`` through-table. The caller must own both
        the guide and the deck. Cross-game attempts return ``400
        FLEXSLOT_VALIDATION_ERROR``. Re-attaching an already-attached deck is a
        no-op.
      summary: Attach a deck to a sideboard guide
      parameters:
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Sideboard Guides
      requestBody:
        content:
          application/json:
            schema:
              $ref: >-
                #/components/schemas/PublicSideboardGuideAttachDeckRequestRequest
          application/x-www-form-urlencoded:
            schema:
              $ref: >-
                #/components/schemas/PublicSideboardGuideAttachDeckRequestRequest
          multipart/form-data:
            schema:
              $ref: >-
                #/components/schemas/PublicSideboardGuideAttachDeckRequestRequest
        required: true
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSideboardGuideDetail'
          description: ''
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
  /api/public/v1/games/{game}/sideboard-guides/{id}/decks/{deck_id}:
    delete:
      operationId: games_sideboard_guides_detach_deck
      description: >-
        Removes a ``SideboardGuideDeckAttachment`` row. Owner-only. The primary
        deck cannot be detached -- attempts return ``400
        FLEXSLOT_VALIDATION_ERROR``.
      summary: Detach a deck from a sideboard guide
      parameters:
        - in: path
          name: deck_id
          schema:
            type: string
            format: uuid
          required: true
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      tags:
        - Sideboard Guides
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '204':
          description: No response body
        '400':
          description: No response body
        '401':
          description: No response body
        '403':
          description: No response body
        '404':
          description: No response body
  /api/public/v1/games/{game}/sideboard-guides/my:
    get:
      operationId: games_sideboard_guides_my
      description: >-
        Returns the authenticated user's sideboard guides for a game across all
        visibilities, excluding archived. Requires PAT auth — partner-only
        callers receive 401 because there is no user to scope to.


        The ``featured_cards``, ``colors`` and ``matchup_count`` fields are only
        present when ``include=summary`` is passed; the default payload omits
        them to stay lean.
      summary: List the caller's own sideboard guides
      parameters:
        - in: query
          name: archetypeId
          schema:
            type: string
          description: >-
            Case-insensitive substring filter on related matchup
            ``opponent_archetype``.
        - in: query
          name: createdAfter
          schema:
            type: string
            format: date-time
          description: ISO 8601 lower bound on creation timestamp (inclusive).
        - in: query
          name: createdBefore
          schema:
            type: string
            format: date-time
          description: ISO 8601 upper bound on creation timestamp (inclusive).
        - in: query
          name: deckId
          schema:
            type: string
            format: uuid
          description: Filter to guides whose m2m membership includes this deck.
        - in: query
          name: editedAfter
          schema:
            type: string
            format: date-time
          description: ISO 8601 lower bound on last-edit timestamp (inclusive).
        - in: query
          name: editedBefore
          schema:
            type: string
            format: date-time
          description: ISO 8601 upper bound on last-edit timestamp (inclusive).
        - in: query
          name: format
          schema:
            type: string
          description: >-
            Case-insensitive substring filter on the guide's plain ``format``
            string. Guides have no format FK, so this is a string filter named
            ``format`` — a deliberate divergence from the deck list's
            ``formatId``.
        - in: path
          name: game
          schema:
            type: string
          required: true
        - in: query
          name: hasVideo
          schema:
            type: boolean
          description: Filter to guides that do/don't link a YouTube video.
        - in: query
          name: include
          schema:
            type: string
            enum:
              - summary
          description: >-
            Set to ``summary`` to add ``featured_cards``, ``colors`` and
            ``matchup_count`` to each list item — enough to render a rich guide
            card without a per-guide detail fetch. Omitted by default to keep
            the list lean.
        - in: query
          name: limit
          schema:
            type: integer
          description: Page size (max 100, default 20).
        - in: query
          name: order
          schema:
            type: string
            enum:
              - asc
              - desc
          description: Sort direction. Defaults to ``desc``.
        - in: query
          name: page
          schema:
            type: integer
          description: 1-indexed page number.
        - in: query
          name: q
          schema:
            type: string
          description: Substring filter on guide name (case-insensitive).
        - in: query
          name: sort
          schema:
            type: string
            enum:
              - createdAt
              - editedAt
              - likes
              - views
          description: Sort key. Defaults to ``editedAt``.
      tags:
        - Sideboard Guides
      security:
        - PartnerHmac: []
          PartnerSlug: []
          PersonalAccessToken: []
        - PartnerHmac: []
          PartnerSlug: []
        - ApiKey: []
        - PersonalAccessToken: []
        - OAuth2DPoP: []
          OAuth2DPoPProof: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicSideboardGuideSummaryItem'
          description: ''
  /api/public/v1/health:
    get:
      operationId: health_retrieve
      description: >-
        Returns 200 OK if the public API is reachable. Used by partner
        monitoring.
      summary: Public API liveness check
      tags:
        - Probes
      security:
        - {}
      responses:
        '200':
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  version:
                    type: string
          description: ''
  /api/public/v1/me/access-tokens/:
    get:
      operationId: me_access_tokens_self
      description: >-
        Returns the metadata of the Personal Access Token used to authenticate
        this request. Never returns the user's other tokens — this is
        intentionally narrower than the internal Settings UI list. Never exposes
        the token hash or plaintext.
      summary: Read metadata for the calling PAT
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PatOutput'
          description: ''
  /api/public/v1/me/partner:
    get:
      operationId: me_partner_detail
      description: >-
        Returns the Partner row owned by the user attached to the calling PAT.
        Never returns the signing-secret hash or the API-key hash. Use this to
        read scopes, allowed_origins, rate_limit_tier, etc. before deciding
        whether to issue a rotation or scope-change request.
      summary: Read the calling user's partner row
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerSelf'
          description: ''
  /api/public/v1/me/partner/allowed-origins:
    post:
      operationId: me_partner_allowed_origins_add
      description: |-
        Manage the partner's allowed-origins list.

        POST adds an origin (idempotent — adding an existing origin is a no-op).
        DELETE with ?origin=... removes one (404 if not present, so the partner
        knows their request had no effect).

        Origins gate write-path CORS — adding a new app domain shouldn't require
        a Flexslot admin to do anything. This is the most common partner
        self-service action.
      summary: Add an allowed origin to the partner
      tags:
        - Me
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/_OriginRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/_OriginRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/_OriginRequestRequest'
        required: true
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerAllowedOriginsResponse'
          description: ''
    delete:
      operationId: me_partner_allowed_origins_remove
      description: |-
        Manage the partner's allowed-origins list.

        POST adds an origin (idempotent — adding an existing origin is a no-op).
        DELETE with ?origin=... removes one (404 if not present, so the partner
        knows their request had no effect).

        Origins gate write-path CORS — adding a new app domain shouldn't require
        a Flexslot admin to do anything. This is the most common partner
        self-service action.
      summary: Remove an allowed origin from the partner
      parameters:
        - in: query
          name: origin
          schema:
            type: string
          description: Origin URL to remove. Returns 404 if not present.
          required: true
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerAllowedOriginsRemoveResponse'
          description: ''
  /api/public/v1/me/partner/oauth2-clients:
    get:
      operationId: me_partner_oauth2_clients_list
      description: >-
        Returns the OAuth2 clients owned by the PAT user's partner, newest
        first, excluding revoked clients. Never returns the client secret hash.
      summary: List the calling partner's OAuth2 clients
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/OAuth2ClientSelf'
          description: ''
    post:
      operationId: me_partner_oauth2_clients_create
      description: >-
        Creates an OAuth2 client for the PAT user's partner. Confidential
        clients receive a plaintext secret in the response exactly ONCE; public
        clients receive an empty secret (PKCE only). Subject to a per-partner
        client limit.
      summary: Create a new OAuth2 client
      tags:
        - Me
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuth2ClientCreateRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OAuth2ClientCreateRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/OAuth2ClientCreateRequestRequest'
        required: true
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2ClientSecretResponse'
          description: ''
  /api/public/v1/me/partner/oauth2-clients/{client_id}:
    get:
      operationId: me_partner_oauth2_clients_detail
      description: >-
        Returns a single OAuth2 client owned by the PAT user's partner. 404 if
        not owned.
      summary: Read one OAuth2 client
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2ClientSelf'
          description: ''
    delete:
      operationId: me_partner_oauth2_clients_revoke
      description: >-
        Revokes the client and cascade-revokes every grant and access/refresh
        token issued under it. Returns 204. 404 if the client is not owned by
        the caller's partner.
      summary: Revoke an OAuth2 client (cascade grants + tokens)
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '204':
          description: No response body
  /api/public/v1/me/partner/oauth2-clients/{client_id}/rotate-secret:
    post:
      operationId: me_partner_oauth2_clients_rotate_secret
      description: >-
        Mints a new client secret and returns the plaintext exactly ONCE. Only
        valid for confidential clients — rotating a public client returns 400.
        404 if the client is not owned by the caller's partner.
      summary: Rotate a confidential OAuth2 client's secret
      parameters:
        - in: path
          name: client_id
          schema:
            type: string
          required: true
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2ClientSecretResponse'
          description: ''
  /api/public/v1/me/partner/rotate-api-key:
    post:
      operationId: me_partner_rotate_api_key
      description: >-
        Rotates the X-API-Key credential for the calling user's partner. If the
        partner has no existing API key (predates the FSG-582 self-service
        surface), this mints the first one. Returns the new plaintext ONCE —
        copy it immediately.
      summary: Rotate (or generate) the partner API key
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRotateApiKeyResponse'
          description: ''
  /api/public/v1/me/partner/rotate-signing-secret:
    post:
      operationId: me_partner_rotate_signing_secret
      description: >-
        Rotates the HMAC signing secret for the calling user's partner. Returns
        the new plaintext ONCE — copy it immediately; we don't persist plaintext
        anywhere. The previous secret is invalidated as soon as this request
        returns 200.
      summary: Rotate the partner signing secret
      tags:
        - Me
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PartnerRotateSigningSecretResponse'
          description: ''
  /api/public/v1/me/partner/scope-requests:
    post:
      operationId: me_partner_scope_request_create
      description: >-
        Submit a scope change request for admin review. Adding scopes is
        higher-risk than CORS-origin self-service, so requests go through
        Flexslot admin instead of taking effect immediately. The partner is
        notified of approval/rejection by email.
      summary: Request additional scopes or game-scopes
      tags:
        - Me
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/_ScopeChangeRequestRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/_ScopeChangeRequestRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/_ScopeChangeRequestRequest'
        required: true
      security:
        - PersonalAccessToken: []
        - OAuth2Bearer: []
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/_ScopeChangeResponse'
          description: ''
  /api/public/v1/oauth/introspect:
    post:
      operationId: oauth2_introspect
      description: >-
        Returns the active state and metadata of a token previously issued by
        this server. Only the issuing client may introspect its own tokens — all
        other cases return ``{"active": false}``.
      summary: OAuth 2.0 Token Introspection (RFC 7662)
      tags:
        - OAuth2
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OAuth2IntrospectRequestRequest'
        required: true
      security:
        - {}
      responses:
        '200':
          description: RFC 7662 §2.2 introspection response.
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2IntrospectError400'
          description: RFC 6749 §5.2 error envelope.
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2IntrospectError401'
          description: RFC 6749 §5.2 invalid_client error.
  /api/public/v1/oauth/revoke:
    post:
      operationId: oauth2_revoke
      description: >-
        Invalidates a token (access or refresh) previously issued by this
        server. Always returns HTTP 200 with an empty body — unknown /
        other-client / already-revoked tokens are silently accepted to avoid
        leaking existence.
      summary: OAuth 2.0 Token Revocation (RFC 7009)
      tags:
        - OAuth2
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OAuth2RevokeRequestRequest'
        required: true
      security:
        - {}
      responses:
        '200':
          description: Revocation accepted. Empty body.
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2RevokeError400'
          description: RFC 6749 §5.2 error envelope.
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2RevokeError401'
          description: RFC 6749 §5.2 invalid_client error.
  /api/public/v1/oauth/token:
    post:
      operationId: oauth2_token
      description: >-
        Exchanges an authorization code for an access + refresh token pair
        (``grant_type=authorization_code``) or rotates an existing refresh token
        (``grant_type=refresh_token``). Returns the RFC 6749 §5.1 JSON shape on
        success or the §5.2 error shape on failure.
      summary: OAuth2 token endpoint (RFC 6749 §4.1.3 + §6)
      tags:
        - OAuth2
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OAuth2TokenRequestRequest'
        required: true
      security:
        - {}
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2TokenResponse'
          description: ''
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2TokenError400'
          description: RFC 6749 §5.2 error.
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2TokenError401'
          description: RFC 6749 §5.2 invalid_client error.
components:
  schemas:
    AllowedScopesEnum:
      enum:
        - cards:read
        - decks:read
        - decks:write
        - exports:read
        - guides:read
        - guides:write
        - sideboards:read
        - sideboards:write
      type: string
      description: |-
        * `cards:read` - cards:read
        * `decks:read` - decks:read
        * `decks:write` - decks:write
        * `exports:read` - exports:read
        * `guides:read` - guides:read
        * `guides:write` - guides:write
        * `sideboards:read` - sideboards:read
        * `sideboards:write` - sideboards:write
    CardIdentifierInputRequest:
      type: object
      properties:
        scryfall_id:
          type: string
        mtgjson_id:
          type: string
        tcgplayer_id:
          type: string
        cardId:
          type: string
        variantId:
          type: string
        name:
          type: string
        set:
          type: string
        collector_number:
          type: string
    CardResolveRequestRequest:
      type: object
      properties:
        identifiers:
          type: array
          items:
            $ref: '#/components/schemas/CardIdentifierInputRequest'
        format:
          type: string
          nullable: true
        strict:
          type: boolean
          default: false
      required:
        - identifiers
    CardResolveResponse:
      type: object
      properties:
        resolved:
          type: array
          items:
            type: object
        unresolved:
          type: array
          items:
            type: object
      required:
        - resolved
        - unresolved
    ContentTypeEnum:
      enum:
        - markdown
        - tiptap
      type: string
      description: |-
        * `markdown` - markdown
        * `tiptap` - tiptap
    DecisionEnum:
      enum:
        - allow
        - deny
      type: string
      description: |-
        * `allow` - allow
        * `deny` - deny
    DeckSourceProviderEnum:
      enum:
        - piltover
        - moxfield
        - archidekt
        - cube_cobra
        - mtggoldfish
        - tappedout
        - deckstats
        - edhrec
        - mtg_arena
        - mtgo
        - forge
        - carde_io
        - manual
      type: string
      description: |-
        * `piltover` - Piltover
        * `moxfield` - Moxfield
        * `archidekt` - Archidekt
        * `cube_cobra` - Cube Cobra
        * `mtggoldfish` - MTGGoldfish
        * `tappedout` - TappedOut
        * `deckstats` - Deckstats
        * `edhrec` - EDHREC
        * `mtg_arena` - MTG Arena
        * `mtgo` - MTGO
        * `forge` - Forge
        * `carde_io` - Carde.io
        * `manual` - Manual
      x-enum-varnames:
        - PILTOVER
        - MOXFIELD
        - ARCHIDEKT
        - CUBE_COBRA
        - MTGGOLDFISH
        - TAPPEDOUT
        - DECKSTATS
        - EDHREC
        - MTG_ARENA
        - MTGO
        - FORGE
        - CARDE_IO
        - MANUAL
      x-enum-descriptions:
        - Piltover
        - Moxfield
        - Archidekt
        - Cube Cobra
        - MTGGoldfish
        - TappedOut
        - Deckstats
        - EDHREC
        - MTG Arena
        - MTGO
        - Forge
        - Carde.io
        - Manual
    DifficultyEnum:
      enum:
        - VERY_EASY
        - EASY
        - MEDIUM
        - HARD
        - VERY_HARD
      type: string
      description: |-
        * `VERY_EASY` - VERY_EASY
        * `EASY` - EASY
        * `MEDIUM` - MEDIUM
        * `HARD` - HARD
        * `VERY_HARD` - VERY_HARD
    FeaturedCard:
      type: object
      description: |-
        Structured featured/special card entry — game-agnostic.

        Covers MTG commander/partner/companion and Riftbound legend/champion/
        battlefield uniformly. ``slot`` is the base type (group key);
        ``slotKey`` preserves the ordinal so ``battlefield_1`` and
        ``battlefield_2`` stay distinguishable.
      properties:
        slot:
          type: string
          description: Base special-card type, e.g. 'legend', 'battlefield'.
        slotKey:
          type: string
          description: Raw type incl. ordinal, e.g. 'battlefield_1'.
        label:
          type: string
          description: Human-readable label, e.g. 'Battlefield'.
        sortOrder:
          type: integer
          description: Order within the slot.
        foil:
          type: boolean
        cardId:
          type: string
        name:
          type: string
        imageUrl:
          type: string
          nullable: true
          description: >-
            Absolute CDN URL to the card's full-art image. null when the card
            has no stored image.
        type_line:
          type: string
          nullable: true
        set:
          type: string
          nullable: true
        collector_number:
          type: string
          nullable: true
      required:
        - cardId
        - collector_number
        - foil
        - imageUrl
        - label
        - name
        - set
        - slot
        - slotKey
        - sortOrder
        - type_line
    GrantTypesEnum:
      enum:
        - authorization_code
        - refresh_token
      type: string
      description: |-
        * `authorization_code` - authorization_code
        * `refresh_token` - refresh_token
    OAuth2AuthorizeDecisionRequestRequest:
      type: object
      description: |-
        Schema for ``POST /api/public/v1/oauth/authorize``.

        Carries every parameter from the original ``/authorize`` GET so the
        server can re-validate from scratch — the client cannot bypass
        validation by mutating params between the preview and the POST.
      properties:
        decision:
          $ref: '#/components/schemas/DecisionEnum'
        client_id:
          type: string
          minLength: 1
        redirect_uri:
          type: string
          minLength: 1
        response_type:
          type: string
          minLength: 1
        scope:
          type: string
        state:
          type: string
          default: ''
        code_challenge:
          type: string
          minLength: 1
        code_challenge_method:
          type: string
          minLength: 1
        prompt:
          type: string
          nullable: true
      required:
        - client_id
        - code_challenge
        - code_challenge_method
        - decision
        - redirect_uri
        - response_type
        - scope
    OAuth2AuthorizeDecisionResponse:
      type: object
      description: >-
        The decision endpoint always returns a single ``redirect`` URL.


        Allow → ``<partner-redirect-uri>?code=...&state=...&iss=...``

        Deny →
        ``<partner-redirect-uri>?error=access_denied&...&state=...&iss=...``

        Recoverable validation failure → error redirect (same shape).


        "Do not redirect" failures (invalid client/redirect_uri) raise the

        canonical 4xx envelope and never produce a body that conforms to

        this serializer.
      properties:
        redirect:
          type: string
      required:
        - redirect
    OAuth2AuthorizePreviewClient:
      type: object
      properties:
        client_id:
          type: string
        application_name:
          type: string
        application_logo_url:
          type: string
        application_url:
          type: string
        privacy_policy_url:
          type: string
        tos_url:
          type: string
      required:
        - application_logo_url
        - application_name
        - application_url
        - client_id
        - privacy_policy_url
        - tos_url
    OAuth2AuthorizePreviewResponse:
      type: object
      description: |-
        Discriminated union of the four preview-endpoint outcomes.

        All fields are optional — exactly one branch is populated per response:

        - ``error_redirect`` (string URL): recoverable validation failure;
          SPA navigates to it.
        - ``auto_grant_redirect`` (string URL): skip-consent case; SPA
          navigates immediately.
        - ``client`` + ``scopes`` + ``user`` (all three): render consent UI.

        The 4xx "do not redirect" branch uses the canonical
        ``{error, message, details}`` envelope from the exception handler
        and never reaches this serializer.
      properties:
        error_redirect:
          type: string
        auto_grant_redirect:
          type: string
        client:
          $ref: '#/components/schemas/OAuth2AuthorizePreviewClient'
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/OAuth2AuthorizePreviewScope'
        user:
          $ref: '#/components/schemas/OAuth2AuthorizePreviewUser'
    OAuth2AuthorizePreviewScope:
      type: object
      properties:
        slug:
          type: string
        label:
          type: string
        description:
          type: string
      required:
        - description
        - label
        - slug
    OAuth2AuthorizePreviewUser:
      type: object
      properties:
        username:
          type: string
        email:
          type: string
      required:
        - email
        - username
    OAuth2ClientCreateRequestClientTypeEnum:
      enum:
        - confidential
        - public
      type: string
      description: |-
        * `confidential` - confidential
        * `public` - public
    OAuth2ClientCreateRequestRequest:
      type: object
      properties:
        application_name:
          type: string
          minLength: 1
          maxLength: 255
        client_type:
          allOf:
            - $ref: '#/components/schemas/OAuth2ClientCreateRequestClientTypeEnum'
          default: confidential
        redirect_uris:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 512
        allowed_scopes:
          type: array
          items:
            $ref: '#/components/schemas/AllowedScopesEnum'
        grant_types:
          type: array
          items:
            $ref: '#/components/schemas/GrantTypesEnum'
          default:
            - authorization_code
            - refresh_token
        application_url:
          type: string
          default: ''
          maxLength: 512
        application_logo_url:
          type: string
          default: ''
          maxLength: 512
        privacy_policy_url:
          type: string
          default: ''
          maxLength: 512
        tos_url:
          type: string
          default: ''
          maxLength: 512
      required:
        - allowed_scopes
        - application_name
        - redirect_uris
    OAuth2ClientSecretResponse:
      type: object
      properties:
        client:
          $ref: '#/components/schemas/OAuth2ClientSelf'
        plaintext_client_secret:
          type: string
        secret_rotated_at:
          type: string
          format: date-time
          nullable: true
      required:
        - client
        - plaintext_client_secret
        - secret_rotated_at
    OAuth2ClientSelf:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        client_id:
          type: string
          readOnly: true
        client_type:
          allOf:
            - $ref: '#/components/schemas/OAuth2ClientTypeEnum'
          readOnly: true
        application_name:
          type: string
          readOnly: true
        application_url:
          type: string
          format: uri
          readOnly: true
        application_logo_url:
          type: string
          format: uri
          readOnly: true
        privacy_policy_url:
          type: string
          format: uri
          readOnly: true
        tos_url:
          type: string
          format: uri
          readOnly: true
        redirect_uris:
          type: array
          items:
            type: string
            maxLength: 512
          readOnly: true
        allowed_scopes:
          type: array
          items:
            type: string
            maxLength: 64
          readOnly: true
        grant_types:
          type: array
          items:
            type: string
            maxLength: 32
          readOnly: true
        require_pkce:
          type: boolean
          readOnly: true
        dpop_bound_access_tokens:
          type: boolean
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/OAuth2ClientStatusEnum'
          readOnly: true
        secret_rotated_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        last_used_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
      required:
        - allowed_scopes
        - application_logo_url
        - application_name
        - application_url
        - client_id
        - client_type
        - created_at
        - dpop_bound_access_tokens
        - grant_types
        - id
        - last_used_at
        - privacy_policy_url
        - redirect_uris
        - require_pkce
        - secret_rotated_at
        - status
        - tos_url
        - updated_at
    OAuth2ClientStatusEnum:
      enum:
        - active
        - suspended
        - revoked
      type: string
      description: |-
        * `active` - Active
        * `suspended` - Suspended
        * `revoked` - Revoked
      x-enum-varnames:
        - ACTIVE
        - SUSPENDED
        - REVOKED
      x-enum-descriptions:
        - Active
        - Suspended
        - Revoked
    OAuth2ClientTypeEnum:
      enum:
        - confidential
        - public
      type: string
      description: |-
        * `confidential` - Confidential
        * `public` - Public
      x-enum-varnames:
        - CONFIDENTIAL
        - PUBLIC
      x-enum-descriptions:
        - Confidential
        - Public
    OAuth2IntrospectError400:
      type: object
      properties:
        error:
          type: string
        error_description:
          type: string
      required:
        - error
        - error_description
    OAuth2IntrospectError401:
      type: object
      properties:
        error:
          type: string
        error_description:
          type: string
      required:
        - error
        - error_description
    OAuth2IntrospectRequestRequest:
      type: object
      properties:
        token:
          type: string
          minLength: 1
          description: The token to introspect.
        token_type_hint:
          type: string
          minLength: 1
          description: 'Optional advisory hint: ''access_token'' or ''refresh_token''.'
        client_id:
          type: string
          minLength: 1
        client_secret:
          type: string
          minLength: 1
      required:
        - token
    OAuth2RevokeError400:
      type: object
      properties:
        error:
          type: string
        error_description:
          type: string
      required:
        - error
        - error_description
    OAuth2RevokeError401:
      type: object
      properties:
        error:
          type: string
        error_description:
          type: string
      required:
        - error
        - error_description
    OAuth2RevokeRequestRequest:
      type: object
      properties:
        token:
          type: string
          minLength: 1
          description: The token to revoke.
        token_type_hint:
          type: string
          minLength: 1
          description: 'Optional advisory hint: ''access_token'' or ''refresh_token''.'
        client_id:
          type: string
          minLength: 1
        client_secret:
          type: string
          minLength: 1
      required:
        - token
    OAuth2TokenError400:
      type: object
      properties:
        error:
          type: string
        error_description:
          type: string
      required:
        - error
        - error_description
    OAuth2TokenError401:
      type: object
      properties:
        error:
          type: string
        error_description:
          type: string
      required:
        - error
        - error_description
    OAuth2TokenRequestRequest:
      type: object
      properties:
        grant_type:
          type: string
          minLength: 1
          description: '''authorization_code'' or ''refresh_token''.'
        code:
          type: string
          minLength: 1
          description: Authorization-code grant only.
        redirect_uri:
          type: string
          minLength: 1
          description: Authorization-code grant only.
        code_verifier:
          type: string
          minLength: 1
          description: Authorization-code grant only (PKCE).
        refresh_token:
          type: string
          minLength: 1
          description: Refresh-token grant only.
        scope:
          type: string
          minLength: 1
          description: Optional refresh-token scope narrowing.
        client_id:
          type: string
          minLength: 1
          description: When not using HTTP Basic.
        client_secret:
          type: string
          minLength: 1
          description: Confidential client only.
      required:
        - grant_type
    OAuth2TokenResponse:
      type: object
      properties:
        access_token:
          type: string
        token_type:
          type: string
        expires_in:
          type: integer
        refresh_token:
          type: string
        scope:
          type: string
      required:
        - access_token
        - expires_in
        - refresh_token
        - scope
        - token_type
    PartnerAllowedOriginsRemoveResponse:
      type: object
      properties:
        allowed_origins:
          type: array
          items:
            type: string
            format: uri
      required:
        - allowed_origins
    PartnerAllowedOriginsResponse:
      type: object
      properties:
        allowed_origins:
          type: array
          items:
            type: string
            format: uri
          description: Updated full list of allowed origins.
      required:
        - allowed_origins
    PartnerRotateApiKeyResponse:
      type: object
      properties:
        plaintext_api_key:
          type: string
          description: Shown ONCE — copy immediately. Cannot be recovered.
        api_key_prefix:
          type: string
          description: First 12 chars; helpful for the partner to identify the key in logs.
      required:
        - api_key_prefix
        - plaintext_api_key
    PartnerRotateSigningSecretResponse:
      type: object
      properties:
        plaintext_signing_secret:
          type: string
          description: Shown ONCE — copy immediately. Cannot be recovered.
      required:
        - plaintext_signing_secret
    PartnerSelf:
      type: object
      description: |-
        Self-serializer for /me/partner.

        Returns only what's safe to surface to the partner owner. The signing
        secret hash and API key hash are NEVER included — they're sha256 outputs
        but exposing them would let an attacker who exfiltrated a hash
        independently verify guesses against the auth backend without ever
        hitting the rate limiter, so we treat them as secrets too.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        slug:
          type: string
          readOnly: true
          pattern: ^[-a-zA-Z0-9_]+$
        name:
          type: string
          readOnly: true
        contact_email:
          type: string
          format: email
          readOnly: true
        website_url:
          type: string
          format: uri
          readOnly: true
        api_key_prefix:
          type: string
          readOnly: true
        scopes:
          type: array
          items:
            type: string
            maxLength: 64
          readOnly: true
        game_scopes:
          type: array
          items:
            type: string
            maxLength: 32
            pattern: ^[-a-zA-Z0-9_]+$
          readOnly: true
        allowed_origins:
          type: array
          items:
            type: string
            format: uri
            maxLength: 200
          readOnly: true
        rate_limit_tier:
          allOf:
            - $ref: '#/components/schemas/RateLimitTierEnum'
          readOnly: true
        status:
          allOf:
            - $ref: '#/components/schemas/OAuth2ClientStatusEnum'
          readOnly: true
        secret_rotated_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        last_used_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
      required:
        - allowed_origins
        - api_key_prefix
        - contact_email
        - created_at
        - game_scopes
        - id
        - last_used_at
        - name
        - rate_limit_tier
        - scopes
        - secret_rotated_at
        - slug
        - status
        - updated_at
        - website_url
    PatOutput:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          maxLength: 255
        token_prefix:
          type: string
          maxLength: 12
        scopes:
          type: array
          items:
            type: string
        last_used_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
          readOnly: true
      required:
        - created_at
        - id
        - name
        - scopes
        - token_prefix
    PatchedPublicDeckUpdateRequestRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        notes:
          type: string
          nullable: true
          maxLength: 50000
        visibility:
          type: array
          items:
            type: string
            minLength: 1
        format:
          type: string
          format: uuid
          nullable: true
          description: Format UUID for the deck.
        youtube_url:
          type: string
          format: uri
          maxLength: 500
        legal:
          type: boolean
        thumbnail:
          type: string
          maxLength: 255
        is_archived:
          type: boolean
        source_provider:
          $ref: '#/components/schemas/DeckSourceProviderEnum'
        source_external_id:
          type: string
          maxLength: 128
        source_external_version:
          type: string
          maxLength: 32
        source_external_url:
          type: string
          format: uri
    PatchedPublicSideboardGuideUpdateRequestRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
          maxLength: 500000
        visibility:
          type: array
          items:
            type: string
            minLength: 1
        tags:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 50
        format:
          type: string
          maxLength: 50
        thumbnail:
          type: string
          nullable: true
          maxLength: 255
        youtube_url:
          type: string
          format: uri
          maxLength: 500
        premium:
          type: boolean
        is_archived:
          type: boolean
    PublicDeckCreateRequestRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        notes:
          type: string
          nullable: true
          maxLength: 50000
        visibility:
          type: array
          items:
            type: string
            minLength: 1
        format:
          type: string
          format: uuid
          nullable: true
          description: Format UUID for the deck.
        youtube_url:
          type: string
          format: uri
          maxLength: 500
        legal:
          type: boolean
        thumbnail:
          type: string
          maxLength: 255
        source_provider:
          $ref: '#/components/schemas/DeckSourceProviderEnum'
        source_external_id:
          type: string
          maxLength: 128
        source_external_version:
          type: string
          maxLength: 32
        source_external_url:
          type: string
          format: uri
      required:
        - name
    PublicDeckDetail:
      type: object
      description: >-
        Provides ``featured_cards`` for any deck serializer.


        Relies on ``special_cards__card`` (and legacy
        commander/partner/companion

        FKs) being prefetched/selected by the caller's selector. The

        ``GameSpecialCardType`` lookup is cached on serializer context so a
        whole

        page of decks costs one query, not one per deck.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
        author:
          type: object
          nullable: true
          readOnly: true
        game:
          type: string
          readOnly: true
        visibility:
          type: array
          items:
            type: string
          readOnly: true
        format:
          type: object
          nullable: true
          readOnly: true
        archetype:
          type: string
          nullable: true
          readOnly: true
        views:
          type: integer
          readOnly: true
        likes:
          type: integer
          readOnly: true
        hasGuide:
          type: boolean
          readOnly: true
        hasSideboardGuide:
          type: boolean
          readOnly: true
        hasVideo:
          type: boolean
          readOnly: true
        editedAt:
          type: string
          format: date-time
          readOnly: true
        createdAt:
          type: string
          format: date-time
          readOnly: true
        sourceProvider:
          type: string
          nullable: true
          readOnly: true
        notes:
          type: string
          readOnly: true
        featured_cards:
          type: array
          items:
            $ref: '#/components/schemas/FeaturedCard'
          readOnly: true
        sourceExternalId:
          type: string
          nullable: true
          readOnly: true
        sourceExternalUrl:
          type: string
          nullable: true
          readOnly: true
      required:
        - archetype
        - author
        - createdAt
        - editedAt
        - featured_cards
        - format
        - game
        - hasGuide
        - hasSideboardGuide
        - hasVideo
        - id
        - likes
        - name
        - notes
        - sourceExternalId
        - sourceExternalUrl
        - sourceProvider
        - views
        - visibility
    PublicDeckExportRequestRequest:
      type: object
      properties:
        deck_id:
          type: string
          format: uuid
          nullable: true
        deck_payload:
          type: object
          nullable: true
    PublicDeckGuideOutput:
      type: object
      properties:
        id:
          type: string
          format: uuid
        deck_id:
          type: string
          format: uuid
        content_type:
          type: string
        content: {}
        is_premium:
          type: boolean
        is_truncated:
          type: boolean
          default: false
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - content
        - content_type
        - created_at
        - deck_id
        - id
        - is_premium
        - updated_at
    PublicDeckGuideUpsertRequestRequest:
      type: object
      properties:
        content_type:
          $ref: '#/components/schemas/ContentTypeEnum'
        content: {}
        is_premium:
          type: boolean
          default: false
      required:
        - content
        - content_type
    PublicDeckLikeResponse:
      type: object
      properties:
        is_liked:
          type: boolean
        total_likes:
          type: integer
      required:
        - is_liked
        - total_likes
    PublicDeckRollbackRequestRequest:
      type: object
      properties:
        version_number:
          type: integer
          minimum: 1
      required:
        - version_number
    PublicDeckSummaryItem:
      type: object
      description: >-
        Provides ``featured_cards`` for any deck serializer.


        Relies on ``special_cards__card`` (and legacy
        commander/partner/companion

        FKs) being prefetched/selected by the caller's selector. The

        ``GameSpecialCardType`` lookup is cached on serializer context so a
        whole

        page of decks costs one query, not one per deck.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
        author:
          type: object
          nullable: true
          readOnly: true
        game:
          type: string
          readOnly: true
        visibility:
          type: array
          items:
            type: string
          readOnly: true
        format:
          type: object
          nullable: true
          readOnly: true
        archetype:
          type: string
          nullable: true
          readOnly: true
        views:
          type: integer
          readOnly: true
        likes:
          type: integer
          readOnly: true
        hasGuide:
          type: boolean
          readOnly: true
        hasSideboardGuide:
          type: boolean
          readOnly: true
        hasVideo:
          type: boolean
          readOnly: true
        editedAt:
          type: string
          format: date-time
          readOnly: true
        createdAt:
          type: string
          format: date-time
          readOnly: true
        sourceProvider:
          type: string
          nullable: true
          readOnly: true
        featured_cards:
          type: array
          items:
            $ref: '#/components/schemas/FeaturedCard'
          readOnly: true
        section_counts:
          type: object
          additionalProperties:
            type: integer
          readOnly: true
        colors:
          type: array
          items:
            type: string
          readOnly: true
      required:
        - archetype
        - author
        - createdAt
        - editedAt
        - format
        - game
        - hasGuide
        - hasSideboardGuide
        - hasVideo
        - id
        - likes
        - name
        - sourceProvider
        - views
        - visibility
    PublicDeckVersion:
      type: object
      properties:
        version_number:
          type: integer
          readOnly: true
        name_at_version:
          type: string
          readOnly: true
        edited_at:
          type: string
          format: date-time
          readOnly: true
        history_type:
          type: string
          readOnly: true
        visibility:
          type: array
          items:
            type: string
          readOnly: true
      required:
        - edited_at
        - history_type
        - name_at_version
        - version_number
        - visibility
    PublicDeckViewIncrementResponse:
      type: object
      properties:
        pageviews:
          type: integer
        incremented:
          type: boolean
      required:
        - incremented
        - pageviews
    PublicDeckWriteResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
          nullable: true
        notes:
          type: string
          readOnly: true
          nullable: true
        visibility:
          type: array
          items:
            type: string
          readOnly: true
        format:
          type: string
          format: uuid
          readOnly: true
          nullable: true
        youtube_url:
          type: string
          format: uri
          readOnly: true
          nullable: true
        thumbnail:
          type: string
          readOnly: true
          nullable: true
        legal:
          type: boolean
          readOnly: true
        is_archived:
          type: boolean
          readOnly: true
        likes:
          type: integer
          readOnly: true
        pageviews:
          type: integer
          readOnly: true
        version:
          type: integer
          readOnly: true
        source_provider:
          type: string
          readOnly: true
        source_external_id:
          type: string
          readOnly: true
        source_external_version:
          type: string
          readOnly: true
        source_external_url:
          type: string
          readOnly: true
        source_imported_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
        created_at:
          type: string
          format: date-time
          readOnly: true
        updated_at:
          type: string
          format: date-time
          readOnly: true
      required:
        - created_at
        - format
        - id
        - is_archived
        - legal
        - likes
        - name
        - notes
        - pageviews
        - source_external_id
        - source_external_url
        - source_external_version
        - source_imported_at
        - source_provider
        - thumbnail
        - updated_at
        - version
        - visibility
        - youtube_url
    PublicSideboardGuideAttachDeckRequestRequest:
      type: object
      properties:
        deck_id:
          type: string
          format: uuid
      required:
        - deck_id
    PublicSideboardGuideCreateRequestRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        deck_id:
          type: string
          format: uuid
        description:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
          maxLength: 500000
        visibility:
          type: array
          items:
            type: string
            minLength: 1
        tags:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 50
        format:
          type: string
          maxLength: 50
        thumbnail:
          type: string
          nullable: true
          maxLength: 255
        youtube_url:
          type: string
          format: uri
          maxLength: 500
        premium:
          type: boolean
        is_archived:
          type: boolean
        matchups:
          type: array
          items:
            $ref: '#/components/schemas/PublicSideboardGuideMatchupRequest'
        attached_deck_ids:
          type: array
          items:
            type: string
            format: uuid
      required:
        - deck_id
        - name
    PublicSideboardGuideDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
        author:
          type: object
          nullable: true
          readOnly: true
        game:
          type: string
          readOnly: true
        visibility:
          type: array
          items:
            type: string
          readOnly: true
        deck_id:
          type: string
          format: uuid
          readOnly: true
        format:
          type: string
          readOnly: true
        views:
          type: integer
          readOnly: true
        likes:
          type: integer
          readOnly: true
        hasMatchups:
          type: boolean
          readOnly: true
        thumbnail:
          type: string
          readOnly: true
          nullable: true
        editedAt:
          type: string
          format: date-time
          readOnly: true
        createdAt:
          type: string
          format: date-time
          readOnly: true
        sourceProvider:
          type: string
          nullable: true
          readOnly: true
        description:
          type: string
          readOnly: true
          nullable: true
        notes:
          type: string
          readOnly: true
          nullable: true
        youtube_url:
          type: string
          format: uri
          readOnly: true
          nullable: true
        premium:
          type: boolean
          readOnly: true
        is_archived:
          type: boolean
          readOnly: true
        tags:
          type: array
          items:
            type: string
          readOnly: true
        matchups:
          type: array
          items:
            type: object
          readOnly: true
        decks:
          type: array
          items:
            type: object
          readOnly: true
        attached_deck_ids:
          type: array
          items:
            type: string
          readOnly: true
      required:
        - attached_deck_ids
        - author
        - createdAt
        - deck_id
        - decks
        - description
        - editedAt
        - format
        - game
        - hasMatchups
        - id
        - is_archived
        - likes
        - matchups
        - name
        - notes
        - premium
        - sourceProvider
        - tags
        - thumbnail
        - views
        - visibility
        - youtube_url
    PublicSideboardGuideMatchupRequest:
      type: object
      properties:
        opponent_deck_name:
          type: string
          minLength: 1
          maxLength: 255
        opponent_archetype:
          type: string
          maxLength: 100
        difficulty:
          $ref: '#/components/schemas/DifficultyEnum'
        notes:
          type: string
          nullable: true
          maxLength: 50000
        play_notes:
          type: string
          nullable: true
          maxLength: 50000
        draw_notes:
          type: string
          nullable: true
          maxLength: 50000
        sort_order:
          type: integer
        thumbnail:
          type: string
          nullable: true
          maxLength: 255
        is_featured:
          type: boolean
      required:
        - opponent_deck_name
    PublicSideboardGuideSummaryItem:
      type: object
      description: >-
        Provides ``featured_cards`` for any deck serializer.


        Relies on ``special_cards__card`` (and legacy
        commander/partner/companion

        FKs) being prefetched/selected by the caller's selector. The

        ``GameSpecialCardType`` lookup is cached on serializer context so a
        whole

        page of decks costs one query, not one per deck.
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        name:
          type: string
          readOnly: true
        author:
          type: object
          nullable: true
          readOnly: true
        game:
          type: string
          readOnly: true
        visibility:
          type: array
          items:
            type: string
          readOnly: true
        deck_id:
          type: string
          format: uuid
          readOnly: true
        format:
          type: string
          readOnly: true
        views:
          type: integer
          readOnly: true
        likes:
          type: integer
          readOnly: true
        hasMatchups:
          type: boolean
          readOnly: true
        thumbnail:
          type: string
          readOnly: true
          nullable: true
        editedAt:
          type: string
          format: date-time
          readOnly: true
        createdAt:
          type: string
          format: date-time
          readOnly: true
        sourceProvider:
          type: string
          nullable: true
          readOnly: true
        featured_cards:
          type: array
          items:
            $ref: '#/components/schemas/FeaturedCard'
          readOnly: true
        colors:
          type: array
          items:
            type: string
          readOnly: true
        matchup_count:
          type: integer
          readOnly: true
      required:
        - author
        - createdAt
        - deck_id
        - editedAt
        - format
        - game
        - hasMatchups
        - id
        - likes
        - name
        - sourceProvider
        - thumbnail
        - views
        - visibility
    RateLimitTierEnum:
      enum:
        - standard
        - premium
      type: string
      description: |-
        * `standard` - Standard (1000/min)
        * `premium` - Premium (20000/min)
    _OriginRequestRequest:
      type: object
      properties:
        origin:
          type: string
          format: uri
          minLength: 1
          description: Full origin to add/remove (e.g. https://app.example.com).
      required:
        - origin
    _ScopeChangeRequestRequest:
      type: object
      properties:
        requested_scopes:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 64
          maxItems: 20
        requested_game_scopes:
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 32
            pattern: ^[-a-zA-Z0-9_]+$
          maxItems: 10
        justification:
          type: string
          minLength: 10
          maxLength: 2000
      required:
        - justification
    _ScopeChangeResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
        message:
          type: string
      required:
        - id
        - message
        - status
    DeckSourceProviderEnumPretty:
      type: string
      enum:
        - Piltover
        - Moxfield
        - Archidekt
        - Cube Cobra
        - MTGGoldfish
        - TappedOut
        - Deckstats
        - EDHREC
        - MTG Arena
        - MTGO
        - Forge
        - Carde.io
        - Manual
      description: |-
        Pretty display names for DeckSourceProviderEnum:
        * `piltover` - Piltover
        * `moxfield` - Moxfield
        * `archidekt` - Archidekt
        * `cube_cobra` - Cube Cobra
        * `mtggoldfish` - MTGGoldfish
        * `tappedout` - TappedOut
        * `deckstats` - Deckstats
        * `edhrec` - EDHREC
        * `mtg_arena` - MTG Arena
        * `mtgo` - MTGO
        * `forge` - Forge
        * `carde_io` - Carde.io
        * `manual` - Manual
      x-enum-varnames:
        - PILTOVER
        - MOXFIELD
        - ARCHIDEKT
        - CUBE_COBRA
        - MTGGOLDFISH
        - TAPPEDOUT
        - DECKSTATS
        - EDHREC
        - MTG_ARENA
        - MTGO
        - FORGE
        - CARDE_IO
        - MANUAL
      x-enum-descriptions:
        - Piltover
        - Moxfield
        - Archidekt
        - Cube Cobra
        - MTGGoldfish
        - TappedOut
        - Deckstats
        - EDHREC
        - MTG Arena
        - MTGO
        - Forge
        - Carde.io
        - Manual
    OAuth2ClientStatusEnumPretty:
      type: string
      enum:
        - Active
        - Suspended
        - Revoked
      description: |-
        Pretty display names for OAuth2ClientStatusEnum:
        * `active` - Active
        * `suspended` - Suspended
        * `revoked` - Revoked
      x-enum-varnames:
        - ACTIVE
        - SUSPENDED
        - REVOKED
      x-enum-descriptions:
        - Active
        - Suspended
        - Revoked
    OAuth2ClientTypeEnumPretty:
      type: string
      enum:
        - Confidential
        - Public
      description: |-
        Pretty display names for OAuth2ClientTypeEnum:
        * `confidential` - Confidential
        * `public` - Public
      x-enum-varnames:
        - CONFIDENTIAL
        - PUBLIC
      x-enum-descriptions:
        - Confidential
        - Public
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        Partner API key (format: ak_<token>). Alternative to HMAC for simpler
        integrations.
    JWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Enter JWT token in the format: Bearer <token>'
    OAuth2Bearer:
      type: http
      scheme: bearer
      bearerFormat: flx_at_...
      description: >-
        OAuth2 access token issued via the authorization-code + PKCE flow
        (format: flx_at_<token>). Grants the partner user-scoped access to act
        on behalf of the consenting end-user.
    OAuth2DPoP:
      type: http
      scheme: DPoP
      description: >-
        DPoP-bound OAuth2 access token (RFC 9449). Issued by the /oauth/token
        endpoint when the client is registered with
        dpop_bound_access_tokens=true. Each protected request must include a
        fresh DPoP proof JWT in the DPoP header. Token format: flx_at_<token>.
    OAuth2DPoPProof:
      type: apiKey
      in: header
      name: DPoP
      description: >-
        RFC 9449 DPoP proof JWT. Required on every request that presents
        Authorization: DPoP <access_token>. Must carry fresh iat, jti, htm
        matching the request method, htu matching the request URL, and ath
        matching SHA-256 of the access token.
    PartnerHmac:
      type: apiKey
      in: header
      name: X-Signature
      description: >-
        HMAC-SHA256 over canonical request string. Format:
        v1.<unix_seconds>.<hex_hmac_sha256>.
    PartnerSlug:
      type: apiKey
      in: header
      name: X-Partner-Id
      description: Partner identifier slug. Required alongside X-Signature for HMAC auth.
    PersonalAccessToken:
      type: http
      scheme: bearer
      bearerFormat: flx_pat_...
      description: 'User-issued personal access token (format: flx_pat_<token>).'
servers:
  - url: https://api.flexslot.gg
    description: Production
  - url: https://api-dev.flexslot.gg
    description: Development
