openapi: 3.0.3
info:
  title: CallPayMin API
  description: |
    CallPayMin Public API for WebRTC calling, meeting rooms, billing, and AI summaries.

    ## Authentication
    All API requests require authentication using an API key:
    ```
    Authorization: Bearer cpm_live_xxx
    ```
    or
    ```
    X-API-Key: cpm_live_xxx
    ```

    ## Payment Modes

    Organizations can choose their payment processing mode:

    ### Self-Managed Mode
    - Organization handles customer billing independently
    - Receives webhooks for `balance.low` events
    - Adds funds to user balances via API
    - Handles expert payouts independently
    - Manages own payout system

    ### Fully Managed Mode
    - CallPayMin handles billing end-to-end (customer charges + revenue split + expert payouts)
    - Auto-charges user's saved card when balance is low
    - KYC verification + 1099 reporting handled by CallPayMin
    - Expert payouts via Airwallex (US ACH) and Wise (global rails)
    - View balance and request payouts from the org dashboard

    ## Subscription Plans

    Pricing varies by payment mode. Self-managed mode has lower prices since you handle billing.

    ### Self-Managed Mode Pricing
    | Plan | Price | Call Min | AI Summaries | Jobs | Rooms | Recording | Concurrent |
    |------|-------|----------|--------------|------|-------|-----------|------------|
    | Free | $0/mo | 100 | 10 | 1 (eval) | eval | 30 min | 1 |
    | Starter | $59/mo | 1,000 | 100 | 10 | 1,000 min | 500 min | 3 |
    | Professional | $179/mo | 2,500 | 250 | 25 | 3,000 min | 1,200 min | 10 |
    | Growth | $349/mo | 10,000 | 1,000 | 100 | 5,000 min | 5,000 min | 50 |
    | Enterprise | Custom | Contract | Contract | Contract | Contract | Contract | Contract |

    ### Managed Mode Pricing
    | Plan | Price | Call Min | AI Summaries | Jobs | Rooms | Recording | Concurrent |
    |------|-------|----------|--------------|------|-------|-----------|------------|
    | Free | $0/mo | 50 | 5 | 1 (eval) | eval | 15 min (eval) | 1 |
    | Starter | $129/mo | 1,000 | 100 | 10 | 1,000 min | 500 min | 3 |
    | Professional | $349/mo | 2,500 | 250 | 25 | 3,000 min | 1,200 min | 10 |
    | Growth | $699/mo | 10,000 | 1,000 | 100 | 5,000 min | 5,000 min | 50 |
    | Enterprise | $1,499/mo or $14,990/yr | 25,000 | 1,000 | 100 | 5,000 min | 5,000 min | 10 |

    Annual billing is monthly × 10 on every paid tier (two months free).
    Allowances renew monthly, including for yearly subscriptions. Room allowances
    are participant-minutes (participants multiplied by minutes), not a maximum
    participant count.

    There are no automatic usage overages, CallPayMin revenue-share fees, or
    payout fees. Upgrade or contact sales when an allowance is exhausted. Card
    processing charges apply when customers add funds.

    ### Revenue Split

    Business decides the expert/company split (default 80/20):
    - `expert`: Percentage going to expert (e.g., 80%)
    - `organization`: Percentage going to the business (e.g., 20%)

    **Money Flow Example ($100 call, managed mode, 80/20 split):**
    ```
    Service amount:              $100.00
    Expert share (80%):           $80.00
    Business share (20%):         $20.00
    CallPayMin service fee:         $0.00
    ```

    ## Rate Limits

    Rate limit headers are included in all responses:
    - `X-RateLimit-Limit`: Maximum requests per minute
    - `X-RateLimit-Remaining`: Remaining requests
    - `X-RateLimit-Reset`: Unix timestamp when limit resets

    ## Webhooks

    Configure webhooks to receive real-time notifications for events:
    - `call.created`, `call.started`, `call.ended`
    - `chat.created`, `chat.message`, `chat.ended`
    - `job.created`, `job.application_received`, `job.application_reviewed`, `job.application_negotiating`, `job.filled`
    - `work_session.started`, `work_session.ended`, `work_session.screenshot_captured`, `work_session.chat_message`, `work_session.file_uploaded`, `work_session.reviewed`
    - `summary.generated`, `transcription.completed`
    - `balance.low`, `expert.verified`

    All webhooks include HMAC-SHA256 signatures in the `X-CallPayMin-Signature` header.

  version: 1.0.0
  contact:
    name: CallPayMin Support
    email: support@callpaymin.io
  license:
    name: Proprietary

servers:
  - url: https://api.callpaymin.io
    description: Production
  - url: https://apidev.callpaymin.io
    description: Development

tags:
  - name: Health
    description: API health and status
  - name: Organization
    description: Organization management and billing
  - name: Webhooks
    description: Webhook configuration and delivery history
  - name: Users
    description: User management and billing
  - name: Experts
    description: Expert management and payouts
  - name: Jobs
    description: Job postings for expert marketplace
  - name: Work Sessions
    description: Screen-sharing work sessions with experts
  - name: Calls
    description: Video/audio call sessions
  - name: Chats
    description: Chat messaging sessions
  - name: Summaries
    description: AI-powered summaries
  - name: Meeting Rooms
    description: Multi-participant meeting rooms with per-minute billing per user

security:
  - BearerAuth: []
  - ApiKeyAuth: []

paths:
  /health:
    get:
      tags: [Health]
      summary: Health check
      description: Check API health and status
      security: []
      responses:
        '200':
          description: API is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  version:
                    type: string
                    example: "1.0.0"
                  timestamp:
                    type: string
                    format: date-time

  # ============================================
  # ORGANIZATION
  # ============================================
  /organization:
    get:
      tags: [Organization]
      summary: Get organization details
      responses:
        '200':
          description: Organization details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Organization'
    put:
      tags: [Organization]
      summary: Update organization
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                webhooks:
                  type: object
      responses:
        '200':
          description: Updated organization

  /organization/plans:
    get:
      tags: [Organization]
      summary: List available plans
      responses:
        '200':
          description: Available subscription plans
          content:
            application/json:
              schema:
                type: object
                properties:
                  plans:
                    type: array
                    items:
                      $ref: '#/components/schemas/Plan'

  /organization/subscription:
    get:
      tags: [Organization]
      summary: Get current subscription
      responses:
        '200':
          description: Current subscription details
    post:
      tags: [Organization]
      summary: Subscribe to plan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [planId]
              properties:
                planId:
                  type: string
                  enum: [free, starter, professional, growth, enterprise]
                  description: |
                    Plan to subscribe to:
                    - `free`: Limited free tier
                    - `starter`: Entry-level paid plan
                    - `professional`: Higher limits, all APIs unlocked
                    - `growth`: For growing businesses (recommended)
                    - `enterprise`: Sales-assisted pricing for large organizations
      responses:
        '201':
          description: Subscription created
    delete:
      tags: [Organization]
      summary: Cancel subscription
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                immediate:
                  type: boolean
                  default: false
      responses:
        '200':
          description: Subscription canceled

  /organization/usage:
    get:
      tags: [Organization]
      summary: Get usage statistics
      responses:
        '200':
          description: Usage stats for current period

  /organization/invoices:
    get:
      tags: [Organization]
      summary: List invoices
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 10
        - name: upcoming
          in: query
          schema:
            type: boolean
      responses:
        '200':
          description: List of invoices

  # ============================================
  # STRIPE BALANCE (Fully Managed Mode Only)
  # ============================================
  /organization/stripe/balance:
    post:
      tags: [Organization]
      summary: Get Stripe Connected Account balance (fully managed mode)
      description: |
        Retrieves the current balance from your organization's Stripe Connected Account.
        Only available for organizations in Fully Managed payment mode.

        Returns available and pending balance across all currencies.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accountId]
              properties:
                accountId:
                  type: string
                  description: Stripe Connected Account ID
      responses:
        '200':
          description: Balance retrieved successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  balance:
                    type: object
                    properties:
                      available:
                        type: array
                        items:
                          type: object
                          properties:
                            amount:
                              type: integer
                              description: Amount in cents
                            currency:
                              type: string
                      pending:
                        type: array
                        items:
                          type: object
                          properties:
                            amount:
                              type: integer
                              description: Amount in cents
                            currency:
                              type: string
                  account:
                    type: object
                    properties:
                      id:
                        type: string
                      type:
                        type: string
                        enum: [standard, express, custom]
                      charges_enabled:
                        type: boolean
                      payouts_enabled:
                        type: boolean
                      details_submitted:
                        type: boolean
                      requirements:
                        type: object
        '400':
          description: Not available (self-managed mode) or missing account
        '403':
          description: Account ID does not match organization

  /organization/stripe/dashboard-link:
    post:
      tags: [Organization]
      summary: Create Stripe Dashboard login link (fully managed mode)
      description: |
        Generates a login link to the Stripe Express Dashboard for managing payouts.

        For Express accounts: Returns a direct login link
        For Standard accounts: Returns the Stripe Dashboard URL (requires separate login)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accountId]
              properties:
                accountId:
                  type: string
                  description: Stripe Connected Account ID
      responses:
        '200':
          description: Dashboard link created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  url:
                    type: string
                    format: uri
                    description: Dashboard login URL
                  accountType:
                    type: string
                    enum: [standard, express, custom]
        '403':
          description: Account ID does not match organization

  /organization/stripe/sync-status:
    post:
      tags: [Organization]
      summary: Sync Stripe account status (fully managed mode)
      description: |
        Manually syncs the latest Stripe Connected Account status from Stripe API to Firestore.
        Useful when webhooks are delayed or failed.

        Updates charges_enabled, payouts_enabled, and details_submitted status.
      responses:
        '200':
          description: Status synced successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  status:
                    type: string
                    enum: [pending, active, verified]
                  account:
                    type: object
                    properties:
                      id:
                        type: string
                      charges_enabled:
                        type: boolean
                      payouts_enabled:
                        type: boolean
                      details_submitted:
                        type: boolean

  /organization/stripe/onboarding-link:
    post:
      tags: [Organization]
      summary: Generate Stripe onboarding link (fully managed mode)
      description: |
        Creates a new Stripe account onboarding link for completing KYC verification.

        The link expires after some time, so generate a fresh one when needed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accountId]
              properties:
                accountId:
                  type: string
                  description: Stripe Connected Account ID
      responses:
        '200':
          description: Onboarding link created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  url:
                    type: string
                    format: uri
                    description: Stripe onboarding URL (expires in 24h)
        '403':
          description: Account ID does not match organization

  /organization/stripe/migrate-to-express:
    post:
      tags: [Organization]
      summary: Migrate from Standard to Express account (fully managed mode)
      description: |
        Migrates an organization's Stripe Connected Account from Standard to Express type.

        **Important:**
        - Creates a new Express account with the same details
        - Updates organization to use the new Express account
        - Preserves reference to old Standard account
        - You'll need to manually delete the old account from Stripe Dashboard
        - Requires completing onboarding for the new Express account

        **Why migrate?**
        - Express accounts provide seamless dashboard access
        - Simplified onboarding process
        - Better integration with platform features
      responses:
        '200':
          description: Migration successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                  newAccountId:
                    type: string
                  oldAccountId:
                    type: string
                  onboardingUrl:
                    type: string
                    format: uri
                  note:
                    type: string
        '400':
          description: No existing account or already an Express account

  /organization/stripe/debug-account:
    get:
      tags: [Organization]
      summary: Debug Stripe account details (fully managed mode)
      description: |
        Returns full Stripe Connected Account details for debugging purposes.
        Includes requirements, capabilities, and both Stripe and Firestore status.
      responses:
        '200':
          description: Account details retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  account:
                    type: object
                    properties:
                      id:
                        type: string
                      type:
                        type: string
                      charges_enabled:
                        type: boolean
                      payouts_enabled:
                        type: boolean
                      details_submitted:
                        type: boolean
                      requirements:
                        type: object
                      capabilities:
                        type: object
                      future_requirements:
                        type: object
                      country:
                        type: string
                      default_currency:
                        type: string
                      created:
                        type: integer
                      email:
                        type: string
                  firestoreStatus:
                    type: object

  # ============================================
  # WEBHOOKS
  # ============================================
  /organization/webhooks:
    get:
      tags: [Webhooks]
      summary: Get webhook configuration
      description: Returns current webhook URLs, secret prefix, and recent delivery history
      responses:
        '200':
          description: Webhook configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  endpoints:
                    type: object
                    properties:
                      callStarted:
                        type: string
                        format: uri
                      callEnded:
                        type: string
                        format: uri
                      chatMessage:
                        type: string
                        format: uri
                      balanceLow:
                        type: string
                        format: uri
                      expertVerified:
                        type: string
                        format: uri
                  secretPrefix:
                    type: string
                    example: "whsec_abc123..."
                  recentDeliveries:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookDelivery'
                  availableEvents:
                    type: array
                    items:
                      type: object
                      properties:
                        type:
                          type: string
                        description:
                          type: string
    put:
      tags: [Webhooks]
      summary: Update webhook URLs
      description: Configure webhook endpoint URLs. All URLs must use HTTPS.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                callStarted:
                  type: string
                  format: uri
                  description: URL for call.created and call.started events
                callEnded:
                  type: string
                  format: uri
                  description: URL for call.ended, summary.generated events
                chatMessage:
                  type: string
                  format: uri
                  description: URL for chat.message events
                balanceLow:
                  type: string
                  format: uri
                  description: URL for balance.low events (important for self-managed mode)
                expertVerified:
                  type: string
                  format: uri
                  description: URL for expert.verified events
            example:
              callStarted: "https://your-app.com/webhooks/callpaymin/calls"
              callEnded: "https://your-app.com/webhooks/callpaymin/calls"
              balanceLow: "https://your-app.com/webhooks/callpaymin/billing"
      responses:
        '200':
          description: Webhook configuration updated
    post:
      tags: [Webhooks]
      summary: Rotate webhook secret
      description: Generate a new webhook signing secret. The new secret is returned only once.
      responses:
        '200':
          description: New webhook secret
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  secret:
                    type: string
                    description: New webhook secret (save securely, shown only once)
                  note:
                    type: string
              example:
                message: "Webhook secret rotated successfully"
                secret: "whsec_abc123xyz789..."
                note: "Save this secret securely. It will not be shown again."

  /organization/webhook-deliveries:
    get:
      tags: [Webhooks]
      summary: Get webhook delivery history
      description: View recent webhook delivery attempts and their status
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, success, failed]
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: Webhook delivery history
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats:
                    type: object
                    properties:
                      total:
                        type: integer
                      success:
                        type: integer
                      failed:
                        type: integer
                      pending:
                        type: integer
                  deliveries:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookDelivery'

  # ============================================
  # USERS (Billing)
  # ============================================
  /billing/users:
    post:
      tags: [Users]
      summary: Create user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [externalId, email, displayName]
              properties:
                externalId:
                  type: string
                  description: Your system's user ID
                email:
                  type: string
                  format: email
                displayName:
                  type: string
                phone:
                  type: string
                initialBalance:
                  type: number
                  default: 0
                metadata:
                  type: object
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
    get:
      tags: [Users]
      summary: List users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /billing/users/{userId}:
    get:
      tags: [Users]
      summary: Get user details
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found

  /billing/users/{userId}/add-funds:
    post:
      tags: [Users]
      summary: Add funds to user balance
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount:
                  type: number
                  minimum: 0.01
                source:
                  type: string
                  enum: [manual, stripe, auto_recharge]
                  default: manual
      responses:
        '200':
          description: Funds added

  /billing/users/{userId}/transactions:
    get:
      tags: [Users]
      summary: Get transaction history
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: Transaction history

  # ============================================
  # PAY-AS-YOU-GO (Managed Mode Only)
  # ============================================
  /billing/users/{userId}/charge:
    post:
      tags: [Users]
      summary: Charge user's card (managed mode)
      description: |
        Charge user's saved payment method to add funds to their balance.

        **Important:** No platform fee is taken on deposits. User gets full credit
        for the amount charged. Fees are only applied on usage (calls/chats).

        **Options:**
        - `passStripeFeeToUser`: If true, charges extra to cover Stripe's 2.9% + $0.30 fee,
          so user gets exactly the requested amount credited.
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount:
                  type: number
                  minimum: 1
                  description: Amount to credit to user's balance
                paymentMethodId:
                  type: string
                  description: Specific payment method to charge (uses default if not specified)
                description:
                  type: string
                  description: Description for the charge
                passStripeFeeToUser:
                  type: boolean
                  default: false
                  description: If true, charge user extra to cover Stripe fee
            example:
              amount: 100
              description: "Balance top-up"
              passStripeFeeToUser: false
      responses:
        '200':
          description: Charge successful, funds added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transaction'
        '400':
          description: Charge not available (self-managed mode) or no payment method

  /billing/users/{userId}/payment-methods:
    get:
      tags: [Users]
      summary: List user's payment methods (managed mode)
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: List of payment methods
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PaymentMethod'
    post:
      tags: [Users]
      summary: Save payment method (managed mode)
      description: Attach a Stripe payment method to the user
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [paymentMethodId]
              properties:
                paymentMethodId:
                  type: string
                  description: Stripe PaymentMethod ID (from client-side Stripe.js)
                setAsDefault:
                  type: boolean
                  default: true
      responses:
        '201':
          description: Payment method saved

  /billing/users/{userId}/payment-methods/{paymentMethodId}:
    delete:
      tags: [Users]
      summary: Remove payment method (managed mode)
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
        - name: paymentMethodId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Payment method removed

  /billing/users/{userId}/setup-intent:
    post:
      tags: [Users]
      summary: Create setup intent for adding payment method (managed mode)
      description: |
        Returns a client secret for Stripe.js to securely collect payment method details.
        Use this on your frontend with Stripe Elements.
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Setup intent created
          content:
            application/json:
              schema:
                type: object
                properties:
                  clientSecret:
                    type: string
                    description: Pass this to stripe.confirmSetup() on frontend

  # ============================================
  # EXPERTS
  # ============================================
  /experts:
    post:
      tags: [Experts]
      summary: Create expert
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [externalId, email, profile, rates]
              properties:
                externalId:
                  type: string
                email:
                  type: string
                  format: email
                profile:
                  type: object
                  properties:
                    firstName:
                      type: string
                    lastName:
                      type: string
                    phone:
                      type: string
                    country:
                      type: string
                rates:
                  type: object
                  properties:
                    perMinute:
                      type: number
                    perMessage:
                      type: number
                    currency:
                      type: string
                      default: USD
      responses:
        '201':
          description: Expert created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Expert'
    get:
      tags: [Experts]
      summary: List experts
      responses:
        '200':
          description: List of experts

  /experts/{expertId}:
    get:
      tags: [Experts]
      summary: Get expert details
      parameters:
        - name: expertId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Expert details
    put:
      tags: [Experts]
      summary: Update expert
      parameters:
        - name: expertId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Expert updated

  /experts/{expertId}/earnings:
    get:
      tags: [Experts]
      summary: Get expert earnings
      parameters:
        - name: expertId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Earnings summary

  /experts/{expertId}/kyc-link:
    post:
      tags: [Experts]
      summary: Generate Stripe KYC onboarding link
      parameters:
        - name: expertId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: KYC link generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                  expiresAt:
                    type: string
                    format: date-time

  /experts/{expertId}/payouts:
    get:
      tags: [Experts]
      summary: List payouts
      parameters:
        - name: expertId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: List of payouts
    post:
      tags: [Experts]
      summary: Trigger payout
      parameters:
        - name: expertId
          in: path
          required: true
          schema:
            type: string
      responses:
        '201':
          description: Payout initiated

  # ============================================
  # JOBS
  # ============================================
  /jobs:
    post:
      tags: [Jobs]
      summary: Create job posting
      description: |
        Post a job to find experts.

        **Plan Limits (active jobs):**
        - Free: 1 (for evaluation)
        - Starter: 10
        - Professional: 25
        - Growth: 100
        - Enterprise: 100 managed; contract allowance self-managed

        Experts can apply using the invite code, which you can share privately or publicly.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, description, skills, ratePerMinute]
              properties:
                title:
                  type: string
                  example: "Senior React Developer"
                description:
                  type: string
                  example: "Need help building dashboard components"
                skills:
                  type: array
                  items:
                    type: string
                  example: ["React", "TypeScript", "Next.js"]
                ratePerMinute:
                  type: number
                  minimum: 0.01
                  example: 5.00
                requirements:
                  type: string
                  example: "5+ years experience, portfolio required"
                metadata:
                  type: object
                  description: Custom metadata for the job
      responses:
        '201':
          description: Job created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        '403':
          description: Jobs API not available on your plan or job limit reached (Free plan limited to 1 job)
    get:
      tags: [Jobs]
      summary: List jobs
      description: Get all jobs for your organization
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [active, closed, filled]
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: List of jobs
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Job'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /jobs/{jobId}:
    get:
      tags: [Jobs]
      summary: Get job details
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        '404':
          description: Job not found
    put:
      tags: [Jobs]
      summary: Update job
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title:
                  type: string
                description:
                  type: string
                skills:
                  type: array
                  items:
                    type: string
                ratePerMinute:
                  type: number
                requirements:
                  type: string
                status:
                  type: string
                  enum: [active, closed, filled]
                metadata:
                  type: object
      responses:
        '200':
          description: Job updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
    delete:
      tags: [Jobs]
      summary: Close job
      description: Mark job as closed (no longer accepting applications)
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job closed successfully

  /jobs/{jobId}/applications:
    get:
      tags: [Jobs]
      summary: List applications for job
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, approved, rejected]
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: List of applications
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/JobApplication'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /jobs/{jobId}/applications/{applicationId}/review:
    post:
      tags: [Jobs]
      summary: Review application
      description: Approve or reject an expert's application
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
        - name: applicationId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum: [approve, reject]
                message:
                  type: string
                  description: Optional message to the expert
      responses:
        '200':
          description: Application reviewed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobApplication'

  /jobs/{jobId}/applications/{applicationId}/negotiate:
    post:
      tags: [Jobs]
      summary: Send negotiation request
      description: |
        Send a rate negotiation request to an expert.

        Use this when you want to counter-offer a different rate than the expert proposed.
        The expert will receive an email notification and can accept, counter, or decline.
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
        - name: applicationId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [negotiationNotes]
              properties:
                negotiationNotes:
                  type: string
                  minLength: 10
                  description: Message to the expert explaining the negotiation
                counterOfferRate:
                  type: number
                  description: Optional counter-offer rate per minute in USD
      responses:
        '200':
          description: Negotiation request sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  applicationId:
                    type: string
                  status:
                    type: string
                    enum: [negotiating]
                  message:
                    type: string
        '400':
          description: Invalid status - application cannot be negotiated
        '404':
          description: Job or application not found

  # ============================================
  # WORK SESSIONS
  # ============================================
  /work-sessions:
    post:
      tags: [Work Sessions]
      summary: Create work session
      description: |
        Start a screen-sharing work session with an expert.

        **Plan Limits (concurrent sessions):**
        - Free: 1
        - Starter: 3
        - Professional: 10
        - Growth: 50
        - Enterprise: 10 managed; contract allowance self-managed

        Customer-to-expert billing applies at the organization's configured rate;
        the subscription limit is on simultaneous sessions.

        **5-Minute Incremental Billing:**
        - Balance is checked before session starts (must afford 5+ minutes)
        - Billing runs every 5 minutes during active sessions
        - Real-time balance/earnings updates for both parties
        - Auto-ends if balance runs out (max 5 min unbilled work loss)
        - Final billing on session end captures remaining minutes

        **Rates:**
        - Screen sharing: Configurable per-minute rate
        - Video call: Additional $0.04/min (platform fee)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobId, questerId, workerId, ratePerMinute]
              properties:
                jobId:
                  type: string
                  description: ID of the job this session is for
                questerId:
                  type: string
                  description: User ID of the employer/client
                workerId:
                  type: string
                  description: User ID of the expert
                ratePerMinute:
                  type: number
                  minimum: 0.01
                  example: 5.00
                  description: Rate per minute for screen sharing
                metadata:
                  type: object
                  description: Custom metadata
      responses:
        '201':
          description: Work session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkSession'
        '403':
          description: Work Sessions API not available on your plan or session limit reached
    get:
      tags: [Work Sessions]
      summary: List work sessions
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [active, completed, cancelled]
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: List of work sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/WorkSession'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /work-sessions/{sessionId}:
    get:
      tags: [Work Sessions]
      summary: Get session details
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
          description: Required for authorization
      responses:
        '200':
          description: Session details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkSession'

  /work-sessions/{sessionId}/end:
    post:
      tags: [Work Sessions]
      summary: End work session
      description: |
        End the session and process final billing.

        **Incremental Billing:**
        Sessions are billed every 5 minutes during active work. When ending:
        1. Any remaining unbilled minutes are charged
        2. Final billing record is created
        3. Session is marked as completed

        **Revenue Split:** 80% goes to expert, 20% to platform (default split).
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                endReason:
                  type: string
                  enum: [completed, cancelled, timeout]
                  default: completed
      responses:
        '200':
          description: Session ended, billing processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkSession'

  /work-sessions/{sessionId}/screenshots:
    post:
      tags: [Work Sessions]
      summary: Upload screenshot
      description: Capture and upload a screenshot during the session
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [width, height]
              properties:
                width:
                  type: integer
                  example: 1920
                height:
                  type: integer
                  example: 1080
      responses:
        '201':
          description: Screenshot uploaded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkSessionScreenshot'
    get:
      tags: [Work Sessions]
      summary: List screenshots
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: List of screenshots
          content:
            application/json:
              schema:
                type: object
                properties:
                  screenshots:
                    type: array
                    items:
                      $ref: '#/components/schemas/WorkSessionScreenshot'
                  hasMore:
                    type: boolean
                  cursor:
                    type: string

  /work-sessions/{sessionId}/chat:
    post:
      tags: [Work Sessions]
      summary: Send chat message
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [senderId, senderRole, text]
              properties:
                senderId:
                  type: string
                senderRole:
                  type: string
                  enum: [quester, worker]
                text:
                  type: string
      responses:
        '201':
          description: Message sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkSessionMessage'
    get:
      tags: [Work Sessions]
      summary: Get chat messages
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: Chat messages
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: array
                    items:
                      $ref: '#/components/schemas/WorkSessionMessage'
                  hasMore:
                    type: boolean

  /work-sessions/{sessionId}/files:
    get:
      tags: [Work Sessions]
      summary: List files or get download URL
      description: |
        List all files shared in the work session chat, or get a download URL for a specific file.

        **To list files:** Call without the `fileKey` parameter
        **To get download URL:** Include the `fileKey` query parameter

        Files are stored for 48 hours after session ends, then automatically cleaned up.
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
        - name: fileKey
          in: query
          required: false
          schema:
            type: string
          description: If provided, returns a signed download URL for this file
      responses:
        '200':
          description: Files list or download URL
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: List of files
                    properties:
                      files:
                        type: array
                        items:
                          type: object
                          properties:
                            key:
                              type: string
                            fileName:
                              type: string
                            size:
                              type: integer
                            uploadedAt:
                              type: string
                              format: date-time
                  - type: object
                    description: Download URL
                    properties:
                      downloadUrl:
                        type: string
                        format: uri
                        description: Signed URL valid for 1 hour
    post:
      tags: [Work Sessions]
      summary: Upload file to chat
      description: |
        Upload a file to share in the work session chat.

        **Limits:**
        - Maximum file size: 10MB
        - Allowed types: code files, documents, images, archives

        Files are stored for 48 hours after session ends.
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fileName, fileData, senderId]
              properties:
                fileName:
                  type: string
                  description: Original file name with extension
                fileData:
                  type: string
                  description: Base64 encoded file content
                fileType:
                  type: string
                  description: MIME type (e.g., application/pdf)
                senderId:
                  type: string
                  description: User ID of the sender
                senderRole:
                  type: string
                  enum: [quester, worker]
      responses:
        '201':
          description: File uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  fileKey:
                    type: string
                    description: Storage key for the file
                  fileName:
                    type: string
                  fileSize:
                    type: integer
                  timestamp:
                    type: integer
        '400':
          description: File too large or type not allowed

  /work-sessions/{sessionId}/review:
    get:
      tags: [Work Sessions]
      summary: Get session review
      description: Get the review submitted for a completed work session
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Review details
          content:
            application/json:
              schema:
                type: object
                properties:
                  hasReview:
                    type: boolean
                  review:
                    type: object
                    nullable: true
                    properties:
                      id:
                        type: string
                      rating:
                        type: integer
                        minimum: 1
                        maximum: 5
                      comment:
                        type: string
                      deliverableReceived:
                        type: boolean
                      createdAt:
                        type: string
                        format: date-time
                      questerName:
                        type: string
                      expertName:
                        type: string
    post:
      tags: [Work Sessions]
      summary: Submit session review
      description: |
        Submit a review for a completed work session.

        **Requirements:**
        - Session must be completed (not active)
        - Session cannot already have a review

        The review updates the expert's rating summary and is visible on their profile.
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
        - name: questerId
          in: query
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rating]
              properties:
                rating:
                  type: integer
                  minimum: 1
                  maximum: 5
                  description: Star rating (1-5)
                comment:
                  type: string
                  description: Optional review text
                deliverableReceived:
                  type: boolean
                  default: true
                  description: Whether the work deliverable was received
                reviewerName:
                  type: string
                  description: Name to display on the review
      responses:
        '201':
          description: Review submitted
          content:
            application/json:
              schema:
                type: object
                properties:
                  reviewId:
                    type: string
                  rating:
                    type: integer
                  message:
                    type: string
        '400':
          description: Session not completed or already reviewed

  # ============================================
  # CALLS
  # ============================================
  /calls:
    post:
      tags: [Calls]
      summary: Create call session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [participants, billing]
              properties:
                participants:
                  type: array
                  minItems: 2
                  items:
                    type: object
                    required: [externalId, displayName, role]
                    properties:
                      externalId:
                        type: string
                      displayName:
                        type: string
                      role:
                        type: string
                        enum: [client, expert]
                billing:
                  type: object
                  required: [payerId, ratePerMinute]
                  properties:
                    payerId:
                      type: string
                      description: externalId of the payer
                    ratePerMinute:
                      type: number
                    currency:
                      type: string
                      default: USD
                config:
                  type: object
                  properties:
                    video:
                      type: boolean
                      default: true
                    audio:
                      type: boolean
                      default: true
                    recordingEnabled:
                      type: boolean
                      default: false
                    aiSummaryEnabled:
                      type: boolean
                      default: false
                metadata:
                  type: object
      responses:
        '201':
          description: Call created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Call'
    get:
      tags: [Calls]
      summary: List calls
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, active, ended]
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: List of calls

  /calls/{callId}:
    get:
      tags: [Calls]
      summary: Get call details
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Call details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Call'

  /calls/{callId}/start:
    post:
      tags: [Calls]
      summary: Start call (billing begins)
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Call started

  /calls/{callId}/end:
    post:
      tags: [Calls]
      summary: End call (final billing)
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Call ended with billing summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  status:
                    type: string
                  duration:
                    type: object
                    properties:
                      totalSeconds:
                        type: integer
                      billableSeconds:
                        type: integer
                  cost:
                    type: object
                    properties:
                      total:
                        type: number

  /calls/{callId}/recording:
    get:
      tags: [Calls]
      summary: Get recording info
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Recording info
    post:
      tags: [Calls]
      summary: Upload recording
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fileUrl, durationSeconds]
              properties:
                fileUrl:
                  type: string
                  format: uri
                fileSize:
                  type: integer
                durationSeconds:
                  type: integer
                format:
                  type: string
                  enum: [webm, mp4, mp3]
                  default: webm
      responses:
        '201':
          description: Recording uploaded

  /calls/{callId}/transcription:
    get:
      tags: [Calls]
      summary: Get transcription
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Transcription data
    post:
      tags: [Calls]
      summary: Generate transcription
      description: |
        Transcribes the call recording using AI.

        **Providers (fallback order):**
        1. Deepgram (Nova-2)
        2. AssemblyAI
        3. OpenAI Whisper

        **Cost:** $0.05/minute
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      responses:
        '201':
          description: Transcription generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  transcript:
                    type: string
                  segments:
                    type: array
                    items:
                      type: object
                      properties:
                        start:
                          type: number
                        end:
                          type: number
                        speaker:
                          type: string
                        text:
                          type: string
                  billing:
                    type: object
                    properties:
                      cost:
                        type: number
                      billed:
                        type: boolean

  # ============================================
  # CHATS
  # ============================================
  /chats:
    post:
      tags: [Chats]
      summary: Create chat session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [participants, billing]
              properties:
                participants:
                  type: array
                  items:
                    type: object
                billing:
                  type: object
                  properties:
                    payerId:
                      type: string
                    ratePerMinute:
                      type: number
                    freeTier:
                      type: object
                      properties:
                        messages:
                          type: integer
                          default: 10
      responses:
        '201':
          description: Chat created
    get:
      tags: [Chats]
      summary: List chats
      responses:
        '200':
          description: List of chats

  /chats/{chatId}:
    get:
      tags: [Chats]
      summary: Get chat details
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Chat details

  /chats/{chatId}/messages:
    post:
      tags: [Chats]
      summary: Send message
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [senderExternalId, content]
              properties:
                senderExternalId:
                  type: string
                content:
                  type: string
                contentType:
                  type: string
                  default: text
      responses:
        '201':
          description: Message sent
    get:
      tags: [Chats]
      summary: Get messages
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: List of messages

  /chats/{chatId}/end:
    post:
      tags: [Chats]
      summary: End chat session
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Chat ended

  # ============================================
  # SUMMARIES
  # ============================================
  /summaries/calls/{callId}:
    post:
      tags: [Summaries]
      summary: Generate call summary
      description: |
        Generates an AI summary of the call.

        If a transcript is available, it will be used for a more accurate summary.

        **Providers (fallback order):**
        1. Cerebras (llama3.1-8b)
        2. OpenAI (gpt-4o-mini)
        3. Claude (claude-3-haiku)

        **Cost:** $0.10/summary
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      responses:
        '201':
          description: Summary generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Summary'
    get:
      tags: [Summaries]
      summary: Get call summary
      parameters:
        - name: callId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Summary data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Summary'

  /summaries/chats/{chatId}:
    post:
      tags: [Summaries]
      summary: Generate chat summary
      description: |
        Generates an AI summary of the chat conversation.

        **Cost:** $0.10/summary
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
      responses:
        '201':
          description: Summary generated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Summary'
    get:
      tags: [Summaries]
      summary: Get chat summary
      parameters:
        - name: chatId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Summary data

  # ── Meeting Rooms ────────────────────────────────

  /rooms:
    post:
      tags: [Meeting Rooms]
      summary: Create meeting room
      description: |
        Create a multi-participant meeting room hosted by an expert.

        **Plan Limits (room participant-minutes / month):**
        - Free: eval only
        - Starter: 1,000
        - Professional: 3,000
        - Growth: 5,000
        - Enterprise: 5,000 managed; contract allowance self-managed

        **Per-Minute Billing Per User:**
        - Each participant is billed per minute at the expert's rate
        - Balance checked on join (must afford at least 5 minutes)
        - Billing increments every 5 minutes per participant
        - Auto-disconnect if participant balance runs out
        - Expert earns from ALL participants simultaneously

        **Room Types:**
        - **Instant**: Created with status `active`, ready for participants immediately
        - **Scheduled**: Created with `scheduledTime`, starts in `scheduled` status. Must call `/rooms/{roomId}/start` when ready.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expertExternalId, name]
              properties:
                expertExternalId:
                  type: string
                  description: Your external ID for the expert hosting the room
                name:
                  type: string
                  description: Room name/title
                  example: "JavaScript Office Hours"
                description:
                  type: string
                  description: Room description
                maxParticipants:
                  type: integer
                  minimum: 1
                  maximum: 20
                  default: 5
                  description: Maximum guest participants, from 1 to 20 (expert host not counted; 21 total people maximum)
                scheduledTime:
                  type: string
                  format: date-time
                  description: If provided, creates a scheduled room (otherwise instant/active)
                scheduledEndTime:
                  type: string
                  format: date-time
                  description: Optional scheduled end time
                settings:
                  type: object
                  properties:
                    requireApproval:
                      type: boolean
                      default: false
                    allowScreenShare:
                      type: boolean
                      default: true
                    muteOnJoin:
                      type: boolean
                      default: false
                    videoOnJoin:
                      type: boolean
                      default: true
                metadata:
                  type: object
                  description: Custom metadata
      responses:
        '201':
          description: Room created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Room'
        '404':
          description: Expert not found
        '409':
          description: Expert already has an active room
    get:
      tags: [Meeting Rooms]
      summary: List meeting rooms
      description: List rooms with optional filters for status and expert.
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [scheduled, active, closed]
          description: Filter by room status
        - name: expertExternalId
          in: query
          schema:
            type: string
          description: Filter by expert
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: List of rooms
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Room'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /rooms/{roomId}:
    get:
      tags: [Meeting Rooms]
      summary: Get room details
      description: Get full room details including participants, billing sessions, and stats.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Room details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Room'
        '404':
          description: Room not found
    delete:
      tags: [Meeting Rooms]
      summary: Delete scheduled room
      description: |
        Delete a room that is in `scheduled` status. Only the room creator (expert) can delete it.
        Active or closed rooms cannot be deleted.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expertExternalId]
              properties:
                expertExternalId:
                  type: string
                  description: External ID of the expert who created the room
      responses:
        '200':
          description: Room deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
        '400':
          description: Only scheduled rooms can be deleted
        '403':
          description: Only the room creator can delete it
        '404':
          description: Room not found

  /rooms/{roomId}/start:
    post:
      tags: [Meeting Rooms]
      summary: Start scheduled room
      description: |
        Transition a room from `scheduled` to `active` status. Only the room creator (expert) can start it.
        Once active, participants can join.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expertExternalId]
              properties:
                expertExternalId:
                  type: string
                  description: External ID of the expert who created the room
      responses:
        '200':
          description: Room started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Room'
        '400':
          description: Room is not in scheduled state
        '403':
          description: Only the room creator can start it
        '404':
          description: Room not found
        '409':
          description: Expert already has an active room

  /rooms/{roomId}/join:
    post:
      tags: [Meeting Rooms]
      summary: Join room as participant
      description: |
        Join an active room as a participant. The participant's balance is checked before joining
        (must afford at least 5 minutes at the expert's rate).

        **Returns SFU connection details** (Mediasoup or Cloudflare) for establishing the WebRTC connection.

        **Billing starts immediately** upon joining — the participant is charged per minute.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [participantExternalId]
              properties:
                participantExternalId:
                  type: string
                  description: Your external ID for the participant (user)
                participantName:
                  type: string
                  description: Display name (optional, falls back to user record)
      responses:
        '200':
          description: Joined successfully with SFU connection details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JoinRoomResponse'
        '400':
          description: Room is not active or waiting for host
        '402':
          description: Insufficient balance (minimum 5 minutes required)
        '404':
          description: Room or participant not found
        '409':
          description: Room is at capacity

  /rooms/{roomId}/leave:
    post:
      tags: [Meeting Rooms]
      summary: Leave room
      description: |
        Participant leaves the room. Final billing is processed for any remaining unbilled time.
        Returns the total duration and charges for this participant's session.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [participantExternalId]
              properties:
                participantExternalId:
                  type: string
                  description: External ID of the participant leaving
                reason:
                  type: string
                  description: Optional leave reason
      responses:
        '200':
          description: Left room, billing finalized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LeaveRoomResponse'
        '404':
          description: Room or participant not found

  /rooms/{roomId}/close:
    post:
      tags: [Meeting Rooms]
      summary: Close room
      description: |
        Close the room and end all active sessions. Only the room creator (expert) can close it.

        **Closing a room:**
        1. Processes final billing for ALL remaining participants
        2. Ends all active billing sessions
        3. Marks room as `closed`
        4. Returns aggregate billing results

        Expert earns from the combined billing of all participants.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expertExternalId]
              properties:
                expertExternalId:
                  type: string
                  description: External ID of the expert closing the room
      responses:
        '200':
          description: Room closed, all billing finalized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CloseRoomResponse'
        '400':
          description: Room is already closed
        '403':
          description: Only the room creator can close it
        '404':
          description: Room not found

  /rooms/{roomId}/heartbeat:
    post:
      tags: [Meeting Rooms]
      summary: Participant heartbeat
      description: |
        Send a liveness ping for a participant. Should be called every 30 seconds.

        **Heartbeat checks:**
        - Validates participant is still in the room
        - Detects stale participants (no heartbeat for 90+ seconds)
        - Returns `shouldDisconnect: true` if participant should leave (e.g., balance exhausted)

        Always returns 200 (even on error) to avoid disrupting the client connection loop.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [participantExternalId]
              properties:
                participantExternalId:
                  type: string
                  description: External ID of the participant
      responses:
        '200':
          description: Heartbeat response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HeartbeatResponse'

  /rooms/{roomId}/billing-increment:
    post:
      tags: [Meeting Rooms]
      summary: Process billing increment
      description: |
        Process a 5-minute billing increment for a participant. Called automatically by the client
        SDK every 5 minutes during an active session.

        **Billing flow:**
        1. Calculates minutes since last billing
        2. Charges participant's balance (rate × minutes)
        3. Credits expert earnings (80% of charge)
        4. Returns updated balance and `shouldDisconnect` if balance exhausted

        Always returns 200 to avoid disrupting the billing loop.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [participantExternalId]
              properties:
                participantExternalId:
                  type: string
                  description: External ID of the participant being billed
      responses:
        '200':
          description: Billing increment result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingIncrementResponse'

  /rooms/{roomId}/remove-participant:
    post:
      tags: [Meeting Rooms]
      summary: Remove participant from room
      description: |
        Expert removes a participant from the room. Final billing is processed for the removed participant.
        Only the room creator (expert) can remove participants.
      parameters:
        - name: roomId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [expertExternalId, participantExternalId]
              properties:
                expertExternalId:
                  type: string
                  description: External ID of the expert (room host)
                participantExternalId:
                  type: string
                  description: External ID of the participant to remove
      responses:
        '200':
          description: Participant removed, billing finalized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LeaveRoomResponse'
        '403':
          description: Only the room creator can remove participants
        '404':
          description: Room or participant not found

  /api/v1/calls/{callId}/upload-url:
    post:
      tags: [Recording]
      summary: Get signed upload URL for call recording
      description: Returns a signed Firebase Storage URL for uploading call recording. URL expires in 15 minutes.
      parameters:
        - name: callId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                format: { type: string, example: webm }
                contentType: { type: string, example: audio/webm }
      responses:
        '200':
          description: Signed upload URL
        '404':
          description: Call not found

  /api/v1/rooms/{roomId}/upload-url:
    post:
      tags: [Recording]
      summary: Get signed upload URL for meeting recording
      parameters:
        - name: roomId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                participantExternalId: { type: string }
                format: { type: string }
                contentType: { type: string }
      responses:
        '200':
          description: Signed upload URL

  /api/v1/rooms/{roomId}/sfu-proxy:
    post:
      tags: [Meeting Rooms]
      summary: Proxy requests to Cloudflare Calls SFU API
      description: Used by WebRTC clients for SFU session management and track negotiation
      parameters:
        - name: roomId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: SFU API response

  /api/v1/summaries/rooms/{roomId}:
    post:
      tags: [AI Summaries]
      summary: Generate AI summary for a meeting room
      description: Uses transcription if available. Summary stored on room document for all participants.
      parameters:
        - name: roomId
          in: path
          required: true
          schema: { type: string }
      responses:
        '201':
          description: Summary generated
        '404':
          description: Room not found
    get:
      tags: [AI Summaries]
      summary: Get existing room AI summary
      parameters:
        - name: roomId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Room summary
        '404':
          description: Summary not found

  /api/v1/jobs/{jobId}/apply:
    post:
      tags: [Jobs]
      summary: Expert applies for a job
      description: Creates a pending application. Expert must exist in the organization.
      parameters:
        - name: jobId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [expertExternalId]
              properties:
                expertExternalId: { type: string }
                coverLetter: { type: string }
                proposedRatePerMin: { type: number }
      responses:
        '201':
          description: Application submitted
        '404':
          description: Job or expert not found
        '400':
          description: Already applied or job not active

  /api/v1/webhooks/receive:
    post:
      tags: [Webhooks]
      summary: Receive webhook events
      description: Stores incoming webhook events for dashboard display
      requestBody:
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Event received
    get:
      tags: [Webhooks]
      summary: List received webhook events
      description: Returns most recent webhook events (requires API key)
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 50 }
      responses:
        '200':
          description: List of events

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: "API key as Bearer token: `Authorization: Bearer cpm_live_xxx`"
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key header: `X-API-Key: cpm_live_xxx`"

  schemas:
    Organization:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        email:
          type: string
        plan:
          type: string
          enum: [free, starter, professional, growth, enterprise]
        payments:
          type: object
          properties:
            mode:
              type: string
              enum: [self_managed, fully_managed]
              description: |
                Payment processing mode:
                - `self_managed`: Organization handles customer billing and payouts
                - `fully_managed`: CallPayMin handles customer funding, revenue allocation, and payouts
            revenueShare:
              type: object
              properties:
                expert:
                  type: number
                  description: Percentage for expert (default 80)
                platform:
                  type: number
                  description: Legacy field name for the organization's share (default 20), not a CallPayMin fee
        earnings:
          type: object
          description: Organization's earnings from calls/chats (managed mode)
          properties:
            lifetime:
              type: object
              properties:
                gross:
                  type: number
                  description: Total service earnings before expert/company allocation
                net:
                  type: number
                  description: Organization share after expert allocation
            pendingPayout:
              type: number
              description: Amount ready to withdraw
            lastPayoutAt:
              type: string
              format: date-time
            callpayminFeesTotal:
              type: number
              description: Total fees paid to CallPayMin
        stripe:
          type: object
          description: Stripe Connect account (fully managed mode only)
          properties:
            accountId:
              type: string
              description: Stripe Connected Account ID
            accountType:
              type: string
              enum: [standard, express]
              description: Type of Stripe account
            status:
              type: string
              enum: [pending, active, verified, restricted]
              description: Overall account status
            payoutsEnabled:
              type: boolean
              description: Whether payouts are enabled
            chargesEnabled:
              type: boolean
              description: Whether charges are enabled
            onboardingCompleted:
              type: boolean
              description: Whether KYC onboarding is complete
            previousAccountId:
              type: string
              description: Previous account ID if migrated
        webhooks:
          type: object
          properties:
            callStarted:
              type: string
              format: uri
            callEnded:
              type: string
              format: uri
            chatMessage:
              type: string
              format: uri
            balanceLow:
              type: string
              format: uri
            expertVerified:
              type: string
              format: uri
        status:
          type: string
          enum: [active, suspended]

    WebhookDelivery:
      type: object
      properties:
        id:
          type: string
        eventId:
          type: string
        url:
          type: string
          format: uri
        status:
          type: string
          enum: [pending, success, failed]
        attempts:
          type: integer
        lastAttemptAt:
          type: string
          format: date-time
        nextRetryAt:
          type: string
          format: date-time
        response:
          type: object
          properties:
            statusCode:
              type: integer
            body:
              type: string
        error:
          type: string
        createdAt:
          type: string
          format: date-time

    WebhookEvent:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum:
            - call.created
            - call.started
            - call.ended
            - chat.created
            - chat.message
            - chat.ended
            - job.created
            - job.application_received
            - job.application_reviewed
            - job.application_negotiating
            - job.filled
            - work_session.started
            - work_session.ended
            - work_session.screenshot_captured
            - work_session.chat_message
            - work_session.file_uploaded
            - work_session.reviewed
            - summary.generated
            - transcription.completed
            - balance.low
            - balance.recharged
            - expert.verified
            - expert.payout
        data:
          type: object
          description: Event-specific payload
        created:
          type: integer
          description: Unix timestamp

    Plan:
      type: object
      properties:
        id:
          type: string
          enum: [free, starter, professional, growth, enterprise]
        name:
          type: string
          example: "Growth"
        description:
          type: string
        pricing:
          type: object
          properties:
            monthly:
              type: number
              description: Monthly price in USD (-1 for custom pricing)
            annual:
              type: number
              description: Annual price in USD (discounted)
        limits:
          type: object
          properties:
            callMinutes:
              type: integer
              description: Included call minutes per month (-1 for unlimited)
            chatMessages:
              type: integer
              description: Included chat messages per month
            aiSummaries:
              type: integer
              description: Included AI summaries per month
            requestsPerMinute:
              type: integer
              description: API rate limit
            recordingMinutes:
              type: integer
              description: Included recording minutes per month
            transcriptionMinutes:
              type: integer
              description: Included transcription minutes per month
            roomMinutes:
              type: integer
              description: Included room participant-minutes per month
            jobs:
              type: integer
              description: Maximum active jobs
            apiKeys:
              type: integer
              description: Maximum active API keys (-1 for a contracted unlimited allowance)
        overageRates:
          type: object
          description: Cost per unit when exceeding limits
          properties:
            callMinutes:
              type: number
            chatMessages:
              type: number
            aiSummaries:
              type: number
            recordingMinutes:
              type: number
            transcriptionMinutes:
              type: number
        features:
          type: array
          items:
            type: string
        recommended:
          type: boolean
          description: Whether this plan is recommended

    User:
      type: object
      properties:
        id:
          type: string
        externalId:
          type: string
        email:
          type: string
        displayName:
          type: string
        balance:
          type: object
          properties:
            available:
              type: number
            pending:
              type: number
            currency:
              type: string
        autoRecharge:
          type: object
          properties:
            enabled:
              type: boolean
            threshold:
              type: number
            amount:
              type: number
        status:
          type: string
          enum: [active, suspended]

    Expert:
      type: object
      properties:
        id:
          type: string
        externalId:
          type: string
        email:
          type: string
        profile:
          type: object
        rates:
          type: object
        stripe:
          type: object
          properties:
            accountId:
              type: string
            status:
              type: string
            payoutsEnabled:
              type: boolean
        earnings:
          type: object
        status:
          type: string

    Call:
      type: object
      properties:
        id:
          type: string
        participants:
          type: array
          items:
            type: object
        webrtc:
          type: object
          properties:
            roomId:
              type: string
            signalingUrl:
              type: string
            turnServers:
              type: array
        billing:
          type: object
        duration:
          type: object
        cost:
          type: object
        recording:
          type: object
        transcription:
          type: object
        aiSummary:
          type: object
        status:
          type: string
          enum: [pending, active, ended, failed]
        createdAt:
          type: string
          format: date-time

    Summary:
      type: object
      properties:
        id:
          type: string
        callId:
          type: string
        chatId:
          type: string
        status:
          type: string
          enum: [pending, processing, completed, failed]
        summary:
          type: string
        generatedAt:
          type: string
          format: date-time
        billing:
          type: object
          properties:
            cost:
              type: number
            billed:
              type: boolean

    Pagination:
      type: object
      properties:
        hasMore:
          type: boolean
        cursor:
          type: string

    Transaction:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
        expertId:
          type: string
        type:
          type: string
          enum: [credit, debit, refund]
        category:
          type: string
          enum: [deposit, call, chat, summary, refund]
        amount:
          type: number
          description: Positive for credit, negative for debit
        currency:
          type: string
        balanceBefore:
          type: number
        balanceAfter:
          type: number
        reference:
          type: object
          properties:
            type:
              type: string
            id:
              type: string
        split:
          type: object
          description: Revenue split details (for call/chat transactions)
          properties:
            gross:
              type: number
            platformFee:
              type: number
            expertShare:
              type: number
            businessShare:
              type: number
            expertNet:
              type: number
            businessNet:
              type: number
            callpayminProfit:
              type: number
        paymentMode:
          type: string
          enum: [self_managed, managed]
        description:
          type: string
        status:
          type: string
          enum: [completed, failed]
        createdAt:
          type: string
          format: date-time

    PaymentMethod:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
        type:
          type: string
          example: card
        card:
          type: object
          properties:
            brand:
              type: string
              example: visa
            last4:
              type: string
              example: "4242"
            expMonth:
              type: integer
            expYear:
              type: integer
        isDefault:
          type: boolean
        createdAt:
          type: string
          format: date-time

    Job:
      type: object
      properties:
        id:
          type: string
        organizationId:
          type: string
        title:
          type: string
          example: "Senior React Developer"
        description:
          type: string
        skills:
          type: array
          items:
            type: string
          example: ["React", "TypeScript", "Next.js"]
        ratePerMinute:
          type: number
          example: 5.00
        currency:
          type: string
          default: USD
        requirements:
          type: string
        status:
          type: string
          enum: [active, closed, filled]
        inviteCode:
          type: string
          description: 8-character code experts use to apply
          example: "AB12CD34"
        applicationCount:
          type: integer
          description: Number of applications received
        metadata:
          type: object
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    JobApplication:
      type: object
      properties:
        id:
          type: string
        jobId:
          type: string
        expertId:
          type: string
        expertName:
          type: string
        expertEmail:
          type: string
        status:
          type: string
          enum: [pending, approved, rejected, negotiating, revoked]
        coverLetter:
          type: string
        portfolio:
          type: string
          format: uri
        proposedRatePerMin:
          type: number
          description: Expert's proposed rate per minute in USD
        counterOfferRate:
          type: number
          description: Employer's counter-offer rate per minute
        agreedRatePerMin:
          type: number
          description: Final agreed rate after negotiation
        negotiationNotes:
          type: string
          description: Negotiation message from employer
        negotiationRequestedAt:
          type: string
          format: date-time
        reviewedAt:
          type: string
          format: date-time
        reviewMessage:
          type: string
        appliedAt:
          type: string
          format: date-time

    WorkSession:
      type: object
      properties:
        id:
          type: string
        organizationId:
          type: string
        jobId:
          type: string
        jobTitle:
          type: string
        questerId:
          type: string
          description: Employer/client user ID
        questerName:
          type: string
        workerId:
          type: string
          description: Expert user ID
        workerName:
          type: string
        status:
          type: string
          enum: [active, completed, cancelled]
        startedAt:
          type: string
          format: date-time
        endedAt:
          type: string
          format: date-time
        duration:
          type: number
          description: Total duration in minutes
        ratePerMinute:
          type: number
          description: Rate for screen sharing
          example: 5.00
        totalCost:
          type: number
          description: Total cost charged to quester
        currency:
          type: string
          default: USD
        screenshotCount:
          type: integer
        messageCount:
          type: integer
        videoCallMinutes:
          type: number
          description: Optional video call duration (adds $0.04/min)
        endReason:
          type: string
          enum: [completed, cancelled, timeout, insufficient_funds]
        # Incremental billing fields
        lastBilledAt:
          type: string
          format: date-time
          description: Timestamp of last billing increment
        totalBilledSoFar:
          type: number
          description: Total amount billed through increments
        totalWorkerPaidSoFar:
          type: number
          description: Total paid to worker (80% of billed)
        billingCompleted:
          type: boolean
          description: Whether final billing has been processed
        insufficientBalance:
          type: boolean
          description: True if session ended due to low balance
        billing:
          type: object
          description: Final billing summary
          properties:
            subtotal:
              type: number
              description: Total charged to quester
            platformFee:
              type: number
              description: 20% platform fee
            workerPayout:
              type: number
              description: 80% worker payout
        metadata:
          type: object
        # Review fields
        hasReview:
          type: boolean
          description: Whether the session has been reviewed
        reviewId:
          type: string
          description: ID of the review document
        reviewRating:
          type: integer
          minimum: 1
          maximum: 5
          description: Rating given in review (1-5)
        reviewedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    WorkSessionScreenshot:
      type: object
      properties:
        id:
          type: string
        sessionId:
          type: string
        storageKey:
          type: string
          description: Firebase Storage path
        capturedAt:
          type: string
          format: date-time
        width:
          type: integer
        height:
          type: integer

    WorkSessionMessage:
      type: object
      properties:
        id:
          type: string
        sessionId:
          type: string
        senderId:
          type: string
        senderName:
          type: string
        senderRole:
          type: string
          enum: [quester, worker]
        text:
          type: string
        timestamp:
          type: string
          format: date-time
        read:
          type: boolean
        # File attachment fields
        isFile:
          type: boolean
          description: True if this message is a file share
        fileName:
          type: string
          description: Original file name
        fileKey:
          type: string
          description: Storage key for downloading the file
        fileSize:
          type: integer
          description: File size in bytes

    # ── Meeting Room Schemas ─────────────────────

    Room:
      type: object
      properties:
        id:
          type: string
          description: Unique room ID
        name:
          type: string
          description: Room name/title
        description:
          type: string
        expertExternalId:
          type: string
          description: External ID of the expert hosting the room
        expertName:
          type: string
        expertRate:
          type: number
          description: Expert's per-minute rate
          example: 2.50
        status:
          type: string
          enum: [scheduled, active, closed]
        currentParticipants:
          type: integer
          description: Number of participants currently in the room
        maxParticipants:
          type: integer
          minimum: 1
          maximum: 20
          description: Maximum guest participants (expert host not counted; 21 total people maximum)
        meetingUrl:
          type: string
          description: URL for joining the room
        sfuProvider:
          type: string
          enum: [mediasoup, cloudflare]
          description: SFU provider for WebRTC
        scheduledTime:
          type: string
          format: date-time
          description: Scheduled start time (if scheduled room)
        scheduledEndTime:
          type: string
          format: date-time
          description: Scheduled end time (optional)
        participants:
          type: array
          items:
            $ref: '#/components/schemas/RoomParticipant'
          description: Currently connected participants
        activeBillingSessions:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/RoomBillingSession'
          description: Active billing sessions keyed by participant internal ID
        settings:
          type: object
          properties:
            requireApproval:
              type: boolean
            allowScreenShare:
              type: boolean
            muteOnJoin:
              type: boolean
            videoOnJoin:
              type: boolean
        stats:
          type: object
          properties:
            totalEarnings:
              type: number
              description: Total expert earnings
            totalRevenue:
              type: number
              description: Total revenue (before split)
            totalPlatformFees:
              type: number
            totalDuration:
              type: number
              description: Total participant-minutes
            totalSessions:
              type: integer
              description: Total billing sessions completed
            uniqueParticipants:
              type: integer
        metadata:
          type: object
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        closedAt:
          type: string
          format: date-time

    RoomParticipant:
      type: object
      properties:
        id:
          type: string
          description: Internal participant ID
        externalId:
          type: string
          description: Your external ID for this participant
        displayName:
          type: string
        role:
          type: string
          enum: [host, participant]
        joinedAt:
          type: string
          format: date-time

    RoomBillingSession:
      type: object
      properties:
        participantExternalId:
          type: string
        participantName:
          type: string
        startTimestamp:
          type: number
          description: Unix timestamp when billing started
        lastBilledTimestamp:
          type: number
          description: Unix timestamp of last billing increment
        lastHeartbeat:
          type: number
          description: Unix timestamp of last heartbeat
        expertRate:
          type: number
          description: Rate per minute
        amountChargedSoFar:
          type: number
          description: Total charged to this participant so far
        status:
          type: string
          enum: [active, ended_insufficient_balance]

    JoinRoomResponse:
      type: object
      properties:
        roomId:
          type: string
        participantId:
          type: string
          description: Internal participant ID
        sfuProvider:
          type: string
          enum: [mediasoup, cloudflare]
        sfuToken:
          type: string
          description: Authentication token for the SFU server
        mediasoupUrl:
          type: string
          description: Mediasoup server URL (if using mediasoup)
        cloudflareAppId:
          type: string
          description: Cloudflare app ID (if using cloudflare)
        cloudflareApiBase:
          type: string
          description: Cloudflare API base URL (if using cloudflare)
        cloudflareProxyUrl:
          type: string
          description: Cloudflare proxy URL (if using cloudflare)
        balance:
          type: number
          description: Participant's current balance
        expertRate:
          type: number
          description: Expert's per-minute rate
        affordableMinutes:
          type: number
          description: How many minutes the participant can afford

    LeaveRoomResponse:
      type: object
      properties:
        roomId:
          type: string
        participantExternalId:
          type: string
        duration:
          type: number
          description: Total duration in minutes
        totalCharged:
          type: number
          description: Total amount charged
        reason:
          type: string
          description: Leave reason

    CloseRoomResponse:
      type: object
      properties:
        roomId:
          type: string
        status:
          type: string
          example: closed
        totalEarnings:
          type: number
          description: Total expert earnings from all participants
        totalRevenue:
          type: number
          description: Total revenue collected
        totalDuration:
          type: number
          description: Total participant-minutes across all sessions
        billingResults:
          type: array
          items:
            type: object
            properties:
              participantExternalId:
                type: string
              duration:
                type: number
                description: Duration in minutes
              totalCharged:
                type: number

    HeartbeatResponse:
      type: object
      properties:
        ok:
          type: boolean
        shouldDisconnect:
          type: boolean
          description: If true, participant should leave the room
        reason:
          type: string
          description: Reason for disconnect (e.g., insufficient_balance)
        staleParticipants:
          type: array
          items:
            type: string
          description: External IDs of stale participants (host only)

    BillingIncrementResponse:
      type: object
      properties:
        success:
          type: boolean
        minutesBilled:
          type: number
          description: Minutes billed in this increment
        actualCharge:
          type: number
          description: Amount charged in this increment
        newBalance:
          type: number
          description: Participant's new balance after charge
        totalCharged:
          type: number
          description: Total charged in this session so far
        shouldDisconnect:
          type: boolean
          description: If true, participant should leave (balance exhausted)
        reason:
          type: string
        message:
          type: string

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
