openapi: 3.1.0
info:
  title: FLOOR product list
  description: >
    Public product list for shopping bots. No login.
    Field protocol is floor.b2a/v1. settlement is not_settled.
    The bot pays at payment.checkout_url or payment.accepts (x402).
    FLOOR does not settle x402 or hold funds.
  version: "1.0.0"
servers:
  - url: https://floor-desk-ecru.vercel.app
paths:
  /api/catalog:
    get:
      summary: Fetch the FLOOR product list
      operationId: getCatalog
      security: []
      responses:
        "200":
          description: Product list document
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Catalog"
  /api/listings:
    get:
      summary: Same public items as GET /api/catalog, including payment.accepts
      operationId: getListings
      security: []
      responses:
        "200":
          description: Product list document
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Catalog"
    post:
      summary: List a product. First complete listing is free. No login.
      operationId: postListing
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListingPost"
            example:
              kind: digital
              name: Photo pack
              specs:
                - { name: Format, value: JPEG }
                - { name: Count, value: "24 photos" }
                - { name: Resolution, value: "4000 px" }
                - { name: Color, value: sRGB }
                - { name: License, value: one buyer }
                - { name: Size, value: "120 MB" }
              inventory: unlimited
              refund_days: 0
              warranty: none
              delivery: download
              lead_time: instant
              payment:
                checkout_url: https://pay.example.com/photos
                network: base
                payTo: 0xYourPublicReceiveAddress
                price: "12.50"
              owner:
                name: optional seller name
      responses:
        "201":
          description: Listed. Item is on GET /api/catalog and GET /api/listings.
        "400":
          description: Incomplete. Plain-English reason JSON.
        "402":
          description: Further listings need a desk. $49 once for 12 months.
  /api/buy:
    post:
      summary: Start a buy. Same HTTP 402 as GET /pay/{id}, plus unpaid receipt_id. settled is false until a rail confirms.
      operationId: postBuy
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [item_id]
              properties:
                item_id:
                  type: string
            example:
              item_id: floor-desk
      responses:
        "402":
          description: Payment required. checkout_url and/or x402 accepts. receipt_id is an unpaid intent. settled is false.
        "400":
          description: Missing item_id or no pay rail.
        "404":
          description: Item is not on the list.
  /api/feedback:
    post:
      summary: Store a short note. Email optional. message required. tried is list, buy, or desk.
      operationId: postFeedback
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message, tried]
              properties:
                message:
                  type: string
                tried:
                  type: string
                  enum: [list, buy, desk]
                email:
                  type: string
            example:
              tried: desk
              message: Second listing returned 402.
      responses:
        "201":
          description: Saved.
        "400":
          description: Incomplete. Names the field.
  /api/desk/ack:
    post:
      summary: QA test mint only. Requires env FLOOR_TEST_DESK_SECRET. Not a verified Whop payment.
      operationId: postDeskAck
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                secret:
                  type: string
      responses:
        "201":
          description: Test cookie and desk_token. settled is false.
        "403":
          description: Wrong test secret.
        "404":
          description: Test mint is off.
  /api/desk/unlock:
    post:
      summary: After a Whop payment, confirm membership and return desk_token. Does not mint on honor.
      operationId: postDeskUnlock
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                payment_id:
                  type: string
                membership_id:
                  type: string
            example:
              payment_id: pay_XXXXXXXX
      responses:
        "200":
          description: desk_token after Whop confirm. settled is true only when a paid amount was verified.
        "400":
          description: Missing payment_id or membership_id.
        "402":
          description: Whop did not confirm a desk membership.
        "503":
          description: WHOP_API_KEY is not set.
  /api/webhooks/whop:
    post:
      summary: Whop signed webhook. Requires WHOP_WEBHOOK_SECRET. Unsigned events are rejected.
      operationId: postWhopWebhook
      security: []
      responses:
        "200":
          description: Event accepted. Entitlement recorded. desk_token minted or ignored.
        "401":
          description: Signature did not match.
        "503":
          description: WHOP_WEBHOOK_SECRET is not set.
components:
  schemas:
    Catalog:
      type: object
      additionalProperties: false
      required: [protocol, generated_at, items, settlement]
      properties:
        protocol:
          type: string
          const: floor.b2a/v1
        generated_at:
          type: string
          format: date-time
        items:
          type: array
          description: Public products. May include the FLOOR desk. Extra items come from sellers.
          items:
            $ref: "#/components/schemas/CatalogItem"
        settlement:
          type: string
          const: not_settled
    CatalogItem:
      type: object
      description: A listed product must be fully specified. The live list may be empty.
      required:
        - sku
        - title
        - kind
        - owner
        - specs
        - inventory
        - lead_time
        - return_days
        - warranty
        - payment
        - sla_hours
      properties:
        sku:
          type: string
        title:
          type: string
        kind:
          type: string
          enum: [physical, digital]
        owner:
          type: object
          required: [type, name]
          properties:
            type:
              type: string
              enum: [house, desk]
            name:
              type: string
        specs:
          type: array
          minItems: 6
          items:
            type: object
            required: [name, value]
            properties:
              name:
                type: string
              value:
                type: string
        inventory:
          type: integer
          minimum: 0
        unlimited:
          type: boolean
        lead_time:
          type: string
        return_days:
          type: integer
          minimum: 0
        warranty:
          type: string
        checkout:
          type: string
          format: uri
        payment:
          type: object
          description: checkout_url and/or x402 accepts. Public fields only.
          properties:
            checkout_url:
              type: string
              format: uri
            accepts:
              type: array
              items:
                type: object
                required: [scheme, network, maxAmountRequired, asset, payTo, resource, description]
                properties:
                  scheme:
                    type: string
                    const: exact
                  network:
                    type: string
                    enum: [base, solana]
                  maxAmountRequired:
                    type: string
                  asset:
                    type: string
                  payTo:
                    type: string
                  resource:
                    type: string
                  description:
                    type: string
        ships_from:
          type: string
        delivery:
          type: string
        price:
          type: string
        refund:
          type: string
        sla_hours:
          type: integer
          minimum: 0
    ListingPost:
      type: object
      description: >
        Same facts as the human desk form. title or name. return_days or refund_days.
        payment.checkout_url and/or x402 network, payTo, price. Public fields only.
      required: [kind, specs, inventory, warranty]
      properties:
        kind:
          type: string
          enum: [physical, digital]
        title:
          type: string
        name:
          type: string
        specs:
          type: array
          minItems: 6
        inventory: {}
        return_days:
          type: integer
        refund_days:
          type: integer
        warranty:
          type: string
        ships_from:
          type: string
        lead_time:
          type: string
        delivery:
          type: string
        payment:
          type: object
          properties:
            checkout_url:
              type: string
            network:
              type: string
              enum: [base, solana]
            payTo:
              type: string
            price:
              type: string
        owner:
          type: object
          properties:
            name:
              type: string
