openapi: 3.1.0
info:
  title: APEX Pulse API
  version: 0.1.0
  summary: Intelligence and simulation engine of the APEX Suite.
  description: |
    Pulse is the APEX Suite's intelligence engine. It runs multi-agent shadow
    simulations of public discourse and deliberative assemblies, designs
    simulation blueprints from prompts and documents, collects and summarises
    monitoring signals, computes trends and anomalies, executes structured
    data sweeps, and collects normalised procurement opportunities from
    configured portal registries.

    **Pulse is in Preview: authentication and entitlement enforcement are still
    hardening. Call Pulse only from trusted server-side environments and expect
    surface changes.**

    ## Authentication

    Send a platform Bearer credential on every request:
    `Authorization: Bearer <credential>`. Requests without the header receive
    `401 {"error": "Missing or invalid Authorization header"}`.

    ## Tenant carriage

    Every operation takes a required `tenant_id` query parameter. Resources
    owned by another tenant return `404`.

    ## Rate limits and payload cap

    Requests are limited to 120 per minute per tenant; exceeding the limit
    returns `429` with a `Retry-After` header. Request bodies are capped at
    50 megabytes; larger bodies return `413`.

    ## Streaming

    Endpoints marked as streaming return Server-Sent Events with content type
    `text/event-stream`. Keep the connection open and consume events as they
    arrive.
servers:
  - url: https://pulse.dev.apex.reisiger.org
    description: Development
  - url: http://localhost:9141
    description: Local
security:
  - bearerAuth: []
tags:
  - name: Simulations
    description: >-
      Shadow-analysis simulations — create, monitor, stream, inject catalysts,
      and retrieve results, reports, and summaries.
  - name: Blueprints
    description: >-
      Simulation blueprint design — turn a prompt and optional documents into a
      structured simulation blueprint before running it.
  - name: Signals
    description: Monitoring signals collected for the tenant, with summaries and live streaming.
  - name: Trends
    description: Trend series and anomaly detection over collected signals.
  - name: Sweeps
    description: >-
      Structured data sweeps — analyse a target site, run a configured sweep,
      inspect status, and query collected results.
  - name: Opportunities
    description: >-
      Opportunity scout — normalised procurement opportunities collected from
      the tenant's configured portal registry. Records carry deterministic
      stable identifiers (re-collection never duplicates), amendments bump a
      revision and surface through the `since` cursor, and every collection
      sweep leaves per-portal run records. Response bodies use the APEX
      envelope (`ok` / `data` / `meta`); opportunity fields are camelCase.
      This surface is Preview with additive-only changes.
paths:
  # ---------------------------------------------------------------- Simulations
  /v1/simulations:
    post:
      operationId: createSimulation
      tags: [Simulations]
      summary: Create a simulation
      description: >-
        Creates a simulation from either a natural-language `prompt` (Pulse
        designs the blueprint) or a previously designed `blueprint`. Returns
        the new simulation's identifier and initial status; progress is
        available via the status, stream, and rounds endpoints.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSimulationRequest'
            example:
              prompt: >-
                Simulate public reaction to a proposed municipal water tariff
                increase of 15 percent, including organised community responses.
              config:
                max_rounds: 40
                hours_per_round: 12
                agent_count: 45
                interaction_type: public_discourse
                platforms: [info_plaza, topic_community]
      responses:
        '200':
          description: Simulation created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
    get:
      operationId: listSimulations
      tags: [Simulations]
      summary: List simulations
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Simulations belonging to the tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  simulations:
                    type: array
                    items:
                      $ref: '#/components/schemas/SimulationStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/simulations/{simulation_id}:
    get:
      operationId: getSimulation
      tags: [Simulations]
      summary: Get simulation status
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Current simulation status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
    delete:
      operationId: deleteSimulation
      tags: [Simulations]
      summary: Delete a simulation
      description: Deletes a simulation and its stored artifacts.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Simulation deleted.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/stream:
    get:
      operationId: streamSimulation
      tags: [Simulations]
      summary: Stream simulation events (Server-Sent Events)
      description: >-
        Streams live simulation events — round progress, agent activity, and
        status changes — as Server-Sent Events. Keep the connection open for
        the duration of the run.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Event stream.
          content:
            text/event-stream:
              schema:
                type: string
                description: Server-Sent Events stream of simulation updates.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/inject:
    post:
      operationId: injectSimulationEvent
      tags: [Simulations]
      summary: Inject a mid-simulation catalyst
      description: >-
        Injects a catalyst event into a running simulation — for example a news
        break or a leaked document — and lets the agent population react. The
        simulation must be running; otherwise the request returns `409`.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InjectRequest'
            example:
              event: A leaked engineering memo reveals the tariff model excludes maintenance backlogs.
              mechanism: leaked_document
              hashtag: '#TariffLeaks'
      responses:
        '200':
          description: Catalyst injected.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
        '409':
          description: Simulation is not currently running.
  /v1/simulations/{simulation_id}/rounds:
    get:
      operationId: getSimulationRounds
      tags: [Simulations]
      summary: Get round data
      description: Returns per-round simulation data for analysis and replay.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Round data.
          content:
            application/json:
              schema:
                type: object
                description: Per-round records for the simulation.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/assembly:
    get:
      operationId: getSimulationAssembly
      tags: [Simulations]
      summary: Get assembly state
      description: >-
        Returns the deliberative-assembly state for simulations that use the
        `parallel_assembly` interaction type — motions, delegates, and voting
        outcomes.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Assembly state.
          content:
            application/json:
              schema:
                type: object
                description: Assembly engine state.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/personas:
    get:
      operationId: getSimulationPersonas
      tags: [Simulations]
      summary: Get agent personas
      description: Returns the generated agent personas participating in the simulation.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Agent personas.
          content:
            application/json:
              schema:
                type: object
                description: Persona list with demographic and stance attributes.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/graph:
    get:
      operationId: getSimulationGraph
      tags: [Simulations]
      summary: Get the interaction network graph
      description: Returns the agent interaction network for visualisation.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Network graph.
          content:
            application/json:
              schema:
                type: object
                description: Nodes-and-edges interaction network.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/results:
    get:
      operationId: getSimulationResults
      tags: [Simulations]
      summary: Get simulation results
      description: Returns the structured outcome data for a finished simulation.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Structured results.
          content:
            application/json:
              schema:
                type: object
                description: Simulation outcome data.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/report:
    get:
      operationId: getSimulationReport
      tags: [Simulations]
      summary: Get the narrative report
      description: Returns the generated narrative analysis report for a finished simulation.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Narrative report.
          content:
            application/json:
              schema:
                type: object
                description: Narrative report document.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/summary:
    get:
      operationId: getSimulationSummary
      tags: [Simulations]
      summary: Get the simulation summary
      description: Returns a condensed summary of a finished simulation.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      responses:
        '200':
          description: Summary.
          content:
            application/json:
              schema:
                type: object
                description: Condensed simulation summary.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  /v1/simulations/{simulation_id}/chat:
    post:
      operationId: chatAboutSimulation
      tags: [Simulations]
      summary: Ask questions about a finished simulation
      description: >-
        Conversational analysis over a finished simulation. Send a message and
        the prior conversation history; Pulse answers grounded in the
        simulation's data and report.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - $ref: '#/components/parameters/SimulationIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChatRequest'
            example:
              message: Which stakeholder group shifted stance most after the leak, and why?
              history:
                - role: user
                  content: Summarise the overall sentiment trajectory.
                - role: assistant
                  content: Sentiment opened neutral, dipped sharply after round 12, and partially recovered.
      responses:
        '200':
          description: Assistant reply grounded in the simulation.
          content:
            application/json:
              schema:
                type: object
                description: Reply message and supporting references.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/SimulationNotFound'
  # ---------------------------------------------------------------- Blueprints
  /v1/blueprints:
    get:
      operationId: listBlueprints
      tags: [Blueprints]
      summary: List blueprints
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Blueprints belonging to the tenant.
          content:
            application/json:
              schema:
                type: object
                description: Blueprint list.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/blueprints/design:
    post:
      operationId: designBlueprint
      tags: [Blueprints]
      summary: Design a simulation blueprint
      description: >-
        Designs a full simulation blueprint from a prompt, optionally grounded
        in uploaded documents and stakeholder guidance. The returned blueprint
        can be passed to `POST /v1/simulations`.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DesignBlueprintRequest'
            example:
              prompt: >-
                Design a deliberative assembly on a proposed municipal water
                tariff increase, with community, business, and municipal
                stakeholder groups.
              interaction_type: parallel_assembly
              documents:
                - filename: tariff-proposal.pdf
                  content_base64: JVBERi0xLjQKJcOkw7zDtsOfCg==
                  content_type: application/pdf
              stakeholder_guidance: Include organised agriculture as a distinct stakeholder group.
              research_mode: opus
      responses:
        '200':
          description: Designed blueprint.
          content:
            application/json:
              schema:
                type: object
                description: Blueprint document ready to run.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /v1/blueprints/design/stream:
    post:
      operationId: designBlueprintStream
      tags: [Blueprints]
      summary: Design a blueprint with streaming progress (Server-Sent Events)
      description: >-
        Same as `POST /v1/blueprints/design`, but streams design progress and
        the final blueprint as Server-Sent Events.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DesignBlueprintRequest'
            example:
              prompt: Design a public-discourse simulation of a fuel levy announcement.
              interaction_type: public_discourse
              research_mode: none
      responses:
        '200':
          description: Event stream of design progress ending with the blueprint.
          content:
            text/event-stream:
              schema:
                type: string
                description: Server-Sent Events stream.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  # ---------------------------------------------------------------- Signals
  /v1/signals:
    get:
      operationId: listSignals
      tags: [Signals]
      summary: List collected signals
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - name: source_type
          in: query
          description: Filter by signal source type.
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum signals to return.
          schema:
            type: integer
            maximum: 1000
      responses:
        '200':
          description: Signals for the tenant.
          content:
            application/json:
              schema:
                type: object
                description: Signal list.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/signals/summary:
    get:
      operationId: getSignalsSummary
      tags: [Signals]
      summary: Get a signals summary
      description: Returns aggregate counts and highlights across collected signals.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Aggregate summary.
          content:
            application/json:
              schema:
                type: object
                description: Signal summary.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/signals/stream:
    get:
      operationId: streamSignals
      tags: [Signals]
      summary: Stream signals (Server-Sent Events)
      description: Streams new signals for the tenant as Server-Sent Events.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Event stream of signals.
          content:
            text/event-stream:
              schema:
                type: string
                description: Server-Sent Events stream.
        '401':
          $ref: '#/components/responses/Unauthorized'
  # ---------------------------------------------------------------- Trends & anomalies
  /v1/trends/entity:
    get:
      operationId: getEntityTrend
      tags: [Trends]
      summary: Get a trend series for an entity
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - name: entity
          in: query
          required: true
          description: Entity to compute the trend for.
          schema:
            type: string
        - name: bucket_hours
          in: query
          description: Bucket width in hours.
          schema:
            type: integer
            minimum: 1
            maximum: 168
        - name: lookback_hours
          in: query
          description: How far back to compute, in hours.
          schema:
            type: integer
            maximum: 720
      responses:
        '200':
          description: Bucketed trend series.
          content:
            application/json:
              schema:
                type: object
                description: Trend series.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/trends/volume:
    get:
      operationId: getVolumeTrend
      tags: [Trends]
      summary: Get signal volume trends
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - name: group_by
          in: query
          description: Grouping key for volume buckets.
          schema:
            type: string
      responses:
        '200':
          description: Volume series.
          content:
            application/json:
              schema:
                type: object
                description: Volume trend series.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/trends/topics:
    get:
      operationId: getTopicTrends
      tags: [Trends]
      summary: Get trending topics
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Trending topics.
          content:
            application/json:
              schema:
                type: object
                description: Topic trend list.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/anomalies:
    get:
      operationId: listAnomalies
      tags: [Trends]
      summary: List detected anomalies
      description: Returns anomalies detected against per-entity baselines.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Detected anomalies.
          content:
            application/json:
              schema:
                type: object
                description: Anomaly list.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/anomalies/baselines:
    get:
      operationId: getAnomalyBaselines
      tags: [Trends]
      summary: Get anomaly baselines
      description: Returns the statistical baselines anomaly detection compares against.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Baselines per entity.
          content:
            application/json:
              schema:
                type: object
                description: Baseline statistics.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  # ---------------------------------------------------------------- Sweeps
  /v1/sweeps/investigate:
    post:
      operationId: investigateSweepTarget
      tags: [Sweeps]
      summary: Analyse a target address for sweeping
      description: >-
        Analyses a web address — detects tables and pagination — and returns a
        draft sweep configuration in YAML. A failure to reach or parse the
        target returns `502`.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SweepInvestigateRequest'
            example:
              url: https://example.org/public-register
              stealth_level: headers
      responses:
        '200':
          description: Draft sweep configuration and detection details.
          content:
            application/json:
              schema:
                type: object
                description: Analysis result with draft configuration.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '502':
          description: Target could not be reached or analysed.
  /v1/sweeps/run:
    post:
      operationId: runSweep
      tags: [Sweeps]
      summary: Run a sweep
      description: >-
        Executes a sweep from a stored configuration name or inline YAML.
        Provide exactly one of `config_name` or `config_yaml`; providing
        neither returns `400`, and an unknown configuration name returns `404`.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SweepRunRequest'
            example:
              config_name: public-register
              start_page: 1
      responses:
        '200':
          description: Sweep run result.
          content:
            application/json:
              schema:
                type: object
                description: Run statistics and output location.
                x-apex-note: Schema partially documented — verify against the service.
        '400':
          description: Neither `config_name` nor `config_yaml` was provided.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Named configuration not found.
  /v1/sweeps/status:
    get:
      operationId: getSweepStatus
      tags: [Sweeps]
      summary: Get sweep status
      description: Returns status and statistics for one sweep or all sweeps.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - name: name
          in: query
          description: Sweep configuration name. Omit for all sweeps.
          schema:
            type: string
      responses:
        '200':
          description: Sweep statistics.
          content:
            application/json:
              schema:
                type: object
                description: Status and statistics.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Named configuration not found.
  /v1/sweeps/query:
    post:
      operationId: querySweepResults
      tags: [Sweeps]
      summary: Query sweep results with read-only SQL
      description: >-
        Runs a read-only SQL `SELECT` statement over a sweep's collected
        results. Only `SELECT` statements are accepted.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SweepQueryRequest'
            example:
              name: public-register
              sql: SELECT registration_number, status FROM records WHERE status = 'lapsed'
              limit: 100
      responses:
        '200':
          description: Query rows.
          content:
            application/json:
              schema:
                type: object
                description: Result rows.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Sweep not found.
  /v1/sweeps/list:
    get:
      operationId: listSweepConfigs
      tags: [Sweeps]
      summary: List available sweep configurations
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Available sweep configurations.
          content:
            application/json:
              schema:
                type: object
                description: Configuration list.
                x-apex-note: Schema partially documented — verify against the service.
        '401':
          $ref: '#/components/responses/Unauthorized'
  # ---------------------------------------------------------------- Opportunities
  /v1/opportunities:
    get:
      operationId: listOpportunities
      tags: [Opportunities]
      summary: List normalised opportunities
      description: >-
        Lists opportunities collected for the tenant, filterable by portal,
        category, country, and published/closing date windows. Results are
        ordered by amendment time and keyset-paginated: pass the returned
        `nextCursor` as `since` on the next call to receive everything new
        **or amended** since the previous poll. Identifiers are stable across
        re-collection — polling with `since` and upserting by `id` yields
        zero duplicates.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - name: portal
          in: query
          description: Filter by source portal name (registry entry name).
          schema:
            type: string
        - name: category
          in: query
          description: Filter by opportunity category.
          schema:
            $ref: '#/components/schemas/OpportunityCategory'
        - name: country
          in: query
          description: Filter by country code.
          schema:
            type: string
        - name: publishedFrom
          in: query
          description: Earliest published date (inclusive, ISO 8601).
          schema:
            type: string
            format: date-time
        - name: publishedTo
          in: query
          description: Latest published date (inclusive, ISO 8601).
          schema:
            type: string
            format: date-time
        - name: closingFrom
          in: query
          description: Earliest closing date (inclusive, ISO 8601).
          schema:
            type: string
            format: date-time
        - name: closingTo
          in: query
          description: Latest closing date (inclusive, ISO 8601).
          schema:
            type: string
            format: date-time
        - name: since
          in: query
          description: >-
            Opaque cursor from a previous response's `nextCursor`. Returns
            records created or amended after the cursor position.
          schema:
            type: string
        - name: limit
          in: query
          description: Maximum records to return.
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
      responses:
        '200':
          description: Opportunities for the tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Opportunity'
                      count:
                        type: integer
                      nextCursor:
                        type: [string, 'null']
                        description: >-
                          Cursor for the next page; pass as `since`. `null`
                          when no records were returned.
                  meta:
                    $ref: '#/components/schemas/ResponseMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Credential is not entitled to this tenant.
        '422':
          description: Unknown category, malformed date, or invalid cursor.
        '503':
          $ref: '#/components/responses/SubstrateUnavailable'
  /v1/opportunities/collect:
    post:
      operationId: collectOpportunities
      tags: [Opportunities]
      summary: Trigger an on-demand collection sweep
      description: >-
        Runs a collection sweep across the tenant's configured portal
        registry and blocks until it finishes. Each portal produces its own
        run record; one portal's failure never aborts the batch. Portals
        requiring credentialed access are skipped with an explicit status.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
      responses:
        '200':
          description: Sweep summary with per-portal outcomes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/CollectionSweepResult'
                  meta:
                    $ref: '#/components/schemas/ResponseMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Credential is not entitled to this tenant.
        '404':
          description: No configuration found for the tenant.
        '409':
          description: >-
            Opportunity collection is not enabled for the tenant, or a sweep
            is already in flight.
        '503':
          $ref: '#/components/responses/SubstrateUnavailable'
  /v1/opportunities/runs:
    get:
      operationId: listOpportunityRuns
      tags: [Opportunities]
      summary: List collection run status
      description: >-
        Returns recent collection runs — one per portal per sweep — with
        status, timing, dedup counts, and any errors. Use this to monitor
        collection health and detect silent yield drops.
      x-apex-availability: preview
      parameters:
        - $ref: '#/components/parameters/TenantIdQuery'
        - name: limit
          in: query
          description: Maximum runs to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Recent collection runs, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  data:
                    type: object
                    properties:
                      runs:
                        type: array
                        items:
                          $ref: '#/components/schemas/CollectionRun'
                  meta:
                    $ref: '#/components/schemas/ResponseMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Credential is not entitled to this tenant.
        '503':
          $ref: '#/components/responses/SubstrateUnavailable'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Platform Bearer credential. Pulse is in Preview — call it only from
        trusted server-side environments.
  parameters:
    TenantIdQuery:
      name: tenant_id
      in: query
      required: true
      description: Tenant identifier.
      schema:
        type: string
      example: your-tenant
    SimulationIdPath:
      name: simulation_id
      in: path
      required: true
      description: Simulation identifier.
      schema:
        type: string
  responses:
    Unauthorized:
      description: Authentication missing or invalid.
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
          example:
            error: Missing or invalid Authorization header
    RateLimited:
      description: Rate limit of 120 requests per minute exceeded.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
    SimulationNotFound:
      description: Simulation not found for this tenant.
    SubstrateUnavailable:
      description: Persistent storage is unavailable; retry later.
  schemas:
    ResponseMeta:
      type: object
      description: Metadata included in every enveloped response.
      properties:
        request_id:
          type: string
        timestamp:
          type: string
          format: date-time
        version:
          type: string
    OpportunityCategory:
      type: string
      enum: [rfp, eoi, tender, pre_qualification, rfi]
      description: >-
        Opportunity category. Requests for quotation are normalised to
        `tender`.
    OpportunitySourceRef:
      type: object
      description: The portal an opportunity was collected from.
      required: [name, url, type]
      properties:
        name:
          type: string
          description: Portal registry entry name, e.g. `etenders`.
        url:
          type: string
          description: Portal base address.
        type:
          type: string
          enum:
            - government_portal
            - development_bank
            - procurement_platform
            - industry_portal
            - news
            - ai_search
    Opportunity:
      type: object
      description: >-
        Normalised opportunity record. `id` is deterministic per source
        notice — re-collection returns the same identifier, and amendments
        update the record in place (surfacing via the `since` cursor) rather
        than creating a new one.
      required: [id, title, category, source, collectedAt, collectorId]
      properties:
        id:
          type: string
          description: Stable opportunity identifier, prefixed `opp_`.
          example: opp_1f2a9c04be7d43a1a6c0
        title:
          type: string
        description:
          type: string
        category:
          $ref: '#/components/schemas/OpportunityCategory'
        source:
          $ref: '#/components/schemas/OpportunitySourceRef'
        country:
          type: [string, 'null']
          example: ZA
        region:
          type: [string, 'null']
        client:
          type: [string, 'null']
          description: Procuring entity, e.g. a government department.
        publishedDate:
          type: [string, 'null']
          format: date-time
        closingDate:
          type: [string, 'null']
          format: date-time
        estimatedValue:
          type: [number, 'null']
        currency:
          type: [string, 'null']
          example: ZAR
        referenceNumber:
          type: [string, 'null']
          description: Source reference — OCDS identifier or portal reference number.
        documentUrl:
          type: [string, 'null']
          description: Address of the tender document, when published.
        contactInfo:
          type: [string, 'null']
        tags:
          type: array
          items:
            type: string
        collectedAt:
          type: string
          format: date-time
        collectorId:
          type: string
          description: Collector that produced the record, e.g. `pulse-etenders`.
        clusterId:
          type: [string, 'null']
          description: >-
            Groups records that likely describe the same real-world
            opportunity published on different portals (cross-portal
            reposts). Records sharing a `clusterId` are aliases of one
            canonical opportunity; `null` means no cross-portal match.
            A cluster change counts as an amendment and surfaces through
            the `since` cursor.
    PortalSweepOutcome:
      type: object
      description: Outcome of one portal within a collection sweep.
      required: [portal, runId, status]
      properties:
        portal:
          type: string
        runId:
          type: string
        status:
          type: string
          enum: [completed, failed, skipped]
        fetched:
          type: integer
          description: Raw notices fetched from the portal (completed runs).
        total:
          type: integer
          description: Notices mapped to opportunity records (completed runs).
        new:
          type: integer
        updated:
          type: integer
        unchanged:
          type: integer
        error:
          type: string
          description: Failure or skip reason (failed and skipped runs).
        yieldAlert:
          $ref: '#/components/schemas/YieldAlert'
    YieldAlert:
      type: object
      description: >-
        Silent yield-drop flag: the run completed, but fetched far fewer
        notices than the portal's trailing baseline (median of recent
        completed runs). Usually means a changed page layout or a quietly
        narrowed feed — investigate the portal rather than the run.
      properties:
        fetched:
          type: integer
          description: Notices fetched by this run.
        baseline:
          type: number
          description: Median fetched count of the portal's recent completed runs.
        ratio:
          type: number
          description: fetched / baseline.
        threshold:
          type: number
          description: Alert fires when ratio falls below this (tenant-configured).
        sampleSize:
          type: integer
          description: Number of prior runs in the baseline.
    CollectionSweepResult:
      type: object
      description: Summary of an on-demand collection sweep.
      properties:
        tenantId:
          type: string
        portals:
          type: array
          items:
            $ref: '#/components/schemas/PortalSweepOutcome'
        completed:
          type: integer
        failed:
          type: integer
        skipped:
          type: integer
        yieldAlerts:
          type: integer
          description: Portals in this sweep flagged with a yield alert.
    CollectionRun:
      type: object
      description: One portal collection run.
      properties:
        runId:
          type: string
        portal:
          type: string
        status:
          type: string
          enum: [running, completed, failed, skipped]
        startedAt:
          type: [string, 'null']
          format: date-time
        completedAt:
          type: [string, 'null']
          format: date-time
        durationSeconds:
          type: [number, 'null']
        counts:
          type: [object, 'null']
          description: Fetched/total/new/updated/unchanged counters.
          additionalProperties:
            type: integer
        yieldAlert:
          oneOf:
            - $ref: '#/components/schemas/YieldAlert'
            - type: 'null'
        errors:
          type: [array, 'null']
          items:
            type: object
            properties:
              error:
                type: string
    SimulationConfig:
      type: object
      description: Configuration for a simulation run.
      properties:
        max_rounds:
          type: integer
          default: 40
        hours_per_round:
          type: integer
          default: 12
          description: Simulated hours that pass per round.
        agent_count:
          type: integer
          default: 45
        interaction_type:
          type: string
          enum: [public_discourse, parallel_assembly]
          default: public_discourse
          description: >-
            `public_discourse` simulates open social platforms;
            `parallel_assembly` adds a deliberative assembly with cross-feed.
        platforms:
          type: array
          items:
            type: string
          default: [info_plaza, topic_community]
          description: Simulated platform spaces agents interact in.
        llm_model:
          type: string
          default: claude-sonnet-4-6
          description: Language model used to drive agents.
        embedding_model:
          type: string
          default: nomic-embed-text
        motions:
          type: array
          items:
            type: string
          description: Motions to table (assembly simulations only).
        voting_model:
          type: string
          default: qualified_majority
          description: Voting rule for assembly simulations.
        voting_threshold:
          type: number
          default: 0.67
          description: Pass threshold for assembly votes.
    CreateSimulationRequest:
      type: object
      description: >-
        Provide either a natural-language `prompt` (Pulse designs the
        blueprint) or a `blueprint` produced by the blueprint design endpoints.
      properties:
        prompt:
          type: [string, 'null']
          description: Scenario description to design a blueprint from.
        blueprint:
          type: [object, 'null']
          description: Previously designed simulation blueprint.
        config:
          $ref: '#/components/schemas/SimulationConfig'
    SimulationStatusResponse:
      type: object
      properties:
        simulation_id:
          type: string
        name:
          type: string
        status:
          type: string
          enum: [pending, designing, ready, running, reporting, complete, failed, cancelled]
        interaction_type:
          type: string
          enum: [public_discourse, parallel_assembly]
        current_round:
          type: integer
        total_rounds:
          type: integer
        agent_count:
          type: integer
        delegate_count:
          type: integer
          description: Assembly delegates (assembly simulations only).
        stakeholder_group_count:
          type: integer
        api_calls:
          type: integer
          description: Model calls made so far.
        cost_zar:
          type: number
          description: Accumulated cost in South African rand.
        started_at:
          type: [string, 'null']
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time
        created_at:
          type: [string, 'null']
          format: date-time
        error:
          type: [string, 'null']
    InjectRequest:
      type: object
      required: [event]
      properties:
        event:
          type: string
          description: Description of the catalyst event.
        mechanism:
          type: string
          enum: [news_break, leaked_document, scandal, policy_reversal, external_shock]
          description: How the catalyst enters the simulated environment.
        hashtag:
          type: string
          default: ''
          description: Optional hashtag attached to the event.
    ChatRequest:
      type: object
      required: [message]
      properties:
        message:
          type: string
        history:
          type: array
          description: Prior conversation turns as role/content pairs.
          items:
            type: object
            additionalProperties:
              type: string
    DocumentUpload:
      type: object
      required: [filename, content_base64]
      properties:
        filename:
          type: string
        content_base64:
          type: string
          description: Base64-encoded file content.
        content_type:
          type: string
          default: application/pdf
    DesignBlueprintRequest:
      type: object
      required: [prompt]
      properties:
        prompt:
          type: string
          description: Scenario description to design the blueprint from.
        interaction_type:
          type: string
          enum: [public_discourse, parallel_assembly]
          default: parallel_assembly
        documents:
          type: array
          description: Grounding documents for the scenario.
          items:
            $ref: '#/components/schemas/DocumentUpload'
        stakeholder_guidance:
          type: string
          default: ''
          description: Free-text guidance on stakeholder composition.
        stakeholder_documents:
          type: array
          description: Documents describing stakeholders.
          items:
            $ref: '#/components/schemas/DocumentUpload'
        research_mode:
          type: string
          enum: [opus, split, none]
          default: opus
          description: How much background research the designer performs.
    SweepInvestigateRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          description: Target address to analyse.
        stealth_level:
          type: string
          default: headers
          description: Request fingerprint level used when fetching the target.
    SweepRunRequest:
      type: object
      description: Provide exactly one of `config_name` or `config_yaml`.
      properties:
        config_name:
          type: [string, 'null']
          description: Name of a stored sweep configuration.
        config_yaml:
          type: [string, 'null']
          description: Inline sweep configuration in YAML.
        start_page:
          type: [integer, 'null']
          description: Page to resume from.
    SweepQueryRequest:
      type: object
      required: [name, sql]
      properties:
        name:
          type: string
          description: Sweep configuration name whose results to query.
        sql:
          type: string
          description: Read-only SQL `SELECT` statement.
        limit:
          type: integer
          default: 100
          maximum: 1000
