openapi: 3.1.0
info:
  title: Orbyt API
  description: |
    Orbyt Job Search CRM API. AI routes require authentication via Supabase session cookie
    and a user-provided AI API key in the `X-User-API-Key` header.

    The MCP endpoints under `/api/mcp/*` authenticate via extension tokens (`Authorization:
    Bearer ext_...`) and require the Orbyt Jobs Ultra or Unlimited plan.

    The Salary (Public) endpoints under `/api/v1/intelligence/salaries/*` are part of Orbyt Intelligence,
    a separately-priced product. The /demo endpoint is open to anonymous requests for evaluation;
    production endpoints require an Intelligence API key via `Authorization: Bearer
    intelligence_...`. Free starts at 60 req/min and 1,000 requests/month with Pro and Ultra
    tiers above. See https://www.orbytjobs.ai/intelligence for pricing.
  version: 1.0.0
  contact:
    name: Orbyt Support
    url: https://www.orbytjobs.ai/support

servers:
  - url: https://www.orbytjobs.ai
    description: Production

tags:
  - name: AI - Job Parsing
    description: Parse job descriptions, URLs, and bank statements
  - name: AI - Scout
    description: AI job search assistant with tool-use
  - name: AI - Resume
    description: Resume tailoring and parsing
  - name: AI - Interview
    description: Interview preparation, debrief, and calendar import
  - name: AI - Wellness
    description: Mood analysis and wellness insights
  - name: AI - Networking
    description: Outreach message drafting
  - name: AI - Runway
    description: Financial runway analysis
  - name: AI - Pipeline
    description: Pipeline analytics and insights
  - name: Scout Actions
    description: Scout tool execution endpoint
  - name: Billing
    description: Stripe subscription management
  - name: AI - Agents
    description: Autonomous agent management and scheduling
  - name: Account
    description: Account management
  - name: MCP
    description: Model Context Protocol endpoints for AI assistants (Claude Desktop, iMessage, etc.). Authenticated via extension tokens (Bearer ext_...). Requires the Orbyt Jobs Ultra or Unlimited plan; keys on other plans receive 403 FORBIDDEN.
  - name: Salary (Public)
    description: |
      Orbyt Intelligence. AI compensation, decoded. Six engines designed, two serving data today (Role Taxonomy, Skill Premiums).
      20 endpoints + 6 MCP tools. lineage on every response.
      3,445 roles (598 specialized), 81 U.S. cities, 54 company leveling frameworks.
      Structured total comp (base, equity, bonus, signing), company size salary bands,
      remote differentials, BLS SOC citations, quarterly history, projections through 2030,
      community-reported crowd data, offer comparison, and AI negotiation scripts.

      Tiers (enforced via `Authorization: Bearer intelligence_...`):
        * Free ($0): 60 req/min, 1,000 requests/month. Current data + 4-quarter preview. AI Role Taxonomy engine. API key required, no card.
        * Pro ($99/mo or $990/yr): 300 req/min, 500,000 requests/month. Full quarterly history. MCP server, lineage endpoint, restricted API keys.
        * Ultra ($199/mo or $1,999/yr): 1500 req/min, 5,000,000 requests/month. Company leveling catalog, annual compensation reports, 15 seats.

      Full spec: https://www.orbytjobs.ai/openapi-intelligence.yaml
      CORS enabled for all origins. Responses include attribution object with source and
      methodology links. Generate an API key at https://www.orbytjobs.ai/intelligence/dashboard.

components:
  securitySchemes:
    supabaseAuth:
      type: apiKey
      in: cookie
      name: sb-access-token
      description: Supabase session cookie (set automatically on login)
    aiApiKey:
      type: apiKey
      in: header
      name: X-User-API-Key
      description: User's AI provider API key (OpenAI, Anthropic, or Grok)
    extensionToken:
      type: http
      scheme: bearer
      description: Extension token (starts with ext_). Generate in Orbyt Settings > Account > Extension Tokens.
    intelligenceApiKey:
      type: http
      scheme: bearer
      description: |
        Orbyt Intelligence API key (starts with `intelligence_`). Generate at
        https://www.orbytjobs.ai/intelligence/dashboard. Sent as `Authorization: Bearer intelligence_...`.
        Required for all production Salary endpoints. The /demo endpoint stays
        anonymous-public for evaluation. A free key unlocks 60 req/min;
        Pro ($299/mo) 300 req/min plus MCP + lineage; Scale ($1,999/mo) 1,500
        req/min plus Company Signals + webhooks; Enterprise ($4,999/mo) 5,000
        req/min plus 99.95% SLA.

  parameters:
    aiProvider:
      name: X-AI-Provider
      in: header
      required: true
      schema:
        type: string
        enum: [openai, anthropic, grok]
    aiModel:
      name: X-AI-Model
      in: header
      required: false
      schema:
        type: string

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

    # Scout Actions
    ScoutActionRequest:
      type: object
      required: [action, params]
      properties:
        action:
          type: string
          enum: [saveJob, updateJob, saveContact, updateContact, addActivity, getJobs, getContacts, getActivities]
        params:
          type: object

    SaveJobParams:
      type: object
      required: [company, role]
      properties:
        company: { type: string, maxLength: 200 }
        role: { type: string, maxLength: 200 }
        location: { type: string, maxLength: 200 }
        salary: { type: string, maxLength: 100 }
        url: { type: string, maxLength: 2048 }
        description: { type: string, maxLength: 10000 }
        notes: { type: string, maxLength: 5000 }
        tags: { type: array, items: { type: string }, maxItems: 10 }
        priority: { type: string, enum: [high, medium, low] }
        status: { type: string, enum: [saved, applied, screening, interviewing] }

    # Parse JD
    ParseJDRequest:
      type: object
      required: [text]
      properties:
        text: { type: string, minLength: 1 }
        meta: { type: object }
        url: { type: string }
        provider: { type: string, enum: [openai, anthropic, grok] }

    ParseJDResponse:
      type: object
      properties:
        company: { type: string }
        role: { type: string }
        location: { type: string }
        salary: { type: string }
        tags: { type: array, items: { type: string } }
        notes: { type: string }
        priority: { type: string }
        nextAction: { type: string }

    # Fetch JD
    FetchJDRequest:
      type: object
      required: [url]
      properties:
        url: { type: string, format: uri }

    FetchJDResponse:
      type: object
      properties:
        text: { type: string }
        url: { type: string }
        meta:
          type: object
          properties:
            title: { type: string }
            ogTitle: { type: string }
            ogDescription: { type: string }
            jobPosting: { type: object }

    # Parse Resume
    ParseResumeResponse:
      type: object
      properties:
        name: { type: string }
        headline: { type: string }
        skills: { type: array, items: { type: string } }
        experience:
          type: array
          items:
            type: object
            properties:
              company: { type: string }
              role: { type: string }
              duration: { type: string }
        suggestedTargets:
          type: array
          items:
            type: object
            properties:
              role: { type: string }
              reason: { type: string }

    # Parse Statement
    ParseStatementResponse:
      type: object
      properties:
        expenses:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              amount: { type: number }
              category: { type: string, enum: [housing, food, transportation, insurance, utilities, subscriptions, healthcare, debt, personal, entertainment, internet, work, phone, other] }

    # Interview Debrief
    InterviewDebriefRequest:
      type: object
      required: [notes]
      properties:
        notes: { type: string, minLength: 20 }
        company: { type: string }
        role: { type: string }

    InterviewDebriefResponse:
      type: object
      properties:
        wentWell: { type: array, items: { type: string } }
        toImprove: { type: array, items: { type: string } }
        nextRoundTips: { type: array, items: { type: string } }
        thankYouDraft: { type: string }
        overallSentiment: { type: string, enum: [positive, neutral, concerned] }
        keyMoments: { type: array, items: { type: string } }

    # Company Brief
    CompanyBriefRequest:
      type: object
      required: [company]
      properties:
        company: { type: string }
        role: { type: string }
        description: { type: string }
        notes: { type: string }

    CompanyBriefResponse:
      type: object
      properties:
        companyOverview: { type: string }
        recentNews: { type: array, items: { type: string } }
        cultureSignals: { type: array, items: { type: string } }
        interviewFocus: { type: array, items: { type: string } }
        questionsToAsk: { type: array, items: { type: string } }
        talkingPoints: { type: array, items: { type: string } }
        redFlags: { type: array, items: { type: string } }

    # Parse Interview Email
    ParseInterviewEmailResponse:
      type: object
      properties:
        company: { type: string }
        role: { type: string }
        date: { type: string, format: date }
        time: { type: string }
        duration: { type: integer }
        location: { type: string }
        interviewer: { type: string }
        notes: { type: string }

    # Salary Advisor
    SalaryAdvisorRequest:
      type: object
      required: [offer, runway]
      properties:
        offer:
          type: object
          properties:
            company: { type: string }
            role: { type: string }
            salary: { type: string }
        runway:
          type: object
          properties:
            capitalAmount: { type: number }
            monthlyExpenses: { type: number }
            monthlyIncome: { type: number }
            runwayMonths: { type: number }
            netBurn: { type: number }

    SalaryAdvisorResponse:
      type: object
      properties:
        canNegotiate: { type: boolean }
        urgency: { type: string, enum: [low, medium, high] }
        advice: { type: string }
        counterOffer: { type: string }
        talkingPoints: { type: array, items: { type: string } }
        riskAssessment: { type: string }

    # Wellness Analysis
    WellnessAnalysisResponse:
      type: object
      properties:
        patterns:
          type: array
          items:
            type: object
            properties:
              title: { type: string }
              body: { type: string }
        resilience: { type: string }
        suggestion: { type: string }
        encouragement: { type: string }

    # Draft Message
    DraftMessageRequest:
      type: object
      required: [contact]
      properties:
        contact:
          type: object
          properties:
            name: { type: string }
            title: { type: string }
            company: { type: string }
            type: { type: string }
        job:
          type: object
          properties:
            company: { type: string }
            role: { type: string }
            status: { type: string }
        messageType:
          type: string
          enum: [intro, follow-up, referral-request, informational, reconnect]

    DraftMessageResponse:
      type: object
      properties:
        subject: { type: string }
        message: { type: string }
        platform: { type: string, enum: [linkedin, email] }
        tone: { type: string }

    # Pipeline Insights
    PipelineInsightsResponse:
      type: object
      properties:
        insights:
          type: array
          items:
            type: object
            properties:
              title: { type: string }
              body: { type: string }

    # Billing
    BillingAddress:
      type: object
      required: [line1, country]
      properties:
        line1: { type: string }
        city: { type: string }
        state: { type: string }
        postal_code: { type: string }
        country: { type: string }

    PaymentMethod:
      type: object
      properties:
        brand: { type: string }
        last4: { type: string }
        expMonth: { type: integer }
        expYear: { type: integer }

    CreateSubscriptionRequest:
      type: object
      required: [plan, paymentMethodId]
      properties:
        plan:
          type: string
          enum: [monthly, yearly, pro-monthly, pro-yearly, ultra-monthly, ultra-yearly]
        paymentMethodId: { type: string }
        couponId: { type: string }
        address:
          $ref: '#/components/schemas/BillingAddress'

    CreateSubscriptionResponse:
      type: object
      properties:
        subscriptionId: { type: string }
        status: { type: string, enum: [active, trialing, incomplete, requires_action] }
        tier: { type: string, enum: [pro, ultra] }
        clientSecret:
          type: string
          description: Present only when 3D Secure is required (status=requires_action)

    ChangePlanRequest:
      type: object
      required: [plan]
      properties:
        plan:
          type: string
          enum: [monthly, yearly, pro-monthly, pro-yearly, ultra-monthly, ultra-yearly]

    ChangePlanResponse:
      type: object
      properties:
        status: { type: string }
        plan: { type: string }
        tier: { type: string, enum: [pro, ultra] }

    CancelResponse:
      type: object
      properties:
        cancelAtPeriodEnd: { type: boolean }
        currentPeriodEnd:
          type: string
          format: date-time
          nullable: true

    ReactivateResponse:
      type: object
      properties:
        status: { type: string }

    SubscriptionResponse:
      type: object
      properties:
        subscription:
          type: object
          nullable: true
          properties:
            id: { type: string }
            user_id: { type: string }
            stripe_customer_id: { type: string }
            status: { type: string, enum: [trialing, active, past_due, canceled, incomplete, incomplete_expired] }
            plan: { type: string }
            tier: { type: string, enum: [free, pro, ultra] }
            stripe_price_id: { type: string }
            current_period_start: { type: string, format: date-time, nullable: true }
            current_period_end: { type: string, format: date-time, nullable: true }
            trial_start: { type: string, format: date-time, nullable: true }
            trial_end: { type: string, format: date-time, nullable: true }
            cancel_at_period_end: { type: boolean }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
        invoices:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              user_id: { type: string }
              status: { type: string, enum: [draft, open, paid, void, uncollectible, refunded, partially_refunded] }
              amount_paid: { type: integer }
              amount_due: { type: integer }
              amount_refunded: { type: integer }
              subtotal: { type: integer, nullable: true }
              tax: { type: integer, nullable: true }
              total: { type: integer, nullable: true }
              currency: { type: string }
              created_at: { type: string, format: date-time }
        paymentMethod:
          allOf:
            - $ref: '#/components/schemas/PaymentMethod'
          nullable: true
        billingAddress:
          allOf:
            - $ref: '#/components/schemas/BillingAddress'
          nullable: true
        adminTier:
          type: string
          enum: [ultra]
          description: Present only for admin users with permanent ultra access

    UpdatePaymentMethodRequest:
      type: object
      required: [paymentMethodId]
      properties:
        paymentMethodId: { type: string }

    RefundRequest:
      type: object
      properties:
        invoiceId:
          type: string
          description: Stripe invoice ID to refund. Defaults to latest paid invoice if omitted.

    RefundResponse:
      type: object
      properties:
        refundId: { type: string }
        amount: { type: integer, description: Amount in cents }
        status: { type: string, enum: [succeeded, pending, failed] }

    ProrationPreviewRequest:
      type: object
      required: [plan]
      properties:
        plan:
          type: string
          enum: [monthly, yearly, pro-monthly, pro-yearly, ultra-monthly, ultra-yearly]

    ProrationPreviewResponse:
      type: object
      properties:
        lineItems:
          type: array
          items:
            type: object
            properties:
              description: { type: string }
              amount: { type: integer, description: Amount in cents (negative for credits) }
              currency: { type: string }
        immediateAmount: { type: integer, description: Net amount due immediately in cents }
        currency: { type: string }

    UpdateAddressRequest:
      type: object
      required: [line1, country]
      properties:
        line1: { type: string }
        city: { type: string }
        state: { type: string }
        postal_code: { type: string }
        country: { type: string }

    PortalResponse:
      type: object
      properties:
        url: { type: string, format: uri, description: Stripe Customer Portal session URL }

    # Scout Chat
    ScoutChatRequest:
      type: object
      required: [messages]
      properties:
        messages:
          type: array
          items:
            type: object
            properties:
              role: { type: string, enum: [user, assistant] }
              content: { type: string }
        context: { type: string }

paths:
  # ── Salary (Public) ──
  /api/v1/intelligence/salaries:
    get:
      tags: [Salary (Public)]
      summary: Get salary data for a role (optionally in a specific city)
      parameters:
        - name: role
          in: query
          required: true
          schema: { type: string }
          description: Role slug (e.g., ai-engineer, software-engineer)
        - name: city
          in: query
          required: false
          schema: { type: string }
          description: City slug (e.g., san-francisco, new-york). Omit for national data + all 81 cities.
      responses:
        "200":
          description: Salary data with experience bands, total comp, trends, and employer data
        "400":
          description: Missing role parameter
        "404":
          description: Role or city not found
        "429":
          description: Rate limit exceeded. Free = 60 req/min, Pro = 300 req/min, Ultra = 1500 req/min.
      security:
        - {}
        - intelligenceApiKey: []

  /api/v1/intelligence/salaries/roles:
    get:
      tags: [Salary (Public)]
      summary: List all 3,445 tracked roles
      responses:
        "200":
          description: Array of roles with slug, title, and national median
      security:
        - {}
        - intelligenceApiKey: []

  /api/v1/intelligence/salaries/cities:
    get:
      tags: [Salary (Public)]
      summary: List all 83 tracked cities
      responses:
        "200":
          description: Array of cities with slug, name, state, and COL multiplier
      security:
        - {}
        - intelligenceApiKey: []

  /api/v1/intelligence/salaries/search:
    get:
      tags: [Salary (Public)]
      summary: Search roles by keyword
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          description: Search query (e.g., "machine learning", "product manager")
      responses:
        "200":
          description: Matching roles (max 50 results)
        "400":
          description: Missing q parameter
      security:
        - {}
        - intelligenceApiKey: []

  /api/v1/intelligence/salaries/calculate:
    get:
      tags: [Salary (Public)]
      summary: Personalized salary estimate
      description: Returns a personalized salary estimate adjusted for role, city cost of living, and experience level. First-class JSON companion to the calculator page at /salaries/calculator. Includes total compensation breakdown and national comparison.
      parameters:
        - name: role
          in: query
          required: true
          schema: { type: string }
          description: Role slug (e.g., "ai-engineer")
        - name: city
          in: query
          required: true
          schema: { type: string }
          description: City slug (e.g., "san-francisco")
        - name: exp
          in: query
          required: false
          schema:
            type: string
            enum: [entry, mid, senior, staff]
            default: mid
          description: Experience level
      responses:
        "200":
          description: Computed estimate with base, total comp, and comparison
        "400":
          description: Missing or invalid parameter
        "404":
          description: Role or city not found
      security:
        - {}
        - intelligenceApiKey: []

  /api/v1/intelligence/salaries/history:
    get:
      tags: [Salary (Public)]
      summary: Quarterly salary history
      description: |
        Quarterly salary snapshots for a role, optionally adjusted for a city's cost of
        living. The free tier returns a 4-quarter preview window plus a `tierNotice` field
        indicating truncation. Pro ($99/mo) and Ultra ($199/mo) tiers return the full series
        with extended lookback as new quarters accrue. Authenticate with
        `Authorization: Bearer intelligence_...`; a free key requires no card.
      parameters:
        - name: role
          in: query
          required: true
          schema: { type: string }
          description: Role slug (e.g., "ai-engineer")
        - name: city
          in: query
          required: false
          schema: { type: string }
          description: City slug for COL-adjusted history (e.g., "san-francisco")
      responses:
        "200":
          description: Quarterly snapshots with change delta. The free tier includes `tierNotice` when series is truncated.
        "400":
          description: Missing role parameter
        "404":
          description: Role or city not found
        "429":
          description: Rate limit exceeded. Free = 60/min, Pro = 300/min, Ultra = 1500/min.
      security:
        - {}
        - intelligenceApiKey: []

  /api/v1/intelligence/salaries/projections:
    get:
      tags: [Salary (Public)]
      summary: Salary projections through 2030
      description: Returns forward salary projections year-over-year through 2030 based on historical growth trends. Useful for career planning and shareable content.
      parameters:
        - name: role
          in: query
          required: true
          schema: { type: string }
          description: Role slug (e.g., "ai-engineer")
        - name: city
          in: query
          required: false
          schema: { type: string }
          description: City slug for COL-adjusted projections
      responses:
        "200":
          description: Annual projections from current year through 2030
        "400":
          description: Missing role parameter
        "404":
          description: Role or city not found
      security:
        - {}
        - intelligenceApiKey: []

  # ── AI - Job Parsing ──
  /api/fetch-jd:
    post:
      tags: [AI - Job Parsing]
      summary: Fetch job description from URL
      description: Fetches and extracts structured metadata from a job posting URL. Handles LinkedIn, Greenhouse, Lever, and generic pages.
      security: [{ supabaseAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FetchJDRequest'
      responses:
        '200':
          description: Page content and metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FetchJDResponse'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/parse-jd:
    post:
      tags: [AI - Job Parsing]
      summary: Parse job description with AI
      description: AI parsing of raw job text into structured fields (company, role, location, salary, tags).
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
        - $ref: '#/components/parameters/aiModel'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ParseJDRequest'
      responses:
        '200':
          description: Parsed job fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParseJDResponse'

  /api/parse-contact:
    post:
      tags: [AI - Job Parsing]
      summary: Parse contact from LinkedIn text
      description: AI parsing of LinkedIn profile text into contact fields. Pro+ only.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
      responses:
        '200':
          description: Parsed contact fields
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: { type: string }
                  title: { type: string }
                  company: { type: string }
                  email: { type: string }
                  suggestedJobs: { type: array, items: { type: object } }

  # ── AI - Scout ──
  /api/scout-chat:
    post:
      tags: [AI - Scout]
      summary: Scout AI chat (streaming)
      description: AI-powered job search assistant. Streams contextual advice based on CRM data.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
        - $ref: '#/components/parameters/aiModel'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScoutChatRequest'
      responses:
        '200':
          description: Streaming text/event-stream response

  /api/scout-chat-v2:
    post:
      tags: [AI - Scout]
      summary: Scout AI chat with tool-use
      description: Scout chat with function calling. Supports save_job, update_job_status, add_contact, log_activity, search_jobs, search_contacts.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
        - $ref: '#/components/parameters/aiModel'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScoutChatRequest'
      responses:
        '200':
          description: Chat response with tool results
          content:
            application/json:
              schema:
                type: object
                properties:
                  content: { type: string }
                  toolResults:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        result: { type: object }

  /api/scout-actions:
    post:
      tags: [Scout Actions]
      summary: Execute Scout tool action
      description: Direct tool execution endpoint. Validates against Zod schemas and executes against Supabase.
      security: [{ supabaseAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScoutActionRequest'
      responses:
        '200':
          description: Action result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { type: object }
                  error: { type: string }

  # ── AI - Resume ──
  /api/resume-tailor:
    post:
      tags: [AI - Resume]
      summary: AI resume tailoring
      description: Rewrites resume sections to match a specific job description.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      responses:
        '200':
          description: Tailored resume content

  /api/parse-resume:
    post:
      tags: [AI - Resume]
      summary: Parse resume text
      description: Extracts skills, experience, education, and suggested targets from resume text. Rate limited to 3/min.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, minLength: 50 }
      responses:
        '200':
          description: Parsed resume data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParseResumeResponse'

  # ── AI - Interview ──
  /api/interview-prep:
    post:
      tags: [AI - Interview]
      summary: Generate interview prep materials
      description: AI generates company insights, likely questions, talking points, STAR stories, and questions to ask.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [company, role]
              properties:
                company: { type: string }
                role: { type: string }
                description: { type: string }
      responses:
        '200':
          description: Interview prep materials

  /api/interview-debrief:
    post:
      tags: [AI - Interview]
      summary: Post-interview debrief
      description: Analyzes interview notes and generates structured feedback, next round tips, and thank-you email draft.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InterviewDebriefRequest'
      responses:
        '200':
          description: Structured debrief
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InterviewDebriefResponse'

  /api/company-brief:
    post:
      tags: [AI - Interview]
      summary: Company research brief
      description: Generates a research brief with company overview, interview focus areas, smart questions, and red flags.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompanyBriefRequest'
      responses:
        '200':
          description: Company research brief
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyBriefResponse'

  /api/parse-interview-email:
    post:
      tags: [AI - Interview]
      summary: Parse interview confirmation email
      description: Extracts date, time, company, location, and interviewer from interview confirmation email text.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, minLength: 20 }
      responses:
        '200':
          description: Parsed interview event data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParseInterviewEmailResponse'

  # ── AI - Wellness ──
  /api/wellness-analysis:
    post:
      tags: [AI - Wellness]
      summary: Mood pattern analysis
      description: Analyzes mood check-in history and journal entries to identify patterns and provide encouragement.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [moodHistory]
              properties:
                moodHistory:
                  type: array
                  minItems: 3
                  items:
                    type: object
                    properties:
                      date: { type: string }
                      mood: { type: string }
                      labels: { type: array, items: { type: string } }
                      note: { type: string }
                journalEntries: { type: array }
                pipelineContext: { type: string, maxLength: 200 }
      responses:
        '200':
          description: Wellness analysis
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WellnessAnalysisResponse'

  # ── AI - Networking ──
  /api/draft-message:
    post:
      tags: [AI - Networking]
      summary: Draft networking message
      description: AI-drafted outreach message based on contact and job context.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftMessageRequest'
      responses:
        '200':
          description: Drafted message
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DraftMessageResponse'

  # ── AI - Runway ──
  /api/parse-statement:
    post:
      tags: [AI - Runway]
      summary: Parse bank statement
      description: Extracts and categorizes expenses from bank/credit card statement text.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, minLength: 20 }
      responses:
        '200':
          description: Categorized expenses
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ParseStatementResponse'

  /api/salary-advisor:
    post:
      tags: [AI - Runway]
      summary: Salary negotiation advisor
      description: AI-powered negotiation advice based on financial runway and offer data.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SalaryAdvisorRequest'
      responses:
        '200':
          description: Negotiation advice
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SalaryAdvisorResponse'

  # ── AI - Pipeline ──
  /api/pipeline-insights:
    post:
      tags: [AI - Pipeline]
      summary: Pipeline analytics insights
      description: AI-generated natural language insights about job search performance.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [stats]
              properties:
                stats: { type: object }
                context: { type: string }
      responses:
        '200':
          description: Pipeline insights
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PipelineInsightsResponse'

  # ── Billing ──
  /api/billing/create-subscription:
    post:
      tags: [Billing]
      summary: Create subscription
      description: Creates a Stripe customer and subscription. Inserts the subscription row directly to avoid race conditions with webhooks. Supports optional billing address for automatic tax calculation and optional coupon codes.
      security: [{ supabaseAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSubscriptionRequest'
      responses:
        '200':
          description: Subscription created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSubscriptionResponse'
        '400':
          description: Invalid plan or missing payment method
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Active subscription already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error or price not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/change-plan:
    post:
      tags: [Billing]
      summary: Change subscription plan
      description: Switches the user's subscription to a different plan with proration. Updates the database immediately without waiting for webhooks.
      security: [{ supabaseAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePlanRequest'
      responses:
        '200':
          description: Plan changed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangePlanResponse'
        '400':
          description: Invalid plan or already on this plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error or price not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/cancel:
    post:
      tags: [Billing]
      summary: Cancel subscription
      description: Cancels the subscription at the end of the current billing period. The user retains access until the period ends.
      security: [{ supabaseAuth: [] }]
      responses:
        '200':
          description: Cancellation scheduled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CancelResponse'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (5/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/reactivate:
    post:
      tags: [Billing]
      summary: Reactivate subscription
      description: Undoes a pending cancellation by setting cancel_at_period_end back to false.
      security: [{ supabaseAuth: [] }]
      responses:
        '200':
          description: Subscription reactivated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReactivateResponse'
        '400':
          description: Subscription is not pending cancellation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (5/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/subscription:
    get:
      tags: [Billing]
      summary: Get subscription details
      description: Fetches the current user's subscription, invoices, payment method, and billing address from Supabase and Stripe. Server-authoritative (not from localStorage).
      security: [{ supabaseAuth: [] }]
      responses:
        '200':
          description: Subscription details with invoices, payment method, and billing address
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubscriptionResponse'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/update-payment-method:
    post:
      tags: [Billing]
      summary: Update payment method
      description: Replaces the default payment method on both the customer and the subscription.
      security: [{ supabaseAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdatePaymentMethodRequest'
      responses:
        '200':
          description: Payment method updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
        '400':
          description: Missing payment method ID
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/buy-boost:
    post:
      tags: [Billing]
      summary: Purchase AI Budget Boost
      description: Charges the user's card on file for a one-time AI budget boost ($1.99 small or $4.99 large). Creates a Stripe invoice for receipt. Pro and Ultra subscribers only.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [boostId]
              properties:
                boostId:
                  type: string
                  enum: [small, large]
                  description: "'small' ($1.99, +$2 budget) or 'large' ($4.99, +$5 budget)"
      responses:
        '200':
          description: Boost purchased successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  budgetCents:
                    type: integer
                  label:
                    type: string
        '402':
          description: Payment failed
        '403':
          description: Not on Pro or Ultra plan

  /api/billing/buy-prep-pack:
    post:
      tags: [Billing]
      summary: Purchase Company Intel
      description: Charges the user's card on file for Company Intel ($3.99). Generates an AI company research brief with interview questions, talking points, salary range, and red flags. Sends a branded email with the brief. Pro and Ultra subscribers only.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobId, company]
              properties:
                jobId:
                  type: string
                  description: Job ID to associate the brief with
                company:
                  type: string
                  description: Company name for research
                role:
                  type: string
                  description: Job role/title (optional, improves brief quality)
                description:
                  type: string
                  description: Job description text (optional, improves brief quality)
      responses:
        '200':
          description: Company Intel brief generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  brief:
                    type: object
                    properties:
                      companyOverview:
                        type: string
                      recentNews:
                        type: array
                        items:
                          type: string
                      cultureSignals:
                        type: array
                        items:
                          type: string
                      interviewFocus:
                        type: array
                        items:
                          type: string
                      questionsToAsk:
                        type: array
                        items:
                          type: string
                      talkingPoints:
                        type: array
                        items:
                          type: string
                      salaryRange:
                        type: string
                      redFlags:
                        type: array
                        items:
                          type: string
                      purchasedAt:
                        type: string
                        format: date-time
        '400':
          description: Missing required fields or not on Pro/Ultra plan
        '402':
          description: Payment failed
        '503':
          description: AI service temporarily unavailable

  /api/billing/buy-resume-tailor:
    post:
      tags: [Billing]
      summary: Purchase AI Resume Tailor
      description: Charges $2.99 (or $3.99 bundle with cover letter) for an AI-tailored resume. Pro and Ultra subscribers only. Uses hosted AI to rewrite resume sections targeting the specified job description.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [experience, skills, jobDescription, jobTitle, jobCompany]
              properties:
                experience:
                  type: array
                  items:
                    type: object
                  description: Work experience entries
                skills:
                  type: array
                  items:
                    type: string
                  description: List of skills
                education:
                  type: array
                  items:
                    type: object
                  description: Education entries
                jobDescription:
                  type: string
                  description: Target job description text
                jobTitle:
                  type: string
                  description: Target job title
                jobCompany:
                  type: string
                  description: Target company name
                bundle:
                  type: boolean
                  description: If true, generates cover letter too ($3.99 instead of $2.99)
      responses:
        '200':
          description: Tailored resume generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  resume:
                    type: object
                    description: AI-tailored resume content
                  coverLetter:
                    type: object
                    description: AI-generated cover letter (only when bundle=true)
        '402':
          description: Payment failed
        '403':
          description: Not on Pro or Ultra plan

  /api/billing/buy-cover-letter:
    post:
      tags: [Billing]
      summary: Purchase AI Cover Letter
      description: Charges $1.99 for an AI-generated cover letter tailored to the target job. Pro and Ultra subscribers only.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [profile, jobDescription, jobTitle, jobCompany]
              properties:
                profile:
                  type: object
                  description: Candidate profile (experience, skills, education)
                jobDescription:
                  type: string
                  description: Target job description text
                jobTitle:
                  type: string
                  description: Target job title
                jobCompany:
                  type: string
                  description: Target company name
                tone:
                  type: string
                  description: Desired tone (e.g. professional, enthusiastic, concise)
      responses:
        '200':
          description: Cover letter generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  coverLetter:
                    type: object
                    properties:
                      greeting:
                        type: string
                      opening:
                        type: string
                      body:
                        type: string
                      closing:
                        type: string
                      signoff:
                        type: string
        '402':
          description: Payment failed
        '403':
          description: Not on Pro or Ultra plan

  /api/billing/buy-salary-playbook:
    post:
      tags: [Billing]
      summary: Purchase Salary Negotiation Playbook
      description: Charges $9.99 for a comprehensive salary negotiation playbook with counter-offer scripts, BATNA analysis, timeline tactics, and negotiation strategy. Pro and Ultra subscribers only.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobId, company, offerAmount]
              properties:
                jobId:
                  type: string
                  description: Job ID to associate the playbook with
                company:
                  type: string
                  description: Company name
                role:
                  type: string
                  description: Job role/title (optional, improves playbook quality)
                offerAmount:
                  oneOf:
                    - type: string
                    - type: number
                  description: Current offer amount
                offerDetails:
                  type: string
                  description: Additional offer details (equity, benefits, signing bonus, etc.)
      responses:
        '200':
          description: Salary negotiation playbook generated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  playbook:
                    type: object
                    properties:
                      counterOffer:
                        type: object
                        description: Recommended counter-offer with justification
                      negotiationScript:
                        type: string
                        description: Word-for-word negotiation script
                      batnaAnalysis:
                        type: object
                        description: Best Alternative to Negotiated Agreement analysis
                      timelineTactics:
                        type: array
                        items:
                          type: string
                        description: Timeline and pacing strategies
                      leveragePoints:
                        type: array
                        items:
                          type: string
                        description: Key leverage points to use
                      riskAssessment:
                        type: string
                        description: Risk assessment of negotiation approach
                      purchasedAt:
                        type: string
                        format: date-time
        '402':
          description: Payment failed
        '403':
          description: Not on Pro or Ultra plan

  /api/billing/refund:
    post:
      tags: [Billing]
      summary: Refund an invoice
      description: Refunds the specified invoice or the latest paid invoice if no ID is given. Extracts the PaymentIntent from the Stripe invoice and issues a full refund. Guards against double-refund race conditions.
      security: [{ supabaseAuth: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefundRequest'
      responses:
        '200':
          description: Refund issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefundResponse'
        '400':
          description: Invoice not in paid status or has no paid amount
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No eligible invoice found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Invoice has already been refunded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (3/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/proration-preview:
    post:
      tags: [Billing]
      summary: Preview plan switch proration
      description: Dry-runs a plan switch using stripe.invoices.createPreview() to show what the user will be charged or credited. Does not create a real invoice.
      security: [{ supabaseAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProrationPreviewRequest'
      responses:
        '200':
          description: Proration preview with line items
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProrationPreviewResponse'
        '400':
          description: Invalid plan or already on this plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error or price not configured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/update-address:
    post:
      tags: [Billing]
      summary: Update billing address
      description: Updates the Stripe customer's billing address. Required for Stripe Tax to calculate tax correctly.
      security: [{ supabaseAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAddressRequest'
      responses:
        '200':
          description: Address updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
        '400':
          description: Address line 1 and country are required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/billing/portal:
    post:
      tags: [Billing]
      summary: Create Stripe Customer Portal session
      description: Creates a Stripe Customer Portal session for complex billing operations not supported by the in-app billing UI. Returns a URL that redirects back to /app?tab=billing.
      security: [{ supabaseAuth: [] }]
      responses:
        '200':
          description: Portal session URL
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PortalResponse'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No subscription found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (5/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: Stripe error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── AI - Voice ──
  /api/voice-parse-job:
    post:
      tags: [AI - Job Parsing]
      summary: Parse voice transcript into job fields
      description: Parses a voice transcript (from speech-to-text) into structured job data including company, role, salary, status, and tags. The transcript is truncated to 2000 characters.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
        - $ref: '#/components/parameters/aiModel'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [transcript]
              properties:
                transcript: { type: string, minLength: 5 }
      responses:
        '200':
          description: Parsed job fields from voice transcript
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    type: object
                    properties:
                      company: { type: string }
                      role: { type: string }
                      salary: { type: string }
                      url: { type: string }
                      status: { type: string, enum: [saved, applied, screening, interviewing, offer] }
                      priority: { type: string, enum: [high, medium, low] }
                      notes: { type: string }
                      location: { type: string }
                      tags: { type: array, items: { type: string } }
        '400':
          description: Transcript missing or too short (minimum 5 characters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/voice-parse-contact:
    post:
      tags: [AI - Job Parsing]
      summary: Parse voice transcript into contact fields
      description: Parses a voice transcript (from speech-to-text) into structured contact data including name, title, company, email, phone, type, and notes. The transcript is truncated to 2000 characters.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
        - $ref: '#/components/parameters/aiModel'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [transcript]
              properties:
                transcript: { type: string, minLength: 5 }
      responses:
        '200':
          description: Parsed contact fields from voice transcript
          content:
            application/json:
              schema:
                type: object
                properties:
                  contact:
                    type: object
                    properties:
                      name: { type: string }
                      title: { type: string }
                      company: { type: string }
                      email: { type: string }
                      phone: { type: string }
                      linkedIn: { type: string }
                      type: { type: string, enum: [connection, recruiter, referral, hiring-manager, friend] }
                      notes: { type: string }
        '400':
          description: Transcript missing or too short (minimum 5 characters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/voice-parse-event:
    post:
      tags: [AI - Job Parsing]
      summary: Parse voice transcript into calendar event fields
      description: Parses a voice transcript (from speech-to-text) into structured calendar event data including title, date, time, end time, location, and notes. Accepts an optional currentDate for resolving relative dates. The transcript is truncated to 2000 characters.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
        - $ref: '#/components/parameters/aiModel'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [transcript]
              properties:
                transcript: { type: string, minLength: 5 }
                currentDate: { type: string, format: date, description: "Current date for resolving relative dates (e.g. 'tomorrow', 'next Tuesday'). Defaults to server date." }
      responses:
        '200':
          description: Parsed calendar event fields from voice transcript
          content:
            application/json:
              schema:
                type: object
                properties:
                  event:
                    type: object
                    properties:
                      title: { type: string }
                      date: { type: string, format: date }
                      time: { type: string, description: "24-hour format HH:MM" }
                      endTime: { type: string, description: "24-hour format HH:MM" }
                      location: { type: string }
                      url: { type: string }
                      notes: { type: string }
        '400':
          description: Transcript missing or too short (minimum 5 characters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/voice-debrief:
    post:
      tags: [AI - Interview]
      summary: Generate interview debrief from voice transcript
      description: Analyzes a voice transcript of a post-interview debrief and generates structured feedback, questions asked, action items, and a thank-you email draft. Transcript is truncated to 4000 characters.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
        - $ref: '#/components/parameters/aiModel'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [transcript]
              properties:
                transcript: { type: string, minLength: 20 }
                company: { type: string }
                role: { type: string }
      responses:
        '200':
          description: Structured interview debrief from voice transcript
          content:
            application/json:
              schema:
                type: object
                properties:
                  debrief:
                    type: object
                    properties:
                      wentWell: { type: array, items: { type: string } }
                      toImprove: { type: array, items: { type: string } }
                      questionsAsked: { type: array, items: { type: string } }
                      nextRoundTips: { type: array, items: { type: string } }
                      thankYouDraft: { type: string }
                      overallSentiment: { type: string, enum: [positive, neutral, concerned] }
                      keyMoments: { type: array, items: { type: string } }
                      actionItems: { type: array, items: { type: string } }
        '400':
          description: Transcript missing or too short (minimum 20 characters)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (10/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── AI - Agents ──
  /api/agents/enhance:
    post:
      tags: [AI - Agents]
      summary: AI enhancement for agent suggestions
      description: Takes template-based agent suggestions and enhances them with AI-generated personalized content (email drafts, advice). Requires Ultra tier and user's AI API key. Caps at 15 situations per request.
      security: [{ supabaseAuth: [], aiApiKey: [] }]
      parameters:
        - $ref: '#/components/parameters/aiProvider'
        - $ref: '#/components/parameters/aiModel'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [situations]
              properties:
                situations:
                  type: array
                  minItems: 1
                  maxItems: 15
                  items:
                    type: object
                    required: [id, agent, type, context]
                    properties:
                      id: { type: string }
                      agent: { type: string, enum: [pipeline-monitor, followup-drafter, application-coach, runway-agent, interview-prep-agent, debrief-reminder, readiness-coach] }
                      type: { type: string }
                      context:
                        type: object
                        properties:
                          company: { type: string }
                          role: { type: string }
                          contactName: { type: string }
                          templateText: { type: string }
      responses:
        '200':
          description: Enhanced suggestions with AI-generated content
          content:
            application/json:
              schema:
                type: object
                properties:
                  enhanced:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        title: { type: string }
                        draft: { type: string }
                        subject: { type: string }
        '400':
          description: Missing API key, invalid provider, empty situations, or invalid agent/type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Requires Ultra tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (5/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/agents/status:
    get:
      tags: [AI - Agents]
      summary: Get agent config and AI usage
      description: Returns agent configuration, recent suggestions, and AI usage for the current billing period. Requires Ultra tier.
      security: [{ supabaseAuth: [] }]
      responses:
        '200':
          description: Agent status with config, suggestions, and usage
          content:
            application/json:
              schema:
                type: object
                properties:
                  config:
                    type: object
                    description: Agent configuration (active agents, schedule preferences)
                  suggestions:
                    type: array
                    items:
                      type: object
                    description: Recent agent suggestions
                  usage:
                    type: object
                    nullable: true
                    properties:
                      total_input_tokens: { type: integer }
                      total_output_tokens: { type: integer }
                      total_cost_cents: { type: number }
                      period_start: { type: string, format: date-time }
                    description: AI usage for current billing period
                  budgetCents: { type: integer, description: "Maximum AI spend per billing period in cents" }
                  featureLimits:
                    type: object
                    description: Per-feature monthly call limits
        '401':
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Requires Ultra tier
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/cron/agent-dispatcher:
    get:
      tags: [AI - Agents]
      summary: Run scheduled agents for Ultra users
      description: Vercel Cron job that runs hourly. For each Ultra user with agents enabled, checks timezone-aware schedules, runs detection, enhances with hosted AI, and stores results. Authenticated via CRON_SECRET header.
      security: []
      parameters:
        - name: authorization
          in: header
          required: true
          schema:
            type: string
          description: "Bearer {CRON_SECRET}, set automatically by Vercel"
      responses:
        '200':
          description: Agent run results
          content:
            application/json:
              schema:
                type: object
                properties:
                  processed: { type: integer, description: Number of users processed }
                  suggestions: { type: integer, description: Total new suggestions generated }
        '401':
          description: Invalid or missing CRON_SECRET
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ── Account ──
  /api/delete-account:
    delete:
      tags: [Account]
      summary: Delete user account
      description: Permanently deletes the user's account and all associated data. Uses service role.
      security: [{ supabaseAuth: [] }]
      responses:
        '200':
          description: Account deleted
        '401':
          description: Not authenticated

  # ── MCP (Model Context Protocol) ──

  /api/mcp/me:
    get:
      operationId: getMe
      tags: [MCP]
      summary: Get user identity, pipeline summary, goals, runway, and wellness
      description: Returns the authenticated user's identity, subscription tier, pipeline summary, Activity Rings progress (goals), financial runway, wellness check-in status, and rate limit info. One call instead of many.
      security: [{ extensionToken: [] }]
      responses:
        '200':
          description: Combined user profile and dashboard data
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: { type: string, nullable: true, description: Display name }
                  email: { type: string, nullable: true }
                  tier: { type: string, enum: [free, unlimited, pro, ultra] }
                  streak: { type: integer, description: Current goals streak in days }
                  lastMood: { type: string, nullable: true, description: Most recent mood check-in label }
                  pipeline:
                    type: object
                    properties:
                      totalJobs: { type: integer }
                      byStatus: { type: object, additionalProperties: { type: integer } }
                      totalContacts: { type: integer }
                      activeOffers: { type: integer }
                  goalsRings:
                    type: object
                    description: Activity Rings progress for today
                    properties:
                      applicationsProgress: { type: number, minimum: 0, maximum: 1 }
                      connectionsProgress: { type: number, minimum: 0, maximum: 1 }
                      engagementProgress: { type: number, minimum: 0, maximum: 1 }
                      allClosed: { type: boolean }
                      applicationsCurrent: { type: integer }
                      applicationsGoal: { type: integer }
                      connectionsCurrent: { type: integer }
                      connectionsGoal: { type: integer }
                      engagementCurrent: { type: integer }
                      engagementGoal: { type: integer }
                  runway:
                    type: object
                    nullable: true
                    description: Financial runway data (null if not configured)
                    properties:
                      runwayMonths: { type: number }
                      monthlyExpenses: { type: number }
                      monthlyIncome: { type: number }
                      capitalAmount: { type: number }
                      burnRate: { type: number }
                      severity: { type: string, enum: [critical, warning, healthy] }
                  wellness:
                    type: object
                    nullable: true
                    description: Wellness check-in status
                    properties:
                      currentMood: { type: string, nullable: true, description: "Today's mood label (great, good, okay, rough, struggling)" }
                      currentStreak: { type: integer }
                      lastCheckinDate: { type: string, format: date-time, nullable: true }
                      weeklyAverage: { type: number, nullable: true }
                      recentCheckins:
                        type: array
                        items:
                          type: object
                          properties:
                            date: { type: string, format: date, description: "YYYY-MM-DD" }
                            mood: { type: string, description: "great, good, okay, rough, struggling" }
                        description: "Last 30 mood check-ins with date and mood label"
                  tokenScopes:
                    type: array
                    items: { type: string }
                  rateLimits:
                    type: object
                    properties:
                      perMinuteLimit: { type: integer }
                      perMinuteRemaining: { type: integer }
                      monthlyLimit: { type: integer }
                      monthlyUsed: { type: integer }
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: meAction
      tags: [MCP]
      summary: Perform an action on the user's account
      description: Currently supports logMood to record a mood check-in for today.
      security: [{ extensionToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action: { type: string, enum: [logMood], description: "Action to perform" }
                mood: { type: string, enum: [great, good, okay, rough, struggling], description: "Mood level (required for logMood)" }
                labels: { type: array, items: { type: string }, description: "Optional mood labels" }
                note: { type: string, description: "Optional note" }
      responses:
        '200':
          description: Action completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string }
                  date: { type: string, format: date }
                  mood: { type: string }

  /api/mcp/jobs:
    get:
      operationId: listJobs
      tags: [MCP]
      summary: List jobs in pipeline
      description: Returns all jobs in the user's pipeline with status, company, role, priority, and next action. Supports filtering by status and cursor-based pagination. Sorted by most recently updated first.
      security: [{ extensionToken: [] }]
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [saved, applied, screening, interviewing, offer, rejected, withdrawn]
          description: Filter jobs by pipeline status
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            maximum: 500
          description: Maximum number of jobs to return
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Pagination cursor (ISO timestamp from previous response's nextCursor)
      responses:
        '200':
          description: Paginated list of jobs with metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobs:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        company: { type: string }
                        role: { type: string }
                        status: { type: string, enum: [saved, applied, screening, interviewing, offer, rejected, withdrawn] }
                        priority: { type: string, enum: [high, medium, low] }
                        location: { type: string }
                        salary: { type: string }
                        url: { type: string }
                        dateAdded: { type: string, format: date-time }
                        nextAction: { type: string }
                        nextActionDate: { type: string, format: date }
                        lastUpdated: { type: string, format: date-time }
                  count: { type: integer, description: Number of jobs returned in this page }
                  hasMore: { type: boolean, description: Whether more results are available }
                  nextCursor: { type: string, nullable: true, description: Cursor for next page }
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: addJob
      tags: [MCP]
      summary: Add a job to pipeline
      description: Creates a new job in the pipeline. Requires at least a company or role name. Checks for duplicate jobs by URL. Dispatches a job.created webhook on success.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                company: { type: string, maxLength: 200, description: Company name }
                role: { type: string, maxLength: 200, description: Job title or role name }
                url: { type: string, maxLength: 2048, description: Job posting URL }
                status: { type: string, enum: [saved, applied, screening, interviewing, offer, rejected, withdrawn], default: saved, description: Initial pipeline status }
                location: { type: string, maxLength: 200, description: Job location }
                salary: { type: string, maxLength: 100, description: Salary range or amount }
                priority: { type: string, enum: [high, medium, low], default: medium, description: Priority level }
                notes: { type: string, maxLength: 5000, description: Additional notes about the job }
                tags: { type: array, items: { type: string }, maxItems: 20, description: Tags for categorization }
      responses:
        '201':
          description: Job created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [created] }
                  job:
                    type: object
                    properties:
                      id: { type: string }
                      company: { type: string }
                      role: { type: string }
                      status: { type: string }
                      priority: { type: string }
        '400':
          description: At least company or role is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Tier job limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Duplicate job (same URL already exists)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/jobs/{id}:
    get:
      operationId: getJob
      tags: [MCP]
      summary: Get job details
      description: Returns full details for a single job including description, notes, tags, linked contact IDs, dates, and favorite status. Use this for detailed views or before making updates.
      security: [{ extensionToken: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The job ID
      responses:
        '200':
          description: Full job details
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    type: object
                    properties:
                      id: { type: string }
                      company: { type: string }
                      role: { type: string }
                      status: { type: string, enum: [saved, applied, screening, interviewing, offer, rejected, withdrawn] }
                      priority: { type: string, enum: [high, medium, low] }
                      location: { type: string }
                      salary: { type: string }
                      url: { type: string }
                      description: { type: string }
                      dateAdded: { type: string, format: date-time }
                      dateApplied: { type: string, format: date }
                      nextAction: { type: string }
                      nextActionDate: { type: string, format: date }
                      notes: { type: string }
                      tags: { type: array, items: { type: string } }
                      favorite: { type: boolean }
                      contactIds: { type: array, items: { type: string } }
                      createdAt: { type: string, format: date-time }
                      updatedAt: { type: string, format: date-time }
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    patch:
      operationId: updateJob
      tags: [MCP]
      summary: Update job fields
      description: Partially updates a job's details. Only provided fields are modified; omitted fields remain unchanged. Use this for editing company, role, location, salary, notes, tags, priority, URL, favorite status, or next action. Dispatches a job.updated webhook on success.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The job ID to update
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                company: { type: string, maxLength: 200, description: Company name }
                role: { type: string, maxLength: 200, description: Job title or role name }
                location: { type: string, maxLength: 200, description: Job location }
                salary: { type: string, maxLength: 100, description: Salary range or amount }
                notes: { type: string, maxLength: 5000, description: Additional notes about the job }
                tags: { type: array, items: { type: string }, maxItems: 20, description: Tags for categorization }
                priority: { type: string, enum: [high, medium, low], description: Priority level }
                url: { type: string, maxLength: 2048, description: Job posting URL }
                favorite: { type: boolean, description: Whether the job is favorited }
                nextAction: { type: string, maxLength: 200, description: "Next action to take (e.g., 'Follow up in 1 week')" }
                nextActionDate: { type: string, format: date, description: Due date for the next action (YYYY-MM-DD) }
      responses:
        '200':
          description: Job updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    type: object
                    properties:
                      id: { type: string }
                      company: { type: string }
                      role: { type: string }
                      status: { type: string }
                      priority: { type: string }
                      location: { type: string }
                      salary: { type: string }
                      url: { type: string }
                      notes: { type: string }
                      tags: { type: array, items: { type: string } }
                      favorite: { type: boolean }
                      nextAction: { type: string }
                      nextActionDate: { type: string }
                      lastUpdated: { type: string, format: date-time }
        '400':
          description: At least one field to update is required, or invalid field values
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      operationId: deleteJob
      tags: [MCP]
      summary: Delete a job
      description: Permanently deletes a job from the pipeline. Also removes related activities. Dispatches a job.deleted webhook with the deleted job's company, role, and status.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The job ID to delete
      responses:
        '200':
          description: Job deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean, enum: [true] }
                  id: { type: string }
        '400':
          description: Job ID is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/jobs/{id}/status:
    post:
      operationId: updateJobStatus
      tags: [MCP]
      summary: Update job status
      description: Updates a job's pipeline status and optionally sets the next action. Automatically sets dateApplied when moving to "applied" status. Dispatches a job.status_changed webhook on success.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The job ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [saved, applied, screening, interviewing, offer, rejected, withdrawn]
                  description: New pipeline status
                nextAction:
                  type: string
                  maxLength: 200
                  description: "Optional next action to set (e.g., 'Follow up in 1 week')"
                nextActionDate:
                  type: string
                  format: date
                  description: Optional due date for the next action (YYYY-MM-DD)
      responses:
        '200':
          description: Status updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    type: object
                    properties:
                      id: { type: string }
                      company: { type: string }
                      role: { type: string }
                      previousStatus: { type: string }
                      status: { type: string }
        '400':
          description: Missing or invalid status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/contacts:
    get:
      operationId: listContacts
      tags: [MCP]
      summary: List contacts
      description: Returns networking contacts with company, role, relationship type, and linked job count. Supports search, sorting, and cursor-based pagination.
      security: [{ extensionToken: [] }]
      parameters:
        - name: search
          in: query
          required: false
          schema:
            type: string
          description: Search across name, company, email, and title (case-insensitive partial match)
        - name: company
          in: query
          required: false
          schema:
            type: string
          description: Filter by company name (case-insensitive partial match)
        - name: sort
          in: query
          required: false
          schema:
            type: string
            enum: [updated_at, name]
            default: updated_at
          description: Sort order. "name" sorts A-Z (cursor is the last name), "updated_at" sorts newest first (cursor is ISO timestamp)
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 50
            maximum: 200
          description: Maximum number of contacts to return per page
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Pagination cursor from previous response's nextCursor
      responses:
        '200':
          description: Paginated list of contacts
          content:
            application/json:
              schema:
                type: object
                properties:
                  contacts:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        title: { type: string }
                        company: { type: string }
                        email: { type: string }
                        phone: { type: string }
                        type: { type: string, enum: [recruiter, hiring-manager, referral, colleague, connection, other] }
                        notes: { type: string }
                        linkedJobCount: { type: integer }
                        lastContacted: { type: string, format: date-time }
                        lastUpdated: { type: string, format: date-time }
                  count: { type: integer, description: Number of contacts returned in this page }
                  hasMore: { type: boolean, description: Whether more results are available }
                  nextCursor: { type: string, nullable: true, description: Cursor for next page }
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: addContact
      tags: [MCP]
      summary: Create a contact
      description: Creates a new networking contact. Supports linking to existing jobs via jobIds for bidirectional tracking. Logs a contact-added activity and dispatches a contact.created webhook.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, maxLength: 200, description: "Contact's full name" }
                title: { type: string, maxLength: 200, description: "Contact's job title" }
                company: { type: string, maxLength: 200, description: "Contact's company" }
                email: { type: string, maxLength: 300, description: "Contact's email address" }
                phone: { type: string, maxLength: 50, description: "Contact's phone number" }
                linkedIn: { type: string, maxLength: 500, description: LinkedIn profile URL }
                type:
                  type: string
                  enum: [recruiter, hiring-manager, referral, colleague, connection, other]
                  default: connection
                  description: Relationship type
                notes: { type: string, maxLength: 5000, description: Notes about the contact }
                jobIds:
                  type: array
                  items: { type: string }
                  maxItems: 50
                  description: Array of job IDs to link this contact to (bidirectional)
      responses:
        '201':
          description: Contact created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [created] }
                  contact:
                    type: object
                    properties:
                      id: { type: string }
                      name: { type: string }
                      title: { type: string }
                      company: { type: string }
                      type: { type: string }
        '400':
          description: Name is required or invalid field values
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Tier contact limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/contacts/{id}:
    get:
      operationId: getContact
      tags: [MCP]
      summary: Get contact details
      description: Returns full details for a single contact including email, phone, LinkedIn, notes, and linked job IDs. Use this for detailed views or before making updates.
      security: [{ extensionToken: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The contact ID
      responses:
        '200':
          description: Full contact details
          content:
            application/json:
              schema:
                type: object
                properties:
                  contact:
                    type: object
                    properties:
                      id: { type: string }
                      name: { type: string }
                      title: { type: string }
                      company: { type: string }
                      email: { type: string }
                      phone: { type: string }
                      linkedIn: { type: string }
                      type: { type: string, enum: [recruiter, hiring-manager, referral, colleague, connection, other] }
                      notes: { type: string }
                      jobIds: { type: array, items: { type: string } }
                      lastContacted: { type: string, format: date-time }
                      createdAt: { type: string, format: date-time }
                      updatedAt: { type: string, format: date-time }
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    patch:
      operationId: updateContact
      tags: [MCP]
      summary: Update contact fields
      description: Partially updates a contact's details. Only provided fields are modified; omitted fields remain unchanged. Dispatches a contact.updated webhook on success.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The contact ID to update
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 200, description: "Contact's full name (cannot be empty)" }
                title: { type: string, maxLength: 200, description: "Contact's job title" }
                company: { type: string, maxLength: 200, description: "Contact's company" }
                email: { type: string, maxLength: 300, description: "Contact's email address" }
                phone: { type: string, maxLength: 50, description: "Contact's phone number" }
                type:
                  type: string
                  enum: [recruiter, hiring-manager, referral, colleague, connection, other]
                  description: Relationship type
                notes: { type: string, maxLength: 5000, description: Notes about the contact }
                jobIds:
                  type: array
                  items: { type: string }
                  maxItems: 50
                  description: Array of job IDs to link this contact to
      responses:
        '200':
          description: Contact updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  contact:
                    type: object
                    properties:
                      id: { type: string }
                      name: { type: string }
                      title: { type: string }
                      company: { type: string }
                      email: { type: string }
                      phone: { type: string }
                      type: { type: string }
                      notes: { type: string }
                      jobIds: { type: array, items: { type: string } }
                      lastUpdated: { type: string, format: date-time }
        '400':
          description: At least one field to update is required, or invalid field values
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      operationId: deleteContact
      tags: [MCP]
      summary: Delete a contact
      description: Permanently deletes a contact. Automatically removes the contact ID from any linked jobs (bidirectional cleanup). Dispatches a contact.deleted webhook with the deleted contact's name, company, and type.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The contact ID to delete
      responses:
        '200':
          description: Contact deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: boolean, enum: [true] }
                  id: { type: string }
        '400':
          description: Contact ID is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/activities:
    get:
      operationId: listActivities
      tags: [MCP]
      summary: List activities with filters
      description: Returns job search activities sorted newest first. Supports filtering by job ID, contact ID, activity type, and date range. Uses cursor-based pagination for large result sets.
      security: [{ extensionToken: [] }]
      parameters:
        - name: jobId
          in: query
          required: false
          schema:
            type: string
          description: Filter activities by job ID
        - name: contactId
          in: query
          required: false
          schema:
            type: string
          description: Filter activities by contact ID
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum: [applied, email-sent, interview-scheduled, offer-received, status-change, note-added, contact-added, job-added]
          description: Filter by activity type
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
          description: Only return activities after this ISO date (e.g., 2024-01-15T00:00:00Z)
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 50
            maximum: 200
          description: Maximum number of activities to return
        - name: cursor
          in: query
          required: false
          schema:
            type: string
          description: Pagination cursor (ISO timestamp from previous response's nextCursor)
      responses:
        '200':
          description: Paginated list of activities
          content:
            application/json:
              schema:
                type: object
                properties:
                  activities:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        type: { type: string, enum: [applied, email-sent, interview-scheduled, offer-received, status-change, note-added, contact-added, job-added] }
                        title: { type: string }
                        description: { type: string }
                        jobId: { type: string }
                        contactId: { type: string }
                        date: { type: string, format: date-time }
                        createdAt: { type: string, format: date-time }
                  count: { type: integer, description: Number of activities returned in this page }
                  hasMore: { type: boolean, description: Whether more results are available }
                  nextCursor: { type: string, nullable: true, description: Cursor for next page }
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: logActivity
      tags: [MCP]
      summary: Log an activity
      description: Records a job search activity such as sending an email, scheduling an interview, or adding a note. Validates that referenced jobId exists if provided. Dispatches an activity.created webhook on success.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, title]
              properties:
                type:
                  type: string
                  enum: [applied, email-sent, interview-scheduled, offer-received, status-change, note-added, contact-added, job-added]
                  description: Activity type
                title:
                  type: string
                  maxLength: 200
                  description: Short description of the activity
                jobId:
                  type: string
                  description: ID of the related job (validated for existence)
                contactId:
                  type: string
                  description: ID of the related contact
                description:
                  type: string
                  maxLength: 2000
                  description: Detailed description of the activity
      responses:
        '201':
          description: Activity logged successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [created] }
                  activity:
                    type: object
                    properties:
                      id: { type: string }
                      type: { type: string }
                      title: { type: string }
        '400':
          description: Missing type or title, or invalid activity type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Referenced job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/calendar:
    get:
      operationId: getCalendar
      tags: [MCP]
      summary: Get upcoming interviews and events
      description: Returns jobs in screening/interviewing stages, upcoming calendar events from event metadata, and recent interview-scheduled activities. Provides a natural language summary of interview and event counts.
      security: [{ extensionToken: [] }]
      responses:
        '200':
          description: Calendar data with summary, interviews, events, and recent activity
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary: { type: string, description: "Natural language summary (e.g., '2 upcoming interviews, 3 calendar events')" }
                  interviews:
                    type: array
                    items:
                      type: object
                      properties:
                        jobId: { type: string }
                        company: { type: string }
                        role: { type: string }
                        stage: { type: string, enum: [screening, interviewing] }
                        nextAction: { type: string }
                        nextActionDate: { type: string, format: date }
                        isUpcoming: { type: boolean }
                  calendarEvents:
                    type: array
                    items:
                      type: object
                      properties:
                        eventId: { type: string }
                        title: { type: string }
                        date: { type: string, format: date }
                        time: { type: string, description: "24-hour format HH:MM" }
                        endTime: { type: string, description: "24-hour format HH:MM" }
                        location: { type: string }
                        type: { type: string, enum: [interview, phone-screen, follow-up, networking, event, other] }
                  recentInterviewActivity:
                    type: array
                    items:
                      type: object
                      properties:
                        title: { type: string }
                        date: { type: string, format: date-time }
                        jobId: { type: string }
                        description: { type: string }
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: createCalendarEvent
      tags: [MCP]
      summary: Create a calendar event
      description: Creates a calendar event such as an interview, phone screen, networking meeting, or follow-up. Stores the event as an activity row with associated metadata (location, duration, alerts) and hour placement. Interview/phone-screen types are logged as interview-scheduled activities.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, date]
              properties:
                title:
                  type: string
                  maxLength: 200
                  description: "Event title (e.g., 'Interview with Google', 'Coffee with recruiter')"
                date:
                  type: string
                  format: date
                  description: Event date in YYYY-MM-DD format
                time:
                  type: string
                  description: "Start time in HH:MM format (24h). Omit for all-day events."
                endTime:
                  type: string
                  description: "End time in HH:MM format (24h). Defaults to 1 hour after start."
                location:
                  type: string
                  maxLength: 500
                  description: Event location or meeting link
                url:
                  type: string
                  maxLength: 2048
                  description: Related URL (meeting link, calendar invite)
                notes:
                  type: string
                  maxLength: 5000
                  description: Additional notes for the event
                jobId:
                  type: string
                  description: ID of the related job (validated for existence)
                type:
                  type: string
                  enum: [interview, phone-screen, follow-up, networking, event, other]
                  default: event
                  description: Event type
      responses:
        '201':
          description: Calendar event created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [created] }
                  event:
                    type: object
                    properties:
                      id: { type: string, description: "Event ID (format: act-{uuid})" }
                      activityId: { type: string }
                      title: { type: string }
                      date: { type: string, format: date }
                      time: { type: string }
                      endTime: { type: string }
                      location: { type: string }
                      url: { type: string }
                      type: { type: string }
                      allDay: { type: boolean }
        '400':
          description: Title and date are required, or invalid date format/event type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Referenced job not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/pipeline:
    get:
      operationId: getPipeline
      tags: [MCP]
      summary: Get pipeline summary
      description: Returns pipeline analytics including total and active job counts, status breakdown, response rate, stale applications (14+ days with no response), jobs needing follow-up (overdue next actions), active offers, high-priority items, and natural language insights.
      security: [{ extensionToken: [] }]
      responses:
        '200':
          description: Pipeline summary with analytics and insights
          content:
            application/json:
              schema:
                type: object
                properties:
                  total: { type: integer, description: Total number of jobs }
                  active: { type: integer, description: Jobs not rejected or withdrawn }
                  statusCounts:
                    type: object
                    additionalProperties: { type: integer }
                    description: "Job count per status (e.g., { saved: 5, applied: 10 })"
                  responseRate: { type: string, description: "Percentage of applied jobs that advanced (e.g., '25%')" }
                  recentlyAdded: { type: integer, description: Jobs added in the last 7 days }
                  insights:
                    type: array
                    items: { type: string }
                    description: Natural language insights about pipeline health
                  needsFollowUp:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        company: { type: string }
                        role: { type: string }
                        action: { type: string }
                        dueDate: { type: string, format: date }
                    description: Jobs with overdue next actions
                  staleApplications:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        company: { type: string }
                        role: { type: string }
                        dateApplied: { type: string, format: date }
                    description: Applied 14+ days ago with no advancement
                  offers:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        company: { type: string }
                        role: { type: string }
                    description: Jobs currently in offer stage
                  highPriority:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        company: { type: string }
                        role: { type: string }
                        status: { type: string }
                    description: Active jobs marked as high priority
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/suggestions:
    get:
      operationId: getSuggestions
      tags: [MCP]
      summary: Get suggestions and recommendations
      description: Returns AI agent suggestions (if Ultra tier) and computed action items including overdue next actions, due-today items, stale applications needing follow-up, and saved jobs to review. Recommendations are sorted by priority (high, medium, low).
      security: [{ extensionToken: [] }]
      responses:
        '200':
          description: Suggestions and action item recommendations
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary: { type: string, description: "Natural language summary (e.g., '3 AI suggestions, 5 action items')" }
                  agentSuggestions:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        agent: { type: string, enum: [pipeline-monitor, followup-drafter, application-coach, runway-agent, interview-prep-agent, debrief-reminder, readiness-coach] }
                        title: { type: string }
                        body: { type: string }
                        priority: { type: string, enum: [high, medium, low] }
                        createdAt: { type: string, format: date-time }
                        action:
                          type: object
                          properties:
                            type: { type: string }
                            label: { type: string }
                    description: AI agent suggestions (Ultra tier only, may be empty)
                  recommendations:
                    type: array
                    items:
                      type: object
                      properties:
                        priority: { type: string, enum: [high, medium, low] }
                        suggestion: { type: string }
                        jobId: { type: string }
                        company: { type: string }
                        role: { type: string }
                    description: Computed action items from pipeline state (max 20)
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/webhooks:
    get:
      operationId: listWebhooks
      tags: [MCP]
      summary: List registered webhooks
      description: Returns all registered outbound webhooks for the authenticated user. Webhooks receive HTTP POST notifications when events occur in the pipeline (e.g., job created, status changed, offer received).
      security: [{ extensionToken: [] }]
      responses:
        '200':
          description: List of registered webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        url: { type: string, format: uri }
                        events:
                          type: array
                          items: { type: string }
                          description: Subscribed event types
                        active: { type: boolean }
                        createdAt: { type: string, format: date-time }
                  count: { type: integer }
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      operationId: registerWebhook
      tags: [MCP]
      summary: Register a webhook
      description: Registers a new outbound webhook URL to receive event notifications. Supports Zapier, Make.com, n8n, Apple Shortcuts, and any HTTP endpoint. Maximum 10 webhooks per user. SSRF protection blocks private/internal IPs. When a secret is provided, each delivery includes an X-Orbyt-Signature header (HMAC SHA-256).
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url:
                  type: string
                  format: uri
                  description: The webhook endpoint URL (must be http or https, no private IPs)
                events:
                  type: array
                  items:
                    type: string
                    enum: [job.created, job.updated, job.status_changed, job.deleted, contact.created, contact.updated, contact.deleted, activity.created, interview.scheduled, offer.received, '*']
                  description: "Event types to subscribe to. Use '*' to subscribe to all events."
                secret:
                  type: string
                  maxLength: 256
                  description: Optional HMAC secret for payload signing (X-Orbyt-Signature header)
      responses:
        '201':
          description: Webhook registered successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [created] }
                  webhook:
                    type: object
                    properties:
                      id: { type: string }
                      url: { type: string }
                      events: { type: array, items: { type: string } }
                      active: { type: boolean }
                      createdAt: { type: string, format: date-time }
        '400':
          description: Invalid URL, private IP, missing events, invalid event types, or max 10 webhooks exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      operationId: deleteWebhook
      tags: [MCP]
      summary: Delete a webhook
      description: Removes a registered webhook by ID. The webhook will stop receiving event notifications immediately. Verifies ownership before deletion.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id:
                  type: string
                  description: The webhook ID to delete
      responses:
        '200':
          description: Webhook deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [deleted] }
                  id: { type: string }
        '400':
          description: Webhook ID is required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/mcp/webhooks/test:
    post:
      operationId: testWebhook
      tags: [MCP]
      summary: Test a webhook with a ping
      description: Sends a synthetic webhook.test event to the specified webhook and returns the delivery result including HTTP status code. Useful for verifying that a webhook endpoint is reachable and responding correctly before relying on it for real events. SSRF protection blocks private/internal IPs.
      security: [{ extensionToken: [] }]
      x-openai-isConsequential: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [webhookId]
              properties:
                webhookId:
                  type: string
                  description: The webhook ID to test
      responses:
        '200':
          description: Test delivery result (success or failure with status code)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, description: Whether the endpoint returned a 2xx status }
                  statusCode: { type: integer, nullable: true, description: HTTP status code from the endpoint }
                  error: { type: string, description: Error message if delivery failed }
                  webhookId: { type: string }
                  url: { type: string }
        '400':
          description: webhookId is required, or webhook URL points to private IP
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Invalid or revoked extension token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded (20/min per token)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
