openapi: 3.0.3
info:
  title: StockUp Quan AI API
  description: Finance-native AI API with server-verified market context, Quan reasoning models, MCP and OpenAPI discovery, 100,000 free Quan 3.4 L tokens, streaming responses, and prepaid usage controls.
  version: 1.1.0
servers:
  - url: https://stockup.cc
    description: StockUp production server
  - url: http://127.0.0.1:5001/stockup-4ab83/us-central1
    description: Local Firebase Emulator
paths:
  /v1/query:
    post:
      summary: Query StockUp Quan Financial AI Models
      description: Submits a query to StockUp Quan financial AI models. Enterprise aliases such as quan-3-4-enterprise support team-based finance AI chatbot workflows and require an active Enterprise API key, approved Enterprise plan, organization token pool, and X-Enterprise-User-Id for team attribution and per-user quota governance.
      operationId: queryQuanModel
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - name: X-Enterprise-User-Id
          in: header
          required: false
          schema:
            type: string
          description: Required when calling Enterprise model aliases. Identifies the active Enterprise employee or member whose per-user quota and team usage should be charged.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QueryRequest'
      responses:
        '200':
          description: Successful query execution
          headers:
            X-StockUp-Billing-Mode:
              schema: { type: string, enum: [free_quan30, free_quan34_light, hybrid_quan34_light, cash] }
              description: Settled billing path for the request.
            X-StockUp-Free-Model:
              schema: { type: string, enum: [quan-3.0, quan-3.4-l] }
              description: Allowance used by the request, when applicable.
            X-StockUp-Free-Tokens-Remaining:
              schema: { type: integer, minimum: 0 }
              description: Remaining account allowance for X-StockUp-Free-Model.
            X-StockUp-Included-Search-Queries:
              schema: { type: integer, minimum: 0 }
              description: Executed searches covered by the Quan 3.4 L allowance.
            X-StockUp-Paid-Search-Queries:
              schema: { type: integer, minimum: 0 }
              description: Executed searches charged to prepaid balance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryResponse'
            text/event-stream:
              description: Server-Sent Events (SSE) stream chunks when stream is true
        '202':
          description: PDF document job accepted when response_format.type is pdf
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentJobAccepted'
        '401':
          description: Unauthorized - API key invalid or missing
        '402':
          description: Payment Required - The Quan 3.4 L allowance cannot cover the complete request, or prepaid balance is insufficient. FREE_QUAN34_LIGHT_ALLOWANCE_EXCEEDED includes freeTokensRemaining.
        '403':
          description: Forbidden - Enterprise key, active plan, member status, user quota, or organization access requirement is not satisfied
        '429':
          description: Too Many Requests - Rate limit, member token limit, or organization monthly token pool limit reached
        '413':
          description: Request body exceeds the 2 MB API limit
        '500':
          description: Internal server error
  /v1/decision:
    post:
      summary: Run Quan Decision Intelligence
      operationId: runQuanDecisionIntelligence
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DecisionIntelligenceRequest' }
      responses:
        '200':
          description: Verified decision, regime, signals, risk, scenarios, evidence, and audit record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DecisionIntelligenceResult' }
  /v1/regime:
    post:
      summary: Get the verified seven-state market regime
      operationId: getQuanRegime
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      responses: { '200': { description: Regime probabilities, signals, allocation, freshness, and warnings } }
  /v1/risk:
    post:
      summary: Analyze verified portfolio risk
      operationId: getQuanPortfolioRisk
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/DecisionIntelligenceRequest' } } }
      responses: { '200': { description: VaR, CVaR, drawdown, volatility, beta, concentration, and audit metadata } }
  /v1/valuation:
    post:
      summary: Run deterministic DCF, WACC, or Monte Carlo valuation
      operationId: runQuanValuation
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tool, inputs]
              properties:
                tool: { type: string, enum: [dcf, wacc, monte_carlo] }
                inputs: { type: object, additionalProperties: true }
      responses: { '200': { description: Deterministic output or explicit unavailableInputs } }
  /v1/backtest:
    post:
      summary: Run the research-labeled verified strategy comparison
      operationId: runQuanBacktest
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      responses: { '200': { description: Actual metrics for 60/40, generic four-regime, Quan seven-regime, and SPY } }
  /v1/signal:
    post:
      summary: Get the provenance-aware composite signal for a security
      operationId: getQuanSignal
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/DecisionIntelligenceRequest' } } }
      responses: { '200': { description: Composite score, signal contributions, coverage, evidence, and audit metadata } }
  /v1/audit:
    post:
      summary: Retrieve an owner-scoped immutable decision audit
      operationId: getQuanDecisionAudit
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [decisionId]
              properties: { decisionId: { type: string, pattern: '^qdi_' } }
      responses:
        '200': { description: Decision audit owned by the authenticated developer or Enterprise organization }
        '404': { description: Audit not found for this owner }
  /v1/documents:
    post:
      summary: Create a private PDF upload
      operationId: createQuanDocument
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/EnterpriseUserId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentCreateRequest'
      responses:
        '201':
          description: Private resumable upload created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DocumentUpload' }
  /v1/documents/{documentId}/complete:
    post:
      summary: Finalize an uploaded PDF and queue validation/OCR
      operationId: completeQuanDocument
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/DocumentId'
        - $ref: '#/components/parameters/EnterpriseUserId'
      responses:
        '202':
          description: Validation queued
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DocumentStatus' }
  /v1/documents/{documentId}:
    get:
      summary: Get PDF validation and extraction status
      operationId: getQuanDocument
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/DocumentId'
        - $ref: '#/components/parameters/EnterpriseUserId'
      responses:
        '200':
          description: Current document status
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DocumentStatus' }
    delete:
      summary: Delete a PDF and its extracted content
      operationId: deleteQuanDocument
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/DocumentId'
        - $ref: '#/components/parameters/EnterpriseUserId'
      responses:
        '200': { description: Document deleted }
  /v1/document-jobs/{jobId}:
    get:
      summary: Get an asynchronous PDF generation job
      operationId: getQuanDocumentJob
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/JobId'
        - $ref: '#/components/parameters/EnterpriseUserId'
      responses:
        '200':
          description: Current job status and artifact when ready
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DocumentJob' }
    delete:
      summary: Cancel a queued or running PDF job
      operationId: cancelQuanDocumentJob
      security: [{ ApiKeyAuth: [] }, { BearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/JobId'
        - $ref: '#/components/parameters/EnterpriseUserId'
      responses:
        '200': { description: Job canceled }
components:
  parameters:
    DocumentId:
      name: documentId
      in: path
      required: true
      schema: { type: string }
    JobId:
      name: jobId
      in: path
      required: true
      schema: { type: string }
    EnterpriseUserId:
      name: X-Enterprise-User-Id
      in: header
      required: false
      schema: { type: string }
      description: Required for Enterprise API keys.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 200 }
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Custom API key authentication (e.g. sk_quan_...)
    BearerAuth:
      type: http
      scheme: bearer
      description: Standard Bearer Authorization token
  schemas:
    QueryRequest:
      type: object
      required:
        - messages
      properties:
        model:
          type: string
          enum:
            - quan-3.0
            - quan-3.3
            - quan-3.4-l
            - quan-3.4
            - quan-3.4-deep-research
            - quan-3-0-enterprise
            - quan-3-4-enterprise
            - quan-3-4-deep-research-enterprise
          default: quan-3.0
          description: Variation of the model to use. quan-3.4-l includes 100,000 account-level tokens for verified developers, then costs $0.75/M input and $4.50/M output tokens. Public aliases support developer API workflows. Enterprise aliases require Enterprise credentials plus X-Enterprise-User-Id.
        messages:
          type: array
          description: Conversational history messages array in OpenAI-compatible chat format.
          items:
            $ref: '#/components/schemas/Message'
        stream:
          type: boolean
          default: true
          description: Whether to stream chunk responses back using Server-Sent Events.
        googleSearch:
          type: boolean
          default: false
          description: Deprecated compatibility alias for webSearch. Free Quan 3.4 L usage includes up to two searches per grounded request. Additional executed searches use prepaid balance at $0.015 per search. Quan 3.0 free usage remains ungrounded.
        webSearch:
          type: boolean
          default: false
          description: Enable Tavily-powered live web grounding. Free Quan 3.4 L usage includes up to two searches per grounded request. Additional executed searches use prepaid balance at $0.015 per search. Quan 3.0 free usage remains ungrounded.
        includeInsightMetadata:
          type: boolean
          default: false
          description: Opt into compact Light Market Insight metadata for easy ticker questions. Text remains the default response; when available, stream:false responses include quanLightMarketInsight and stream:true emits a quan_light_market_insight SSE event.
        researchFallback:
          type: string
          enum: [auto, off]
          default: auto
          description: Bounded fetched-source fallback for authenticated standard Quan/API requests; Lite ignores or caps it.
        maxOutputTokens:
          type: integer
          minimum: 1
          maximum: 65536
          default: 65536
          description: Maximum requested output. If omitted, the API key defaultMaxOutputTokens is used. Requests may not exceed the key maxOutputTokensCap or Gemini's 65,536-token provider limit. Requests that cannot fund the requested budget are rejected before generation; output is never silently clamped. Thinking tokens are included in provider usage.
        temperature:
          type: number
          minimum: 0
          maximum: 2
          default: 0.2
          description: Sampling temperature. Lower values are more deterministic.
        portfolio:
          type: object
          nullable: true
          description: Optional structured portfolio context for Quan 3.4 decision and risk intents. Unknown cash should be null, not zero.
          properties:
            cash: { type: number, nullable: true, minimum: 0 }
            totalValue: { type: number, nullable: true, minimum: 0, description: Declared total portfolio value; does not imply the residual balance is cash. }
            holdings:
              type: array
              maxItems: 100
              items:
                type: object
                required: [ticker]
                properties:
                  ticker: { type: string }
                  shares: { type: number, nullable: true }
                  marketValue: { type: number, nullable: true }
                  averageCost: { type: number, nullable: true }
                  sector: { type: string }
        horizon: { type: string, enum: [days, weeks, months, years] }
        objective: { type: string, enum: [capital_preservation, balanced, growth] }
        constraints: { type: object, additionalProperties: true }
        calculationInputs:
          type: object
          additionalProperties: true
          description: User-supplied or separately verified inputs for deterministic valuation tools.
        document_ids:
          type: array
          maxItems: 5
          items: { type: string }
          description: READY or READY_WITH_WARNINGS private PDF document IDs, limited to 500 pages combined.
        response_format:
          $ref: '#/components/schemas/DocumentResponseFormat'
    DocumentResponseFormat:
      type: object
      properties:
        type: { type: string, enum: [text, pdf], default: text }
        template: { type: string, enum: [document_summary, research_report, investment_plan], default: research_report }
        title: { type: string, maxLength: 180 }
    DocumentCreateRequest:
      type: object
      required: [filename, size]
      properties:
        filename: { type: string, example: apple-10-k.pdf }
        size: { type: integer, minimum: 1, maximum: 52428800 }
        content_type: { type: string, enum: [application/pdf], default: application/pdf }
    DocumentUpload:
      type: object
      properties:
        document_id: { type: string }
        upload_url: { type: string, format: uri }
        upload_method: { type: string, enum: [PUT] }
        upload_headers: { type: object, additionalProperties: { type: string } }
        resumable: { type: boolean, enum: [true] }
        expires_at: { type: string, format: date-time }
    DocumentStatus:
      type: object
      properties:
        document_id: { type: string }
        filename: { type: string }
        status: { type: string, enum: [UPLOADING, VALIDATING, PROCESSING, READY, READY_WITH_WARNINGS, REJECTED, EXPIRED, DELETED] }
        progress: { type: integer, minimum: 0, maximum: 100 }
        page_count: { type: integer, nullable: true }
        unreadable_pages: { type: array, items: { type: integer } }
        warnings: { type: array, items: { type: string } }
        error: { type: object, nullable: true }
        expires_at: { type: string, format: date-time }
    DocumentJobAccepted:
      type: object
      properties:
        job_id: { type: string }
        status: { type: string, enum: [QUEUED] }
        status_url: { type: string }
        expires_at: { type: string, format: date-time }
    DocumentJob:
      type: object
      properties:
        job_id: { type: string }
        status: { type: string, enum: [QUEUED, RUNNING, SUCCEEDED, FAILED, CANCELED, EXPIRED] }
        progress: { type: integer, minimum: 0, maximum: 100 }
        error: { type: object, nullable: true }
        artifact:
          type: object
          nullable: true
          properties:
            filename: { type: string }
            content_type: { type: string, enum: [application/pdf] }
            page_count: { type: integer }
            download_url: { type: string, format: uri }
            expires_at: { type: string, format: date-time }
    DecisionIntelligenceRequest:
      type: object
      properties:
        question: { type: string, maxLength: 4000 }
        ticker: { type: string, maxLength: 12 }
        horizon: { type: string, enum: [days, weeks, months, years] }
        objective: { type: string, enum: [capital_preservation, balanced, growth] }
        portfolio:
          type: object
          properties:
            cash: { type: number, nullable: true, minimum: 0 }
            totalValue: { type: number, nullable: true, minimum: 0, description: Declared total portfolio value; does not imply the residual balance is cash. }
            holdings:
              type: array
              maxItems: 100
              items:
                type: object
                required: [ticker]
                properties:
                  ticker: { type: string }
                  shares: { type: number }
                  marketValue: { type: number }
                  averageCost: { type: number }
        constraints:
          type: object
          properties:
            maxPositionPct: { type: number }
            maxSectorPct: { type: number }
            cashFloorPct: { type: number }
            maxTurnoverPct: { type: number }
            maxVaRPct: { type: number }
        researchFallback:
          type: string
          enum: [auto, off]
          default: auto
          description: Enable bounded fetched-source research when deterministic evidence is incomplete.
    DecisionIntelligenceResult:
      type: object
      required: [version, decisionId, asOf, status, decision, regime, evidence, audit]
      properties:
        version: { type: string, example: '1.0' }
        decisionId: { type: string }
        quantitativeCoreHash: { type: string, pattern: '^[a-f0-9]{64}$', description: Stable hash of the authoritative quantitative core with volatile IDs and timestamps removed; use it to verify cross-surface parity. }
        asOf: { type: string, format: date-time }
        status: { type: string, enum: [complete, degraded, insufficient_data] }
        decision: { type: object, additionalProperties: true }
        regime: { type: object, additionalProperties: true }
        securitySignals: { type: array, items: { type: object, additionalProperties: true } }
        portfolioRisk: { type: object, additionalProperties: true }
        portfolioActions: { type: array, items: { type: object, additionalProperties: true } }
        unchangedPositions: { type: array, items: { type: string } }
        scenarios: { type: array, items: { type: object, additionalProperties: true } }
        expectedScenarioOutcome: { type: object, nullable: true, additionalProperties: true }
        dominantDrivers: { type: array, items: { type: object, additionalProperties: true } }
        reasoningInputs: { type: object, additionalProperties: true }
        counterThesisInputs: { type: array, items: { type: object, additionalProperties: true } }
        coverageBreakdown:
          type: object
          properties:
            macro: { type: number, nullable: true }
            signal: { type: number, nullable: true }
            portfolioRisk: { type: number, nullable: true }
            primarySources: { type: number, nullable: true }
            overall: { type: number }
        primarySourceCoverage: { type: number }
        primarySourceCoverageDetail:
          type: object
          properties:
            macro: { type: number, nullable: true }
            companyFinancials: { type: number, nullable: true }
            filings: { type: number, nullable: true }
            marketData: { type: number, nullable: true }
            news: { type: number, nullable: true }
            overall: { type: number }
        claimLedger:
          type: object
          required: [version, decisionId, intent, claims, claimHash]
          properties:
            version: { type: string }
            decisionId: { type: string }
            intent: { type: string }
            claimHash: { type: string }
            claims:
              type: array
              items:
                type: object
                required: [id, decisionId, category, statement, status]
                additionalProperties: true
        calculations: { type: array, items: { type: object, additionalProperties: true } }
        evidence: { type: array, items: { type: object, additionalProperties: true } }
        audit: { type: object, additionalProperties: true }
        releaseFingerprint: { type: object, additionalProperties: true }
        research:
          type: object
          properties:
            mode: { type: string, enum: [none, cached, live, degraded] }
            queryCount: { type: integer, minimum: 0 }
            verifiedSourceCount: { type: integer, minimum: 0 }
            primarySourceCoverage: { type: number, minimum: 0, maximum: 1 }
            secondarySourceCoverage: { type: number, minimum: 0, maximum: 1 }
            fetchedUrls: { type: array, items: { type: string, format: uri } }
            missingEvidence: { type: array, items: { type: string } }
            conflicts: { type: array, items: { type: string } }
    Message:
      type: object
      required:
        - role
        - content
      properties:
        role:
          type: string
          enum:
            - user
            - assistant
          description: The speaker role.
        content:
          type: string
          description: The textual body content of the message.
    QueryResponse:
      type: object
      properties:
        candidates:
          type: array
          items:
            type: object
            properties:
              content:
                type: object
                properties:
                  parts:
                    type: array
                    items:
                      type: object
                      properties:
                        text:
                          type: string
                          description: The generated markdown text response.
                  role:
                    type: string
                    example: model
        quanLightMarketInsight:
          $ref: '#/components/schemas/LightMarketInsight'
    LightMarketInsight:
      type: object
      description: Optional grounded metadata for simple market questions when includeInsightMetadata is true.
      properties:
        intent:
          type: string
          enum: [performance_snapshot, why_moving, market_vibe, risk_reassurance, simple_buy_sell_read, compare_quick]
        ticker: { type: string, example: AAPL }
        asOf: { type: string, format: date-time }
        summary: { type: string }
        signals:
          type: array
          items:
            type: object
            properties:
              type: { type: string, example: session_move }
              label: { type: string, example: "Today's move" }
              value: { type: string, example: "+1.20%" }
              note: { type: string }
              sourceName: { type: string, example: Yahoo Finance quote }
              sourceUrl: { type: string, format: uri }
        suggestedFollowups:
          type: array
          items: { type: string }
