openapi: 3.1.0
info:
  title: SB Relaxing Club API
  version: 0.1.0
  description: Versioned contract for the SB Relaxing Club mobile and admin clients.
servers:
  - url: http://localhost:8080
    description: Local development
paths:
  /health:
    get:
      summary: Liveness check
      responses:
        '200':
          description: API process is running
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /ready:
    get:
      summary: Readiness check
      responses:
        '200':
          description: Dependencies are ready or readiness checks are disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/v1:
    get:
      summary: API version information
      responses:
        '200':
          description: Version information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /api/v1/auth/register:
    post:
      summary: Register a member account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '201':
          description: Account created pending verification
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '422':
          description: Validation failed
  /api/v1/auth/login:
    post:
      summary: Validate email and password and send a sign-in code by email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '202': {description: Sign-in code sent; session is not created yet}
        '401':
          description: Invalid credentials
        '429': {description: Sign-in code request throttled}
        '503': {description: Email delivery is unavailable}
  /api/v1/auth/login/verify:
    post:
      summary: Verify a sign-in code and create the session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code]
              properties:
                email: {type: string, format: email}
                code: {type: string, minLength: 6, maxLength: 6}
      responses: {'200': {description: Session created}, '401': {description: Code invalid or expired}}
  /api/v1/auth/login/otp/resend:
    post:
      summary: Resend a sign-in code for an active account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties: {email: {type: string, format: email}}
      responses: {'202': {description: Accepted if account is active}, '429': {description: Request throttled}}
  /api/v1/auth/refresh:
    post:
      summary: Rotate a refresh token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshRequest'
      responses:
        '200': {description: Session rotated}
        '401': {description: Invalid or revoked refresh token}
  /api/v1/auth/logout:
    post:
      summary: Revoke a refresh token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefreshRequest'
      responses:
        '200': {description: Session revoked}
        '401': {description: Invalid or revoked refresh token}
  /api/v1/auth/verify-email:
    post:
      summary: Verify an email OTP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id, code]
              properties:
                user_id: {type: string}
                code: {type: string, minLength: 4, maxLength: 8}
      responses:
        '200': {description: Email verified}
        '422': {description: OTP invalid or expired}
  /api/v1/auth/verification/resend:
    post:
      summary: Request another email verification code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: {type: string, format: email}
      responses: {'202': {description: Verification sent if the account is pending}}
  /api/v1/auth/me:
    get:
      summary: Get the authenticated user
      security: [{bearerAuth: []}]
      responses:
        '200': {description: Current user}
        '401': {description: Authentication required}
    patch:
      summary: Update the authenticated user's username and display name
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, display_name]
              properties:
                username: {type: string, minLength: 3, maxLength: 32}
                display_name: {type: string, minLength: 1, maxLength: 100}
      responses: {'200': {description: Profile updated}, '401': {description: Authentication required}, '409': {description: Username is already in use}, '422': {description: Invalid profile fields}}
    delete:
      summary: Delete the authenticated user's account
      security: [{bearerAuth: []}]
      responses: {'200': {description: Account deleted}, '401': {description: Authentication required}}
  /api/v1/auth/password-reset/request:
    post:
      summary: Request a password reset code
      responses:
        '202': {description: Accepted without account enumeration}
  /api/v1/auth/password-reset/confirm:
    post:
      summary: Confirm a password reset code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code, password]
              properties:
                email: {type: string, format: email}
                code: {type: string, minLength: 4, maxLength: 8}
                password: {type: string, minLength: 8, maxLength: 128, format: password}
      responses:
        '200': {description: Password reset}
        '422': {description: Invalid or expired reset code}
  /api/v1/catalog/services:
    get:
      summary: List active services
      responses:
        '200': {description: Active catalog services}
  /api/v1/catalog/categories:
    get:
      summary: List active business listing categories
      responses:
        '200': {description: Active categories usable by POST /api/v1/listings}
  /api/v1/providers:
    post:
      summary: Register a provider profile
      security: [{bearerAuth: []}]
      responses:
        '201': {description: Provider created pending approval}
        '422': {description: Invalid provider fields}
        '401': {description: Authentication required}
  /api/v1/listings:
    get:
      summary: Search published, approved, subscribed business listings
      parameters:
        - {name: q, in: query, schema: {type: string, maxLength: 120}}
        - {name: category_id, in: query, schema: {type: string}}
        - {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
        - {name: offset, in: query, schema: {type: integer, minimum: 0, default: 0}}
      responses:
        '200':
          description: Published businesses with active menu items and integer minor-unit prices
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: {type: boolean}
                  data:
                    type: array
                    items:
                      type: object
                      required: [id, provider_id, provider_name, name, category_id, category_name, menu_items]
                      properties:
                        id: {type: string}
                        provider_id: {type: string}
                        provider_name: {type: string}
                        name: {type: string}
                        category_id: {type: string}
                        category_name: {type: string}
                        description: {type: string}
                        featured: {type: boolean}
                        menu_items:
                          type: array
                          items:
                            type: object
                            required: [id, name, price_amount, currency]
                            properties:
                              id: {type: string}
                              name: {type: string}
                              description: {type: string}
                              price_amount: {type: integer}
                              currency: {type: string, minLength: 3, maxLength: 3}
        '400': {description: Pagination parameters are not integers}
        '422': {description: Pagination parameters are outside supported bounds}
    post:
      summary: Create a draft business listing
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [provider_id, name, category_id]
              properties:
                provider_id: {type: string}
                category_id: {type: string}
                name: {type: string}
                description: {type: string}
      responses:
        '201': {description: Listing created as draft}
        '404': {description: Caller does not own a provider profile}
        '422': {description: Invalid listing fields}
        '401': {description: Authentication required}
  /api/v1/listings/{id}/contact:
    get:
      summary: Resolve the provider contact for an active listing
      description: Authenticated endpoint; only approved, published, currently subscribed listings return a contact user ID.
      security: [{bearerAuth: []}]
      parameters:
        - {name: id, in: path, required: true, schema: {type: string}}
      responses:
        '200':
          description: Provider contact for opening a chat
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: {type: boolean}
                  data:
                    type: object
                    required: [user_id, display_name]
                    properties:
                      user_id: {type: string}
                      display_name: {type: string}
        '404': {description: Listing is unavailable for contact}
        '401': {description: Authentication required}
  /api/v1/dating/profile:
    put:
      summary: Create or update the authenticated dating profile
      security: [{bearerAuth: []}]
      responses: {'200': {description: Profile saved}}
  /api/v1/dating/browse:
    get:
      summary: Browse opted-in visible dating profiles
      security: [{bearerAuth: []}]
      responses: {'200': {description: Profiles returned}}
  /api/v1/curated-content:
    get:
      summary: List published curated content
      security: [{bearerAuth: []}]
      responses: {'200': {description: Content returned}}
  /api/v1/meetings:
    get:
      summary: List scheduled meetings
      security: [{bearerAuth: []}]
      responses: {'200': {description: Meetings returned}}
    post:
      summary: Schedule a meeting
      security: [{bearerAuth: []}]
      responses: {'201': {description: Meeting created}}
  /api/v1/meetings/{id}/join:
    post:
      summary: Join a scheduled or active meeting
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Participant joined with short-lived room-scoped LiveKit credentials}, '404': {description: Meeting not found}, '422': {description: Meeting has ended}, '503': {description: Live video is not configured}}
  /api/v1/meetings/{id}/leave:
    post:
      summary: Leave a meeting
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Participant left}, '404': {description: Active participation not found}}
  /api/v1/meetings/{id}/end:
    post:
      summary: End a meeting as its host
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Meeting ended}, '403': {description: Only the host may end the meeting}, '404': {description: Meeting not found}}
  /api/v1/admin/community/reports/{id}/moderate:
    post:
      summary: Update a community report status
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Report updated}, '403': {description: Administrator required}}

  /api/v1/reports/summary:
    get:
      summary: Read the scoped 30-day accountant/admin summary
      security: [{bearerAuth: []}]
      responses: {'200': {description: Summary returned}, '403': {description: Accountant or administrator required}}
  /api/v1/admin/audit:
    get:
      summary: Read recent audit events
      security: [{bearerAuth: []}]
      responses: {'200': {description: Audit events returned}, '403': {description: Administrator required}}

  /api/v1/admin/overview:
    get:
      summary: Read administrator operational totals
      security: [{bearerAuth: []}]
      responses: {'200': {description: Marketplace and moderation totals returned}, '403': {description: Administrator required}}
  /api/v1/admin/{kind}:
    get:
      summary: Search an administrator collection
      security: [{bearerAuth: []}]
      parameters:
        - {name: kind, in: path, required: true, schema: {type: string, enum: [users, accountants, services, providers, listings, orders, payments]}}
        - {name: q, in: query, schema: {type: string}}
        - {name: status, in: query, schema: {type: string}}
        - {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100}}
        - {name: offset, in: query, schema: {type: integer, minimum: 0}}
      responses: {'200': {description: Filtered collection and pagination returned}, '403': {description: Administrator required}}
  /api/v1/admin/{kind}/{id}/status:
    patch:
      summary: Update an administrator managed user or listing status
      security: [{bearerAuth: []}]
      parameters:
        - {name: kind, in: path, required: true, schema: {type: string, enum: [users, listings]}}
        - {name: id, in: path, required: true, schema: {type: string}}
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, required: [status], properties: {status: {type: string}}}
      responses: {'200': {description: Status changed and audited}, '403': {description: Administrator required}, '422': {description: Invalid status}}
  /api/v1/admin/orders/{id}/transition:
    post:
      summary: Apply an authorized order workflow transition as an administrator
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, required: [status, version], properties: {status: {type: string}, version: {type: integer}}}
      responses: {'200': {description: Order changed}, '403': {description: Administrator required}, '409': {description: Order changed concurrently}}
  /api/v1/admin/payments/{id}/reject:
    post:
      summary: Reject a submitted payment and synchronize its order payment state
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Payment rejected and decision audited}, '403': {description: Administrator required}, '409': {description: Payment is not awaiting review}}
  /api/v1/admin/accountants/{id}/services:
    get:
      summary: Read services assigned to an accountant
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Service assignments returned}, '403': {description: Administrator required}}
    put:
      summary: Replace an accountant's assigned service scope
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, required: [service_ids], properties: {service_ids: {type: array, items: {type: string}}}}
      responses: {'200': {description: Assignments replaced and audited}, '403': {description: Administrator required}, '422': {description: Invalid accountant or service}}

  /api/v1/admin/users/{id}/role:
    patch:
      summary: Update a user's role
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, required: [role], properties: {role: {type: string, enum: [member, provider, accountant, admin]}}}
      responses: {'200': {description: Role updated}, '403': {description: Administrator required}}
  /api/v1/admin/community/reports:
    get:
      summary: List moderation reports
      security: [{bearerAuth: []}]
      responses: {'200': {description: Reports returned}}
  /api/v1/admin/community/reports/{id}/status:
    post:
      summary: Change a report status
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Report updated}}
  /api/v1/admin/announcements:
    post:
      summary: Create an announcement
      security: [{bearerAuth: []}]
      responses: {'201': {description: Announcement created}}
  /api/v1/announcements:
    get:
      summary: List published announcements
      security: [{bearerAuth: []}]
      responses: {'200': {description: Announcements returned}}
  /metrics:
    get:
      summary: Export process metrics
      responses: {'200': {description: Prometheus text metrics}}
  /api/v1/providers/me:
    patch:
      summary: Update the authenticated provider profile
      security: [{bearerAuth: []}]
      responses: {'200': {description: Provider profile updated}, '401': {description: Authentication required}, '404': {description: Provider profile not found}}
  /api/v1/listings/{id}:
    get:
      summary: Read an active, published business listing
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Listing details including active menu items}, '404': {description: Listing is not public or does not exist}}
    patch:
      summary: Update an owned business listing
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Listing updated}, '404': {description: Listing not found or caller is not its owner}}
    delete:
      summary: Deactivate an owned business listing
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Listing deactivated}, '404': {description: Listing not found or caller is not its owner}}
  /api/v1/admin/providers/{id}/approve:
    post:
      summary: Approve a provider profile and record the decision in the audit log
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Provider approved}, '403': {description: Administrator access required}, '404': {description: Provider not found}}
  /api/v1/orders:
    get:
      summary: List orders visible to the authenticated customer and provider
      security: [{bearerAuth: []}]
      parameters:
        - {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
        - {name: offset, in: query, schema: {type: integer, minimum: 0, default: 0}}
      responses: {'200': {description: Orders owned by the caller as customer or provider}, '422': {description: Invalid pagination}}
    post:
      summary: Create an order as a member or guest
      security: [{bearerAuth: []}, {}]
      parameters: [{name: Idempotency-Key, in: header, required: false, description: Required for guest requests; retry keys are supported for authenticated requests., schema: {type: string, minLength: 1, maxLength: 128}}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [menu_item_id, quantity]
              properties:
                menu_item_id: {type: string}
                quantity: {type: integer, minimum: 1, maximum: 100}
                schedule_at: {type: string, format: date-time}
                notes: {type: string}
                email: {type: string, format: email, description: Required for guests}
                phone: {type: string, description: Required for guests}
              description: Guest requests require email and phone. Client supplied totals, provider, and currency are ignored; the server derives them from the active menu item. Guest responses include a one-time capability; only its hash is stored.
      responses: {'201': {description: Order created; guest response includes a 90-day scoped access capability}, '409': {description: Idempotency key reused with different data}, '422': {description: Item unavailable or request invalid}}
  /api/v1/guest/orders/{id}:
    get:
      summary: Recover a guest booking using its access capability
      parameters:
        - {name: id, in: path, required: true, schema: {type: string}}
        - {name: X-Guest-Access-Token, in: header, required: true, schema: {type: string, minLength: 64, maxLength: 64}}
      responses: {'200': {description: Guest order details}, '404': {description: Booking not found, access invalid, or capability expired}}
  /api/v1/orders/{id}:
    get:
      summary: Read an order owned by the authenticated customer or provider
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Order details}, '404': {description: Order not found or caller is not a participant}}
  /api/v1/orders/{id}/transition:
    post:
      summary: Apply an authorized, versioned order state transition
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status, version]
              properties:
                status: {type: string, enum: [confirmed, in_progress, completed, cancelled]}
                version: {type: integer, minimum: 1}
      responses:
        '200': {description: Updated server order state}
        '403': {description: Caller is not allowed to make this transition}
        '404': {description: Order not found}
        '409': {description: Order version is stale}
        '422': {description: Transition is invalid for the current state}
  /api/v1/orders/{id}/payments:
    post:
      summary: Create a payment attempt for an owned order
      security: [{bearerAuth: []}]
      parameters:
        - {name: id, in: path, required: true, schema: {type: string}}
        - {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [method]
              properties: {method: {type: string, enum: [wechat_qr, alipay_qr]}}
      responses: {'201': {description: Payment attempt created from the persisted order total; includes method-specific configured instructions}, '409': {description: Idempotency key reused with different data}, '422': {description: Order unavailable or not owned by caller}}
  /api/v1/listings/{id}/subscription-payments:
    post:
      summary: Create a listing subscription payment using the server-configured plan price
      security: [{bearerAuth: []}]
      parameters:
        - {name: id, in: path, required: true, schema: {type: string}}
        - {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [method]
              properties: {method: {type: string, enum: [wechat_qr, alipay_qr]}}
      responses: {'201': {description: Payment attempt created using the configured subscription amount and currency; includes method-specific configured instructions}, '404': {description: Listing is not owned by caller}, '409': {description: Idempotency key reused with different data}, '422': {description: Subscription plan or request is unavailable}}
  /api/v1/payments/{id}/submit:
    post:
      summary: Submit a manual payment method
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [method, version]
              properties: {method: {type: string, enum: [wechat_qr, alipay_qr]}, version: {type: integer, minimum: 1}}
      responses: {'200': {description: Payment submitted}}
  /api/v1/payments/{id}/confirm:
    post:
      summary: Confirm a submitted payment
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Payment confirmed}, '403': {description: Provider or administrator required}}
  /api/v1/payments/{id}/evidence:
    post:
      summary: Attach payment evidence
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Evidence submitted}}
  /api/v1/listings/{id}/menu-items:
    post:
      summary: Create a menu item for an owned listing
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'201': {description: Menu item created}, '404': {description: Listing not found or caller is not its owner}}
  /api/v1/menu-items/{id}:
    patch:
      summary: Update a menu item for an owned listing
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Menu item updated}, '404': {description: Menu item not found or caller is not its owner}}
    delete:
      summary: Deactivate a menu item for an owned listing
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Menu item deactivated}, '404': {description: Menu item not found or caller is not its owner}}
  /api/v1/devices:
    post:
      summary: Register a push notification device
      security: [{bearerAuth: []}]
      responses: {'201': {description: Device registered}}
  /api/v1/notifications:
    get:
      summary: List the authenticated user's notifications
      security: [{bearerAuth: []}]
      responses: {'200': {description: Notifications returned}}
  /api/v1/notifications/{id}/read:
    post:
      summary: Mark a notification as read
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Notification acknowledged}}
  /api/v1/providers/me/dashboard:
    get:
      summary: Read the authenticated provider dashboard
      security: [{bearerAuth: []}]
      responses: {'200': {description: Dashboard returned}, '403': {description: Provider access required}}
  /api/v1/providers/me/sales:
    post:
      summary: Record a manual off-platform sale for the authenticated provider
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency, description]
              properties:
                amount: {type: integer, minimum: 0}
                currency: {type: string, minLength: 3, maxLength: 3}
                occurred_at: {type: string, format: date-time}
                description: {type: string, minLength: 1, maxLength: 500}
                customer_label: {type: string, maxLength: 120}
      responses: {'201': {description: Manual sale recorded}, '403': {description: Provider access required}, '422': {description: Invalid sale fields}}
  /api/v1/chat/conversations:
    get:
      summary: List conversations for the authenticated user
      security: [{bearerAuth: []}]
      responses: {'200': {description: Conversations returned}}
    post:
      summary: Create a chat conversation
      security: [{bearerAuth: []}]
      responses: {'201': {description: Conversation created}}
  /api/v1/chat/conversations/{id}/messages:
    get:
      summary: List messages in a conversation
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Messages returned}}
    post:
      summary: Send a conversation message
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'201': {description: Message sent}}
  /api/v1/chat/conversations/{id}/read:
    post:
      summary: Mark conversation messages read
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Read state updated}}
  /api/v1/chat/conversations/{id}/realtime:
    get:
      summary: Open the authenticated conversation WebSocket
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'101': {description: WebSocket upgraded}}
  /api/v1/media/upload:
    post:
      summary: Request a signed media upload URL
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [purpose, mime_type, size_bytes]
              properties:
                purpose: {type: string}
                mime_type: {type: string, enum: [image/jpeg, image/png, image/webp, video/mp4]}
                size_bytes: {type: integer, minimum: 1, maximum: 10485760}
      responses: {'201': {description: Upload URL created}, '422': {description: Upload metadata is invalid}}
  /api/v1/media/{id}:
    get:
      summary: Request an authorized signed media read URL
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Read URL created}, '403': {description: Media access forbidden}}
  /api/v1/community/feed:
    get:
      summary: List community moments
      security: [{bearerAuth: []}]
      responses: {'200': {description: Feed returned}}
  /api/v1/community/moments:
    post:
      summary: Create a community moment
      security: [{bearerAuth: []}]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                body: {type: string, maxLength: 2000}
                visibility: {type: string, enum: [public]}
                media_asset_ids: {type: array, maxItems: 5, items: {type: string}}
      responses: {'201': {description: Moment created}}
  /api/v1/community/moments/{id}/like:
    post:
      summary: Like a community moment
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Moment liked}}
  /api/v1/community/moments/{id}/view:
    post:
      summary: Record a moment view
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: View recorded}}
  /api/v1/community/moments/{id}/counters:
    get:
      summary: Read server-calculated moment counters
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: Counters returned}}
  /api/v1/community/moments/{id}/comments:
    get:
      summary: List comments on a moment
      security: [{bearerAuth: []}]
      parameters:
        - {name: id, in: path, required: true, schema: {type: string}}
        - {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 100, default: 50}}
      responses: {'200': {description: Comments returned}, '422': {description: Invalid limit}}
    post:
      summary: Add a comment to a moment
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'201': {description: Comment created}}
  /api/v1/community/moments/{id}/report:
    post:
      summary: Report a moment
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'201': {description: Report submitted}}
  /api/v1/community/users/{id}/block:
    post:
      summary: Block a user
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: User blocked}}
    delete:
      summary: Unblock a user
      security: [{bearerAuth: []}]
      parameters: [{name: id, in: path, required: true, schema: {type: string}}]
      responses: {'200': {description: User unblocked}}
  /api/v1/admin/exports/reports.csv:
    get:
      summary: Export moderation reports as CSV
      security: [{bearerAuth: []}]
      responses: {'200': {description: CSV export}}

components:
  schemas:
    MeetingParticipant:
      type: object
      required: [meeting_id, user_id, joined_at, livekit_url, participant_token]
      properties:
        meeting_id: {type: string}
        user_id: {type: string}
        joined_at: {type: string, format: date-time}
        livekit_url: {type: string, format: uri, description: WSS URL of the configured LiveKit service}
        participant_token: {type: string, description: Short-lived token scoped to this room and participant}
    Payment:
      type: object
      required: [id, purpose, purpose_id, amount, currency, status, method, version]
      properties:
        id: {type: string}
        purpose: {type: string, enum: [order, listing_subscription]}
        purpose_id: {type: string}
        amount: {type: integer, format: int64, minimum: 0}
        currency: {type: string, minLength: 3, maxLength: 3}
        status: {type: string, enum: [unpaid, payment_submitted, payment_confirmed, payment_rejected, refunded]}
        method: {type: string, enum: [wechat_qr, alipay_qr]}
        version: {type: integer, format: int64, minimum: 1}
        instructions: {type: string, maxLength: 2000, description: Method-specific customer payment instructions. Omitted when not configured.}
    SuccessResponse:
      type: object
      required: [success, data, meta, error, request_id]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          additionalProperties: true
        meta:
          nullable: true
        error:
          nullable: true
        request_id:
          type: string
    RegisterRequest:
      type: object
      required: [username, email, password]
      properties:
        username: {type: string, minLength: 3, maxLength: 32}
        email: {type: string, format: email}
        password: {type: string, minLength: 8, maxLength: 128, format: password}
        display_name: {type: string, maxLength: 120}
        locale: {type: string, example: en}
    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email: {type: string, format: email}
        password: {type: string, format: password}
    RefreshRequest:
      type: object
      required: [refresh_token]
      properties:
        refresh_token: {type: string, minLength: 1}
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
