openapi: 3.1.0
info:
  title: Risos Community Intelligence API
  version: 1.3.0
  description: >-
    Evidence-first, tenant-scoped community intelligence. REST and MCP share the same
    source-scope, policy, Treasury, Research Memory, and evidence capabilities.
servers:
  - url: https://risos.co/api/v1
    description: Production
  - url: https://risos-staging.vercel.app/api/v1
    description: Staging
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: risos_sk
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
      example: { error: "API_SCOPE_REQUIRED:evidence:read" }
    SourceSlug:
      type: string
      enum: [x, bluesky, wikimedia, stack_exchange, threads, youtube, tiktok]
      description: >-
        X, Bluesky, Wikimedia, and Stack Exchange are launch-active. Threads, YouTube,
        and TikTok remain explicit policy/approval gates until activated; a requested
        source is never silently substituted.
    SourceScope:
      oneOf:
        - type: object
          required: [mode]
          properties:
            mode: { const: auto }
        - type: object
          required: [mode, sources]
          properties:
            mode: { const: only }
            sources:
              type: array
              minItems: 1
              maxItems: 1
              items: { $ref: '#/components/schemas/SourceSlug' }
        - type: object
          required: [mode, sources]
          properties:
            mode: { const: selected }
            sources:
              type: array
              minItems: 2
              items: { $ref: '#/components/schemas/SourceSlug' }
    ResearchRequest:
      type: object
      required: [question]
      properties:
        question: { type: string, minLength: 3, maxLength: 1000 }
        sources:
          type: array
          items: { $ref: '#/components/schemas/SourceSlug' }
        sourceScope: { $ref: '#/components/schemas/SourceScope' }
        depth: { type: string, enum: [standard, deep], default: standard }
        timeRange:
          type: object
          properties:
            from: { type: string }
            to: { type: string }
        acquisitionMode:
          type: string
          enum: [memory_only, memory_first, refresh_if_stale, acquire_if_insufficient, force_fresh]
          default: memory_first
        sourceBudgetUsd:
          type: number
          minimum: 0
          maximum: 0.1
    ResearchAccepted:
      type: object
      required: [runId, question, sources]
      properties:
        runId: { type: string, format: uuid }
        workflowRunId: { type: string }
        question: { type: string }
        sources:
          type: array
          items: { $ref: '#/components/schemas/SourceSlug' }
        sourceScope: { type: object, additionalProperties: true }
        execution: { type: string }
    Page:
      type: object
      additionalProperties: true
    IntelligenceQuery:
      type: object
      properties:
        families:
          type: array
          items:
            type: string
            enum: [voice_of_customer, intent, narrative, consensus, trend, audience, influence, event_risk, creative]
        query: { type: string, maxLength: 2000 }
        limit: { type: integer, minimum: 1 }
        status: { type: array, items: { type: string } }
        sources: { type: array, items: { $ref: '#/components/schemas/SourceSlug' } }
    MonitorCreate:
      type: object
      properties:
        name: { type: string, maxLength: 160 }
        subject: { type: string }
        targetKind: { type: string }
        targetRef: { type: string }
        sources: { type: array, items: { $ref: '#/components/schemas/SourceSlug' } }
        sourceScope: { $ref: '#/components/schemas/SourceScope' }
        refreshRules: { type: object, additionalProperties: true }
        notificationThreshold: { type: number, minimum: 0, maximum: 1 }
    SmartCollectionCreate:
      type: object
      required: [members]
      properties:
        name: { type: string, maxLength: 160 }
        description: { type: string, maxLength: 2000 }
        sources: { type: array, items: { $ref: '#/components/schemas/SourceSlug' } }
        sourceScope: { $ref: '#/components/schemas/SourceScope' }
        members:
          type: array
          minItems: 1
          items: { type: object, additionalProperties: true }
        refreshRules: { type: object, additionalProperties: true }
        notificationThreshold: { type: number, minimum: 0, maximum: 1 }
    FreshnessRequest:
      type: object
      required: [resourceType, resourceId]
      properties:
        resourceType: { const: opportunity }
        resourceId: { type: string, format: uuid }
    FreshnessEnvelope:
      type: object
      required: [schemaVersion, kind, data, rawSourcePayloadDelivered]
      properties:
        schemaVersion: { const: risos-public-v1 }
        kind: { const: freshness }
        data:
          type: object
          required: [verdict, asOf, evidenceCount, paidAcquisitionTriggered, explanation]
          properties:
            verdict: { type: string, enum: [current, stale, contradicted, unsupported, unknown] }
            asOf: { type: string, format: date-time }
            newestEvidenceAt:
              oneOf:
                - { type: string, format: date-time }
                - { type: 'null' }
            evidenceCount: { type: integer, minimum: 0 }
            paidAcquisitionTriggered: { const: false }
            explanation: { type: string }
        rawSourcePayloadDelivered: { const: false }
  responses:
    InvalidKey:
      description: Missing, invalid, or revoked API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ScopeRequired:
      description: The API key is valid but lacks the operation's required scope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Admission or rate limit reached; Retry-After may be returned.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ServerError:
      description: Risos or a required provider could not complete the operation.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
security:
  - bearerApiKey: []
paths:
  /health:
    get:
      operationId: health
      security: []
      summary: Check the public API process
      responses:
        '200': { description: Healthy }
  /research:
    post:
      operationId: researchTopic
      summary: Start source-scoped durable research
      description: "Requires research:write. Equivalent MCP tool: research_topic. Source budget is capped at $0.10 per request by the public route."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ResearchRequest' }
      responses:
        '202':
          description: Research accepted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ResearchAccepted' }
        '400': { description: Invalid research input }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /evidence:
    get:
      operationId: retrieveEvidence
      summary: Retrieve tenant-scoped evidence
      description: "Requires evidence:read. Equivalent MCP tool: retrieve_evidence."
      parameters:
        - { in: query, name: q, schema: { type: string } }
        - { in: query, name: research, schema: { type: string, format: uuid } }
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
        - { in: query, name: page, schema: { type: integer, minimum: 1 } }
        - { in: query, name: pageSize, schema: { type: integer, minimum: 1, maximum: 100 } }
      responses:
        '200': { description: Evidence page }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /freshness:
    post:
      operationId: assessFreshness
      summary: Verify an opportunity at action time
      description: >-
        Requires evidence:read. Equivalent MCP tool: assess_freshness. Uses linked
        workspace evidence and never silently triggers paid acquisition.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FreshnessRequest' }
      responses:
        '200':
          description: Versioned public freshness verdict
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FreshnessEnvelope' }
        '400': { description: Unsupported resource type or invalid resource identifier }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
        '404': { description: Opportunity not found in this tenant }
  /memory:
    get:
      operationId: listResearchMemory
      summary: List Research Memory objects
      description: Requires research:read. MCP also exposes inspect_research_memory and show_changes.
      responses:
        '200': { description: Research Memory list }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /intelligence:
    get:
      operationId: queryIntelligence
      summary: Query the nine-family Intelligence Treasury
      description: "Requires evidence:read. Equivalent MCP tool: query_intelligence."
      parameters:
        - { in: query, name: family, schema: { type: string } }
        - { in: query, name: q, schema: { type: string, maxLength: 2000 } }
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
        - { in: query, name: limit, schema: { type: integer, minimum: 1 } }
      responses:
        '200': { description: Intelligence results }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /intelligence/query:
    post:
      operationId: queryIntelligenceStructured
      summary: Query Intelligence Treasury with a structured body
      description: "Requires evidence:read. Equivalent MCP tool: query_intelligence."
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IntelligenceQuery' }
      responses:
        '200': { description: Intelligence results }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /intelligence/{id}:
    get:
      operationId: getIntelligenceObject
      summary: Retrieve one Intelligence Treasury object
      description: Requires evidence:read.
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Intelligence object }
        '404': { description: Not found in this tenant/source scope }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /entities:
    get:
      operationId: listEntities
      summary: Query tenant-visible entities
      description: "Requires evidence:read. Equivalent MCP tool: inspect_entity for detail."
      parameters:
        - { in: query, name: q, schema: { type: string, maxLength: 200 } }
        - { in: query, name: type, schema: { type: string } }
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
        - { in: query, name: limit, schema: { type: integer, minimum: 1 } }
        - { in: query, name: offset, schema: { type: integer, minimum: 0 } }
      responses:
        '200': { description: Entity results }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /entities/{id}:
    get:
      operationId: getEntity
      summary: Retrieve one tenant-visible entity
      description: "Requires evidence:read. Equivalent MCP tool: inspect_entity."
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
      responses:
        '200': { description: Entity detail }
        '404': { description: Entity not found or not visible to this organization }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /trends:
    get:
      operationId: getTrends
      summary: Retrieve trend-family intelligence
      description: "Requires evidence:read. Equivalent MCP tool: get_trends."
      parameters:
        - { in: query, name: q, schema: { type: string, maxLength: 2000 } }
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
        - { in: query, name: limit, schema: { type: integer, minimum: 1 } }
      responses:
        '200': { description: Trend results }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /signals:
    get:
      operationId: getSignals
      summary: Retrieve composed evidence-backed signals
      description: "Requires evidence:read. Equivalent MCP tool: get_signals."
      parameters:
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
      responses:
        '200': { description: Signal results }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /recommendations:
    post:
      operationId: recommendActions
      summary: Compose actions from current tenant intelligence
      description: "Requires opportunity:read. Equivalent MCP tool: recommend_actions."
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                sources: { type: array, items: { $ref: '#/components/schemas/SourceSlug' } }
      responses:
        '200': { description: Recommendation results }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /competitive/compare:
    post:
      operationId: compareCompetitors
      summary: Compare at least two competitors with evidence-backed intelligence
      description: Requires evidence:read. Equivalent MCP tools include compare_competitors and compare_competitors_evidence.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [competitors]
              properties:
                competitors: { type: array, minItems: 2, maxItems: 20, items: { type: string } }
                sources: { type: array, items: { $ref: '#/components/schemas/SourceSlug' } }
      responses:
        '200': { description: Competitive movement comparison }
        '400': { description: At least two competitors are required }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /creative-opportunities:
    get:
      operationId: creativeOpportunities
      summary: Retrieve the evidence-backed Creative Opportunity Matrix
      description: "Requires opportunity:read. Equivalent MCP tool: creative_opportunities."
      parameters:
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
      responses:
        '200': { description: Creative opportunities }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /creative-brief/{id}:
    get:
      operationId: creativeBrief
      summary: Retrieve an evidence-backed What-to-Make brief
      description: Requires opportunity:read.
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
      responses:
        '200': { description: Creative brief }
        '404': { description: Creative opportunity not found }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /opportunities:
    get:
      operationId: findOpportunities
      summary: List evidence-backed opportunities
      description: "Requires opportunity:read. Equivalent MCP tools: find_opportunities and find_saved_opportunities."
      parameters:
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
      responses:
        '200': { description: Opportunity results }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /monitors:
    get:
      operationId: listMonitors
      summary: List durable monitors
      description: Requires monitor:read.
      parameters:
        - { in: query, name: source, schema: { $ref: '#/components/schemas/SourceSlug' } }
      responses:
        '200': { description: Monitor list }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
    post:
      operationId: createMonitor
      summary: Create and start a durable monitor
      description: Requires monitor:write.
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MonitorCreate' }
      responses:
        '201': { description: Monitor created }
        '400': { description: Invalid monitor input }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /monitors/{id}:
    get:
      operationId: inspectMonitor
      summary: Retrieve one monitor with lineage
      description: "Requires monitor:read. Equivalent MCP tool: inspect_monitor."
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Monitor detail }
        '404': { description: Monitor not found in this tenant }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /monitors/collections:
    get:
      operationId: listSmartCollections
      summary: List Smart Collections
      description: Requires monitor:read.
      responses:
        '200': { description: Smart Collection list }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
    post:
      operationId: createSmartCollection
      summary: Create a Smart Collection
      description: Requires monitor:write.
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SmartCollectionCreate' }
      responses:
        '201': { description: Smart Collection created }
        '400': { description: At least one valid member is required }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /monitors/collections/{id}:
    get:
      operationId: getSmartCollection
      summary: Retrieve one Smart Collection
      description: Requires monitor:read.
      parameters:
        - { in: path, name: id, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: Smart Collection detail }
        '404': { description: Collection not found in this tenant }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
  /x/canary:
    post:
      operationId: xCanary
      summary: Run a minimal live X counts canary
      description: >-
        Requires research:write and consumes X API billable units. Intended for explicit
        operational verification only; it is not a health endpoint and should not be polled.
      responses:
        '200': { description: X canary succeeded }
        '401': { $ref: '#/components/responses/InvalidKey' }
        '403': { $ref: '#/components/responses/ScopeRequired' }
        '503': { description: X live credential is not configured }
