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 | Translation | Concurrent |
    |------|-------|----------|--------------|------|-------|-----------|-------------|------------|
    | Free | $0/mo | 100 | 10 | 1 (eval) | eval | 30 min | 30 min | 1 |
    | Starter | $99/mo | 1,000 | 100 | 10 | 1,000 min | 500 min | 500 min | 3 |
    | Professional | $249/mo | 2,500 | 250 | 25 | 3,000 min | 1,200 min | 1,200 min | 10 |
    | Growth | $599/mo | 10,000 | 1,000 | 100 | 5,000 min | 5,000 min | 2,000 min | 50 |
    | Enterprise | Contact Sales | Contract | Contract | Contract | Contract | Contract | Contract | Contract |

    ### Managed Mode Pricing
    | Plan | Price | Call Min | AI Summaries | Jobs | Rooms | Recording | Translation | Concurrent |
    |------|-------|----------|--------------|------|-------|-----------|-------------|------------|
    | Free | $0/mo | 50 | 5 | 1 (eval) | eval | 15 min (eval) | 15 min | 1 |
    | Starter | $199/mo | 1,000 | 100 | 10 | 1,000 min | 500 min | 500 min | 3 |
    | Professional | $499/mo | 2,500 | 250 | 25 | 3,000 min | 1,200 min | 1,200 min | 10 |
    | Growth | $999/mo | 10,000 | 1,000 | 100 | 5,000 min | 5,000 min | 2,000 min | 50 |
    | Enterprise | Contact Sales | Contract | Contract | Contract | Contract | Contract | Contract | Contract |

    *Translation* minutes are the shared audio + caption allowance per org per
    UTC calendar month, consumed by live translation on calls and meeting
    rooms. Translation minutes renew with the subscription cycle.

    A **daily cap** of 10% of the monthly allowance (rounded up) applies inside
    the monthly budget — e.g. Starter's 500 min/month translates to a 50 min/day
    cap, Growth's 2,000/month to a 200/day cap. The daily cap prevents a single
    heavy day from exhausting the month's budget and matches the subscription
    model's "steady usage" assumption. When the daily cap is reached, further
    translation requests return HTTP 402 until the next UTC day; the monthly
    allowance is otherwise untouched. Hard cutoff — no per-minute overage billing
    is applied to the subscription allowance.

    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:
  /calls/{callId}/translation:
    parameters:
      - in: path
        name: callId
        required: true
        schema: {type: string}
    get:
      tags: [Calls]
      summary: Get live translation status or capabilities
      description: Requires calls:write. API keys stay on your backend. See /org-live-translation.md for the client integration and deployment guide.
      parameters:
        - in: query
          name: action
          schema: {type: string, enum: [status, capabilities], default: status}
        - in: query
          name: participantId
          description: Internal call participant ID; required for status.
          schema: {type: string}
      responses:
        '200': {description: Status or capabilities JSON object}
        '401': {description: Invalid API key}
        '403': {description: Missing scope or participant authorization}
        '404': {description: Call not found in organization}
    post:
      tags: [Calls]
      summary: Control live translated audio and captions
      description: |
        Requires calls:write and an active two-participant call. Authenticate the end user on your backend before asserting participantId.
        Request mode audio or captions; the other participant accepts. Each participant chooses their own listening language.
        All actions after request require the current requestId. Audio uses the existing microphone WebRTC gateway.
        Included within monthly organization translation limits; no separate wallet charge. Bill every 10 seconds and stop on exit.
        Both audio clients must report audio-ready before charges start. Caption mode starts on acceptance.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action, participantId]
              properties:
                action: {type: string, enum: [status, request, accept, decline, audio-client-secret, audio-ready, caption, bill, stop]}
                participantId: {type: string, description: Internal participant ID from call.participants}
                requestId: {type: string, description: Current consent request ID; required after request}
                stopRequestedAt: {type: number, description: Original stop time in Unix milliseconds; preserve across retries}
                mode: {type: string, enum: [audio, captions]}
                targetLanguage: {type: string, example: de, description: Listening language code returned by capabilities}
                audioBase64: {type: string, description: Caption-only microphone chunk; max decoded 1.5 MB}
            example: {action: request, participantId: part_example, mode: audio, targetLanguage: de}
      responses:
        '200':
          description: Session state, caption result or participant-scoped gateway credentials as a direct JSON object. Credentials expire after two hours.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: {type: string, enum: [off, requested, active, declined, stopped]}
                  requestId: {type: string}
                  ratePerMinute: {type: number, example: 0.10}
                  totalCharged: {type: number}
                  clientSecret: {type: string, description: Participant-only gateway grant; never an organization API key}
                  sessionUrl: {type: string, format: uri}
                  callId: {type: string, description: Namespaced gateway call ID; pass unchanged}
                  tenant: {type: string}
                  provider: {type: string, enum: [deepgram]}
                  targetLanguage: {type: string}
                  expiresAt: {type: number, description: Unix milliseconds}
        '400': {description: Invalid action or input}
        '401': {description: Invalid API key}
        '402': {description: Payer wallet has insufficient credit}
        '403': {description: Missing scope or participant authorization}
        '404': {description: Call not found}
        '409': {description: Stale request, inactive call, unsupported currency or missing consent}
        '429': {description: Caption chunks too frequent}
        '503': {description: Translation not configured}
  /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:** Included within subscription AI-summary limits
      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:** Included within subscription AI-summary limits
      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
