openapi: 3.1.0
info:
  title: onepizza.io API
  version: 1.2.0
  summary: REST + WebSocket + MCP API for video meetings
  description: |
    The onepizza.io API lets developers, AI agents, and teams create and manage
    video meetings programmatically. Three integration surfaces are supported:

    - **REST API** — documented here. Authenticate with `x-api-key`.
    - **Socket.IO real-time API** — see the human-readable docs at `/docs`.
    - **MCP server** (Model Context Protocol) — drop-in tools for Claude, Cursor,
      and other agents. See the `MCP Integration` section in `/docs`.

    All times are UTC (ISO-8601). All bodies are JSON unless noted. Errors use
    `{ "error": "message" }` with appropriate HTTP status codes.
  contact:
    name: onepizza.io
    url: https://onepizza.io
  license:
    name: Proprietary
servers:
  - url: https://onepizza.io
    description: Production
  - url: http://localhost:3000
    description: Local development

security:
  - apiKey: []

tags:
  - name: Auth
    description: Account creation, login, and password management. Session-cookie based.
  - name: API Keys
    description: Manage API keys used for the `x-api-key` header.
  - name: Meetings
    description: Create, list, retrieve, and end video meetings.
  - name: Guest Meetings
    description: Create meetings without authentication (rate-limited).
  - name: Settings & Controls
    description: Meeting settings, lock/unlock, invites.
  - name: Participants
    description: Mute, unmute, kick, mute-all.
  - name: Polls
    description: In-meeting polls.
  - name: Q&A
    description: In-meeting questions with upvotes & answers.
  - name: Notes
    description: Shared meeting notes.
  - name: Files
    description: In-meeting file sharing.
  - name: Attendance
    description: Attendance reports.
  - name: Templates
    description: Reusable meeting setting presets.
  - name: Recurring
    description: Auto-recurring meetings (RRULE).
  - name: History
    description: Past meeting metadata.
  - name: Transcripts
    description: Chat message transcripts.
  - name: Recordings
    description: Upload, list, download, and delete recordings.
  - name: Webhooks
    description: Outbound HTTP event delivery.
  - name: Billing
    description: Stripe checkout, USDC deposits, balance & history.
  - name: Company
    description: Company / team management.
  - name: Config
    description: Public config (feature flags, ICE servers, SFU).
  - name: Health
    description: Health & readiness probes.

paths:
  # ─── Auth ──────────────────────────────────────────────────────────────────
  /api/auth/register:
    post:
      tags: [Auth]
      summary: Create an account
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RegisterRequest' }
      responses:
        '200':
          description: Account created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RegisterResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /api/auth/login:
    post:
      tags: [Auth]
      summary: Log in (sets session cookie)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:    { type: string, format: email }
                password: { type: string, format: password }
      responses:
        '200':
          description: Logged in
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
                  isAdmin: { type: boolean }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /api/auth/logout:
    post:
      tags: [Auth]
      summary: Destroy the current session
      security: [{ session: [] }]
      responses:
        '200':
          description: Logged out
          content:
            application/json:
              schema: { type: object, properties: { message: { type: string } } }
  /api/auth/me:
    get:
      tags: [Auth]
      summary: Get the currently logged-in user
      security: [{ session: [] }]
      responses:
        '200':
          description: User profile
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UserProfile' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/auth/forgot-password:
    post:
      tags: [Auth]
      summary: Send a password-reset email
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties: { email: { type: string, format: email } }
      responses:
        '200':
          description: Always returns 200 to avoid email enumeration
          content: { application/json: { schema: { type: object, properties: { message: { type: string } } } } }
        '429': { $ref: '#/components/responses/RateLimited' }
  /api/auth/reset-password:
    get:
      tags: [Auth]
      summary: Validate a password reset token
      security: []
      parameters:
        - in: query
          name: token
          required: true
          schema: { type: string }
      responses:
        '200': { description: Token is valid, content: { application/json: { schema: { type: object, properties: { valid: { type: boolean } } } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
    post:
      tags: [Auth]
      summary: Set a new password using a reset token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, newPassword]
              properties:
                token:       { type: string }
                newPassword: { type: string, minLength: 8, maxLength: 128 }
      responses:
        '200': { description: Password updated, content: { application/json: { schema: { type: object, properties: { message: { type: string } } } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/auth/change-password:
    post:
      tags: [Auth]
      summary: Change the password for the logged-in user
      security: [{ session: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currentPassword, newPassword]
              properties:
                currentPassword: { type: string }
                newPassword:     { type: string, minLength: 8, maxLength: 128 }
      responses:
        '200': { description: Password changed }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  # ─── API keys ──────────────────────────────────────────────────────────────
  /api/user/keys:
    get:
      tags: [API Keys]
      summary: List the current user's API keys
      security: [{ session: [] }]
      responses:
        '200':
          description: Keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items: { $ref: '#/components/schemas/ApiKey' }
    post:
      tags: [API Keys]
      summary: Create a new API key (max 5 active per user)
      security: [{ session: [] }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: { label: { type: string, maxLength: 100 } }
      responses:
        '201':
          description: New key (the secret is only returned here, store it now)
          content: { application/json: { schema: { $ref: '#/components/schemas/ApiKey' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/user/keys/{id}:
    delete:
      tags: [API Keys]
      summary: Revoke an API key
      security: [{ session: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        '200': { description: Revoked }
        '404': { $ref: '#/components/responses/NotFound' }

  # ─── Meetings ──────────────────────────────────────────────────────────────
  /api/meetings:
    post:
      tags: [Meetings]
      summary: Create a meeting
      description: |
        Creates an instant meeting, or a scheduled meeting if `scheduledAt` is set.
        Accepts API key (`x-api-key`) **or** a session cookie. Returns an
        `adminToken` — required for admin-only endpoints (`x-admin-token`).
      security:
        - apiKey: []
        - session: []
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateMeetingRequest' }
      responses:
        '201':
          description: Meeting created
          content: { application/json: { schema: { $ref: '#/components/schemas/CreateMeetingResponse' } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [Meetings]
      summary: List active and scheduled meetings
      responses:
        '200':
          description: Meeting list
          content:
            application/json:
              schema:
                type: object
                properties:
                  meetings:
                    type: array
                    items: { $ref: '#/components/schemas/MeetingSummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /api/meetings/scheduled/list:
    get:
      tags: [Meetings]
      summary: List only scheduled meetings
      responses:
        '200':
          description: Scheduled meetings
          content:
            application/json:
              schema:
                type: object
                properties:
                  meetings:
                    type: array
                    items: { $ref: '#/components/schemas/ScheduledMeeting' }
  /api/meetings/{meetingId}:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
    get:
      tags: [Meetings]
      summary: Get meeting details (live or scheduled)
      responses:
        '200':
          description: Meeting
          content: { application/json: { schema: { $ref: '#/components/schemas/MeetingDetail' } } }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Meetings]
      summary: End an active meeting (or cancel a scheduled one)
      parameters:
        - $ref: '#/components/parameters/AdminToken'
      responses:
        '200': { description: Ended }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /api/meetings/guest:
    post:
      tags: [Guest Meetings]
      summary: Create an unauthenticated guest meeting (rate-limited)
      security: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: { title: { type: string, maxLength: 100 } }
      responses:
        '201':
          description: Meeting created
          content: { application/json: { schema: { $ref: '#/components/schemas/CreateMeetingResponse' } } }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ─── Settings & controls ──────────────────────────────────────────────────
  /api/meetings/{meetingId}/settings:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/AdminToken'
    patch:
      tags: [Settings & Controls]
      summary: Update meeting settings
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MeetingSettingsPatch' }
      responses:
        '200':
          description: Updated settings
          content:
            application/json:
              schema:
                type: object
                properties:
                  settings: { $ref: '#/components/schemas/MeetingSettings' }
                  title:    { type: string }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/meetings/{meetingId}/lock:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Settings & Controls]
      summary: Lock the meeting (no new joins)
      responses: { '200': { description: Locked } }
  /api/meetings/{meetingId}/unlock:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Settings & Controls]
      summary: Unlock the meeting
      responses: { '200': { description: Unlocked } }
  /api/meetings/{meetingId}/invite:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Settings & Controls]
      summary: Generate a personalized invite link
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: { name: { type: string } }
      responses:
        '200':
          description: Invite link
          content:
            application/json:
              schema:
                type: object
                properties:
                  joinUrl:     { type: string }
                  inviteToken: { type: string }

  # ─── Participants ─────────────────────────────────────────────────────────
  /api/meetings/{meetingId}/participants/{participantId}/mute:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/ParticipantId'
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Participants]
      summary: Mute a participant
      responses: { '200': { description: Muted }, '404': { $ref: '#/components/responses/NotFound' } }
  /api/meetings/{meetingId}/participants/{participantId}/unmute:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/ParticipantId'
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Participants]
      summary: Unmute a participant
      responses: { '200': { description: Unmuted }, '404': { $ref: '#/components/responses/NotFound' } }
  /api/meetings/{meetingId}/participants/{participantId}/kick:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/ParticipantId'
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Participants]
      summary: Kick a participant
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: { reason: { type: string } }
      responses: { '200': { description: Kicked }, '404': { $ref: '#/components/responses/NotFound' } }
  /api/meetings/{meetingId}/mute-all:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Participants]
      summary: Mute every participant
      responses: { '200': { description: All muted } }

  # ─── Polls ────────────────────────────────────────────────────────────────
  /api/meetings/{meetingId}/polls:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
    post:
      tags: [Polls]
      summary: Create a poll (admin)
      parameters: [{ $ref: '#/components/parameters/AdminToken' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [question, options]
              properties:
                question: { type: string, maxLength: 500 }
                options:
                  type: array
                  minItems: 2
                  maxItems: 10
                  items: { type: string, maxLength: 200 }
      responses:
        '201':
          description: Poll created
          content:
            application/json:
              schema:
                type: object
                properties:
                  pollId:   { type: string }
                  question: { type: string }
                  options:
                    type: array
                    items: { type: object, properties: { id: { type: string }, text: { type: string } } }
    get:
      tags: [Polls]
      summary: List polls and live results
      responses:
        '200':
          description: Polls
          content:
            application/json:
              schema:
                type: object
                properties:
                  polls:
                    type: array
                    items: { $ref: '#/components/schemas/Poll' }
  /api/meetings/{meetingId}/polls/{pollId}/vote:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - in: path
        name: pollId
        required: true
        schema: { type: string }
    post:
      tags: [Polls]
      summary: Cast a vote
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [optionId]
              properties:
                optionId:      { type: string }
                participantId: { type: string }
      responses:
        '200': { description: Voted }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/meetings/{meetingId}/polls/{pollId}/end:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - in: path
        name: pollId
        required: true
        schema: { type: string }
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Polls]
      summary: End a poll (admin)
      responses:
        '200':
          description: Final results
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Poll' }

  # ─── Q&A ──────────────────────────────────────────────────────────────────
  /api/meetings/{meetingId}/questions:
    parameters: [{ $ref: '#/components/parameters/MeetingId' }]
    post:
      tags: [Q&A]
      summary: Ask a question
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text:            { type: string, maxLength: 500 }
                participantName: { type: string, maxLength: 60 }
      responses:
        '201':
          description: Question posted
          content:
            application/json:
              schema: { type: object, properties: { questionId: { type: string } } }
    get:
      tags: [Q&A]
      summary: List Q&A questions (sorted by upvotes)
      responses:
        '200':
          description: Questions
          content:
            application/json:
              schema:
                type: object
                properties:
                  questions:
                    type: array
                    items: { $ref: '#/components/schemas/Question' }
  /api/meetings/{meetingId}/questions/{questionId}/upvote:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - in: path
        name: questionId
        required: true
        schema: { type: string }
    post:
      tags: [Q&A]
      summary: Toggle upvote on a question
      responses: { '200': { description: Upvote toggled } }
  /api/meetings/{meetingId}/questions/{questionId}/answer:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - in: path
        name: questionId
        required: true
        schema: { type: string }
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Q&A]
      summary: Mark a question answered (admin)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                answer:     { type: string, maxLength: 1000 }
                answeredBy: { type: string, maxLength: 60 }
      responses: { '200': { description: Answered } }
  /api/meetings/{meetingId}/questions/{questionId}/dismiss:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - in: path
        name: questionId
        required: true
        schema: { type: string }
      - $ref: '#/components/parameters/AdminToken'
    post:
      tags: [Q&A]
      summary: Dismiss a question (admin)
      responses: { '200': { description: Dismissed } }

  # ─── Notes ────────────────────────────────────────────────────────────────
  /api/meetings/{meetingId}/notes:
    parameters: [{ $ref: '#/components/parameters/MeetingId' }]
    get:
      tags: [Notes]
      summary: Get current meeting notes
      responses:
        '200':
          description: Notes
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MeetingNotes' }
    put:
      tags: [Notes]
      summary: Replace meeting notes (admin)
      parameters: [{ $ref: '#/components/parameters/AdminToken' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties: { content: { type: string, maxLength: 50000 } }
      responses: { '200': { description: Updated } }

  # ─── Files ────────────────────────────────────────────────────────────────
  /api/meetings/{meetingId}/files:
    parameters: [{ $ref: '#/components/parameters/MeetingId' }]
    post:
      tags: [Files]
      summary: Upload a file to the meeting (multipart/form-data)
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:            { type: string, format: binary }
                participantName: { type: string }
      responses:
        '201':
          description: Uploaded
          content: { application/json: { schema: { $ref: '#/components/schemas/MeetingFile' } } }
        '429': { $ref: '#/components/responses/RateLimited' }
    get:
      tags: [Files]
      summary: List files shared in a meeting
      responses:
        '200':
          description: Files
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items: { $ref: '#/components/schemas/MeetingFile' }
  /api/meetings/files/{fileId}/download:
    parameters:
      - in: path
        name: fileId
        required: true
        schema: { type: integer }
    get:
      tags: [Files]
      summary: Download a meeting file
      responses:
        '200':
          description: File contents
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ─── Attendance ───────────────────────────────────────────────────────────
  /api/meetings/{meetingId}/attendance:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/AdminToken'
    get:
      tags: [Attendance]
      summary: Get attendance report (admin)
      responses:
        '200':
          description: Attendance
          content:
            application/json:
              schema:
                type: object
                properties:
                  meetingId: { type: string }
                  title:     { type: string }
                  attendance:
                    type: array
                    items: { $ref: '#/components/schemas/AttendanceEntry' }
  /api/meetings/{meetingId}/attendance/download:
    parameters:
      - $ref: '#/components/parameters/MeetingId'
      - $ref: '#/components/parameters/AdminToken'
    get:
      tags: [Attendance]
      summary: Download attendance as CSV (admin)
      responses:
        '200':
          description: CSV
          content: { text/csv: { schema: { type: string } } }

  # ─── Templates ────────────────────────────────────────────────────────────
  /api/templates:
    get:
      tags: [Templates]
      summary: List meeting templates
      responses:
        '200':
          description: Templates
          content:
            application/json:
              schema:
                type: object
                properties:
                  templates:
                    type: array
                    items: { $ref: '#/components/schemas/MeetingTemplate' }
    post:
      tags: [Templates]
      summary: Create a meeting template
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:     { type: string, maxLength: 100 }
                title:    { type: string, maxLength: 100 }
                settings: { $ref: '#/components/schemas/MeetingSettings' }
      responses:
        '201':
          description: Created
          content: { application/json: { schema: { $ref: '#/components/schemas/MeetingTemplate' } } }
  /api/templates/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    delete:
      tags: [Templates]
      summary: Delete a template
      responses: { '200': { description: Deleted } }

  # ─── Recurring meetings ───────────────────────────────────────────────────
  /api/recurring:
    get:
      tags: [Recurring]
      summary: List recurring meetings
      responses:
        '200':
          description: Recurring meetings
          content:
            application/json:
              schema:
                type: object
                properties:
                  recurring:
                    type: array
                    items: { $ref: '#/components/schemas/RecurringMeeting' }
    post:
      tags: [Recurring]
      summary: Create a recurring meeting (RRULE)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, rrule]
              properties:
                title:    { type: string, maxLength: 100 }
                rrule:    { type: string, example: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;BYHOUR=9' }
                timezone: { type: string, example: UTC }
                settings: { $ref: '#/components/schemas/MeetingSettings' }
      responses:
        '201':
          description: Created
          content: { application/json: { schema: { $ref: '#/components/schemas/RecurringMeeting' } } }
  /api/recurring/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    patch:
      tags: [Recurring]
      summary: Pause / resume a recurring meeting
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [is_active]
              properties: { is_active: { type: boolean } }
      responses: { '200': { description: Updated } }
    delete:
      tags: [Recurring]
      summary: Delete a recurring meeting
      responses: { '200': { description: Deleted } }

  # ─── History ──────────────────────────────────────────────────────────────
  /api/meetings/history:
    get:
      tags: [History]
      summary: Past meetings (last 50)
      security: [{ session: [] }]
      responses:
        '200':
          description: History
          content:
            application/json:
              schema:
                type: object
                properties:
                  meetings:
                    type: array
                    items: { $ref: '#/components/schemas/HistoricalMeeting' }

  # ─── Transcripts ──────────────────────────────────────────────────────────
  /api/meetings/{meetingId}/transcript:
    parameters: [{ $ref: '#/components/parameters/MeetingId' }]
    get:
      tags: [Transcripts]
      summary: Get chat transcript (JSON)
      responses:
        '200':
          description: Transcript
          content:
            application/json:
              schema:
                type: object
                properties:
                  messages:
                    type: array
                    items:
                      type: object
                      properties:
                        participant_name: { type: string }
                        text:             { type: string }
                        created_at:       { type: string, format: date-time }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/meetings/{meetingId}/transcript/download:
    parameters: [{ $ref: '#/components/parameters/MeetingId' }]
    get:
      tags: [Transcripts]
      summary: Download chat transcript (plain text)
      responses:
        '200':
          description: Transcript file
          content: { text/plain: { schema: { type: string } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  # ─── Recordings ───────────────────────────────────────────────────────────
  /api/meetings/{meetingId}/recordings:
    parameters: [{ $ref: '#/components/parameters/MeetingId' }]
    post:
      tags: [Recordings]
      summary: Upload a recording (multipart, max 500 MB)
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [recording]
              properties:
                recording: { type: string, format: binary }
      responses:
        '201':
          description: Uploaded
          content: { application/json: { schema: { $ref: '#/components/schemas/Recording' } } }
    get:
      tags: [Recordings]
      summary: List recordings for a meeting
      responses:
        '200':
          description: Recordings
          content:
            application/json:
              schema:
                type: object
                properties:
                  recordings:
                    type: array
                    items: { $ref: '#/components/schemas/Recording' }
  /api/recordings/{id}/download:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    get:
      tags: [Recordings]
      summary: Download a recording
      responses:
        '200':
          description: Recording bytes
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/recordings/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    delete:
      tags: [Recordings]
      summary: Delete a recording
      responses: { '200': { description: Deleted } }

  # ─── Webhooks ─────────────────────────────────────────────────────────────
  /api/webhooks:
    get:
      tags: [Webhooks]
      summary: List configured webhooks
      security: [{ session: [] }]
      responses:
        '200':
          description: Webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items: { $ref: '#/components/schemas/Webhook' }
    post:
      tags: [Webhooks]
      summary: Register a webhook
      security: [{ session: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:    { type: string, format: uri }
                events:
                  type: array
                  items:
                    type: string
                    enum: [meeting.started, meeting.ended, participant.joined, participant.left, poll.created, poll.ended, breakout.started, breakout.ended]
      responses:
        '201':
          description: Webhook created (the `secret` is only returned once — store it now)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Webhook'
                  - type: object
                    properties: { secret: { type: string } }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/webhooks/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      security: [{ session: [] }]
      responses:
        '200': { description: Deleted }
        '404': { $ref: '#/components/responses/NotFound' }

  # ─── Billing ──────────────────────────────────────────────────────────────
  /api/billing/stripe/checkout:
    post:
      tags: [Billing]
      summary: Start a Stripe Checkout session for a credit top-up
      security: [{ session: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amountUsd]
              properties:
                amountUsd: { type: number, minimum: 5, maximum: 1000 }
      responses:
        '200':
          description: Stripe Checkout URL
          content:
            application/json:
              schema:
                type: object
                properties: { url: { type: string, format: uri } }
        '400': { $ref: '#/components/responses/BadRequest' }
  /api/billing/usdc/address:
    get:
      tags: [Billing]
      summary: Get the deposit USDC address for this account
      security: [{ session: [] }]
      responses:
        '200':
          description: Address
          content:
            application/json:
              schema: { type: object, properties: { address: { type: string, nullable: true } } }
  /api/billing/history:
    get:
      tags: [Billing]
      summary: Account credit balance & last 50 transactions
      security: [{ session: [] }]
      responses:
        '200':
          description: Balance & transactions
          content:
            application/json:
              schema:
                type: object
                properties:
                  balance:      { type: number }
                  transactions:
                    type: array
                    items: { $ref: '#/components/schemas/CreditTransaction' }

  # ─── Company ──────────────────────────────────────────────────────────────
  /api/company/join:
    post:
      tags: [Company]
      summary: Join a company by invite code
      security: [{ session: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [inviteCode]
              properties: { inviteCode: { type: string } }
      responses:
        '200':
          description: Joined
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:     { type: string }
                  companyId:   { type: integer }
                  companyName: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
  /api/company/members:
    get:
      tags: [Company]
      summary: List company members
      security: [{ session: [] }]
      responses:
        '200':
          description: Members
          content:
            application/json:
              schema:
                type: object
                properties:
                  isOwner: { type: boolean }
                  members:
                    type: array
                    items: { $ref: '#/components/schemas/CompanyMember' }
  /api/company/members/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: integer }
    delete:
      tags: [Company]
      summary: Remove a company member (owner only)
      security: [{ session: [] }]
      responses:
        '200': { description: Removed }
        '403': { $ref: '#/components/responses/Forbidden' }

  # ─── Config & SFU ─────────────────────────────────────────────────────────
  /api/config:
    get:
      tags: [Config]
      summary: Public feature flags
      security: []
      responses:
        '200':
          description: Flags
          content:
            application/json:
              schema:
                type: object
                properties:
                  recordingEnabled:    { type: boolean }
                  screenShareEnabled:  { type: boolean }
                  blurEnabled:         { type: boolean }
                  registrationEnabled: { type: boolean }
  /api/config/ice-servers:
    get:
      tags: [Config]
      summary: WebRTC ICE servers (STUN/TURN)
      security: []
      responses:
        '200':
          description: ICE
          content:
            application/json:
              schema:
                type: object
                properties:
                  iceServers:
                    type: array
                    items: { $ref: '#/components/schemas/IceServer' }
  /api/sfu/config:
    get:
      tags: [Config]
      summary: SFU (LiveKit) configuration
      security: []
      responses:
        '200':
          description: SFU info
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:   { type: boolean }
                  url:       { type: string, nullable: true }
                  threshold: { type: integer }
  /api/sfu/token:
    post:
      tags: [Config]
      summary: Mint a LiveKit access token for a meeting participant
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [meetingId, participantId, sfuAuthToken]
              properties:
                meetingId:     { type: string }
                participantId: { type: string }
                sfuAuthToken:  { type: string, description: "Per-participant secret issued at join time." }
                name:          { type: string }
      responses:
        '200':
          description: Token
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  url:   { type: string }
                  room:  { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '503': { description: SFU not configured }

  # ─── Health ───────────────────────────────────────────────────────────────
  /health:
    get:
      tags: [Health]
      summary: Combined health probe (DB + uptime)
      security: []
      responses:
        '200': { description: OK }
        '503': { description: Unavailable }
  /health/liveness:
    get:
      tags: [Health]
      summary: Process is alive
      security: []
      responses: { '200': { description: OK } }
  /health/readiness:
    get:
      tags: [Health]
      summary: Process is ready (DB reachable)
      security: []
      responses:
        '200': { description: Ready }
        '503': { description: Not ready }

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        Account API key from Dashboard → API Keys. Format: `mk_…`.
        Use for all server-to-server calls.
    session:
      type: apiKey
      in: cookie
      name: connect.sid
      description: |
        Browser session cookie set by `POST /api/auth/login`. Used for the
        dashboard and account-management endpoints.

  parameters:
    MeetingId:
      in: path
      name: meetingId
      required: true
      schema: { type: string }
      description: Meeting ID (e.g. `abc-defg-hij`)
    ParticipantId:
      in: path
      name: participantId
      required: true
      schema: { type: string }
    AdminToken:
      in: header
      name: x-admin-token
      required: true
      schema: { type: string }
      description: Admin token returned when the meeting was created.

  responses:
    BadRequest:
      description: Bad request
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: Authentication required or invalid
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: Authenticated but not authorized
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: Resource not found
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflict:
      description: Resource conflict
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Too many requests
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string }

    RegisterRequest:
      type: object
      required: [email, password]
      properties:
        email:       { type: string, format: email, maxLength: 255 }
        password:    { type: string, minLength: 8, maxLength: 128 }
        accountType: { type: string, enum: [personal, company], default: personal }
        companyName: { type: string, maxLength: 100, description: Required when `accountType` is `company` }

    RegisterResponse:
      type: object
      properties:
        message:     { type: string }
        email:       { type: string }
        apiKey:      { type: string, description: 'API key (only returned once)' }
        inviteCode:  { type: string, nullable: true }
        accountType: { type: string }

    UserProfile:
      type: object
      properties:
        id:               { type: integer }
        email:            { type: string }
        is_admin:         { type: boolean }
        account_type:     { type: string }
        company_id:       { type: integer, nullable: true }
        credits_usd:      { type: number }
        company_name:     { type: string, nullable: true }
        company_credits:  { type: number, nullable: true }
        invite_code:      { type: string, nullable: true }
        plan:             { type: string, nullable: true }
        created_at:       { type: string, format: date-time }

    ApiKey:
      type: object
      properties:
        id:           { type: integer }
        key:          { type: string }
        label:        { type: string }
        is_active:    { type: boolean }
        created_at:   { type: string, format: date-time }
        last_used_at: { type: string, format: date-time, nullable: true }

    MeetingSettings:
      type: object
      properties:
        muteOnJoin:      { type: boolean }
        videoOffOnJoin:  { type: boolean }
        maxParticipants: { type: integer, minimum: 2, maximum: 500 }
        locked:          { type: boolean }
        waitingRoom:     { type: boolean }

    MeetingSettingsPatch:
      type: object
      properties:
        muteOnJoin:      { type: boolean }
        videoOffOnJoin:  { type: boolean }
        maxParticipants: { type: integer, minimum: 1, maximum: 100 }
        locked:          { type: boolean }
        title:           { type: string, maxLength: 100 }

    CreateMeetingRequest:
      type: object
      properties:
        title:           { type: string, maxLength: 100 }
        muteOnJoin:      { type: boolean }
        videoOffOnJoin:  { type: boolean }
        maxParticipants: { type: integer, minimum: 2, maximum: 500 }
        waitingRoom:     { type: boolean }
        scheduledAt:     { type: string, format: date-time, description: 'If set, schedules the meeting for a future time (ISO 8601).' }
        templateId:      { type: integer, description: 'Apply a saved meeting template.' }

    CreateMeetingResponse:
      type: object
      properties:
        meetingId:   { type: string }
        adminToken:  { type: string, description: 'Pass as `x-admin-token` for admin endpoints.' }
        joinUrl:     { type: string }
        title:       { type: string }
        status:      { type: string, enum: [active, scheduled] }
        scheduledAt: { type: string, format: date-time, nullable: true }
        settings:    { $ref: '#/components/schemas/MeetingSettings' }

    Participant:
      type: object
      properties:
        participantId:   { type: string }
        name:            { type: string }
        isMuted:         { type: boolean }
        isVideoOff:      { type: boolean }
        isScreenSharing: { type: boolean }
        joinedAt:        { type: integer, description: 'Unix ms timestamp' }

    MeetingSummary:
      type: object
      properties:
        meetingId:        { type: string }
        title:            { type: string }
        createdAt:        { type: integer, description: 'Unix ms' }
        scheduledAt:      { type: string, format: date-time, nullable: true }
        status:           { type: string, enum: [active, scheduled] }
        participantCount: { type: integer }

    MeetingDetail:
      allOf:
        - $ref: '#/components/schemas/MeetingSummary'
        - type: object
          properties:
            participants:
              type: array
              items: { $ref: '#/components/schemas/Participant' }
            settings: { $ref: '#/components/schemas/MeetingSettings' }

    ScheduledMeeting:
      type: object
      properties:
        meetingId:   { type: string }
        title:       { type: string }
        scheduledAt: { type: string, format: date-time }
        status:      { type: string }
        createdAt:   { type: integer }

    Poll:
      type: object
      properties:
        id:         { type: string }
        question:   { type: string }
        isActive:   { type: boolean }
        totalVotes: { type: integer }
        options:
          type: array
          items:
            type: object
            properties:
              id:    { type: string }
              text:  { type: string }
        results:
          type: array
          items:
            type: object
            properties:
              id:    { type: string }
              text:  { type: string }
              count: { type: integer }

    Question:
      type: object
      properties:
        id:           { type: string }
        text:         { type: string }
        askedBy:      { type: object, properties: { name: { type: string } } }
        upvoteCount:  { type: integer }
        isAnswered:   { type: boolean }
        answer:       { type: string, nullable: true }
        answeredBy:   { type: string, nullable: true }
        createdAt:    { type: integer }

    MeetingNotes:
      type: object
      properties:
        content:        { type: string }
        lastUpdatedBy:  { type: string }
        lastUpdatedAt:  { type: integer, nullable: true }

    MeetingFile:
      type: object
      properties:
        id:               { type: integer }
        original_name:    { type: string }
        size_bytes:       { type: integer }
        mime_type:        { type: string }
        participant_name: { type: string }
        created_at:       { type: string, format: date-time }

    AttendanceEntry:
      type: object
      properties:
        participantId:   { type: string }
        name:            { type: string }
        joinedAt:        { type: string, format: date-time }
        leftAt:          { type: string, format: date-time, nullable: true }
        durationSeconds: { type: integer }

    MeetingTemplate:
      type: object
      properties:
        id:         { type: integer }
        name:       { type: string }
        title:      { type: string, nullable: true }
        settings:   { $ref: '#/components/schemas/MeetingSettings' }
        created_at: { type: string, format: date-time }

    RecurringMeeting:
      type: object
      properties:
        id:           { type: integer }
        title:        { type: string }
        rrule:        { type: string }
        timezone:     { type: string }
        next_run_at:  { type: string, format: date-time, nullable: true }
        is_active:    { type: boolean }
        settings:     { $ref: '#/components/schemas/MeetingSettings' }
        created_at:   { type: string, format: date-time }

    HistoricalMeeting:
      type: object
      properties:
        meeting_id:        { type: string }
        title:             { type: string }
        started_at:        { type: string, format: date-time }
        ended_at:          { type: string, format: date-time, nullable: true }
        peak_participants: { type: integer }
        duration_minutes:  { type: number }
        cost_usd:          { type: number }

    Recording:
      type: object
      properties:
        id:         { type: integer }
        filename:   { type: string }
        size_bytes: { type: integer }
        created_at: { type: string, format: date-time }

    Webhook:
      type: object
      properties:
        id:         { type: integer }
        url:        { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        is_active:  { type: boolean }
        created_at: { type: string, format: date-time }

    CreditTransaction:
      type: object
      properties:
        id:          { type: integer }
        type:        { type: string, description: 'e.g. topup_stripe, topup_usdc, meeting_usage' }
        amount_usd:  { type: number }
        meeting_id:  { type: string, nullable: true }
        created_at:  { type: string, format: date-time }

    CompanyMember:
      type: object
      properties:
        id:           { type: integer }
        email:        { type: string }
        account_type: { type: string }
        credits_used: { type: number }
        created_at:   { type: string, format: date-time }

    IceServer:
      type: object
      properties:
        urls:
          oneOf:
            - { type: string }
            - { type: array, items: { type: string } }
        username:   { type: string }
        credential: { type: string }
