openapi: 3.1.0
info:
  title: VoxSim API
  version: '1.0'
  summary: LLM-based synthetic society simulation platform — programmatic access.
  description: |
    Talivio Technology OÜ · Tallinn, Estonia · GDPR + KVKK compliant.

    The VoxSim API exposes synthetic-society simulations across 12 Turkish
    political clusters with echo-chamber-aware reasoning. All endpoints are
    workspace-scoped via Sanctum bearer tokens.

    **Plan tiers:**
    - Atelier / Akademi: 60 req/min, read + Module A only
    - Saha: 300 req/min, +Module F + Module I + bulk CSV export
    - Karargah: 600 req/min, +Module B + LLM audit export + webhooks
    - Karargah+: 1200 req/min, dedicated infrastructure

    Rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`)
    are returned on every response. 429 responses include `Retry-After`.
  contact:
    name: VoxSim Support
    email: eren@talivio.com
    url: https://voxsim.talivio.com
  license:
    name: Proprietary — Talivio Technology OÜ
  termsOfService: https://voxsim.talivio.com/terms

servers:
  - url: https://voxsim.talivio.com/api/v1
    description: Production
  - url: https://voxsim.local/api/v1
    description: Local development

security:
  - BearerAuth: []

tags:
  - name: Account
    description: Workspace identity, usage telemetry, plan limits.
  - name: Simulations
    description: CRUD + module triggers (A Crash Test, F Counter-Narrative, I Optimizer, B Crisis-Break).
  - name: Personas
    description: Customer identity vectors used as the speaker of policy text.
  - name: Clusters
    description: Read-only — 12 political clusters + their latest echo-chamber snapshot.
  - name: Exports
    description: Bulk CSV streaming (Saha+).
  - name: Webhooks
    description: HMAC-signed event push to customer endpoints (Karargah+).

paths:
  /me:
    get:
      tags: [Account]
      summary: Current user + workspace
      operationId: getMe
      responses:
        '200':
          description: User and workspace context.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        '401': { $ref: '#/components/responses/Unauthorized' }

  /usage:
    get:
      tags: [Account]
      summary: Workspace usage telemetry (current month)
      operationId: getUsage
      responses:
        '200':
          description: Usage summary including simulation counts, plan limits, tokens, EUR cost.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'

  /simulations:
    get:
      tags: [Simulations]
      summary: List simulations (paginated)
      operationId: listSimulations
      parameters:
        - { name: per_page, in: query, schema: { type: integer, default: 20, minimum: 1, maximum: 100 } }
        - { name: module, in: query, schema: { type: string, enum: [a, b, d, f, g, h, i, j] } }
        - { name: status, in: query, schema: { type: string, enum: [queued, running, completed, failed, cancelled] } }
      responses:
        '200':
          description: Paginated simulation list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationListResponse'
    post:
      tags: [Simulations]
      summary: Create a Module A (Crash Test) simulation
      operationId: createSimulation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [persona_id, title, policy_text]
              properties:
                persona_id: { type: integer }
                title: { type: string, maxLength: 200 }
                policy_text: { type: string, maxLength: 10000 }
                mock: { type: boolean, default: false, description: "Use deterministic synthetic data instead of LLM (zero cost, instant)." }
            example:
              persona_id: 12
              title: "Asgari ücret zammı 2026"
              policy_text: "Asgari ücreti enflasyon karşısında eritmeyeceğiz..."
      responses:
        '201':
          description: Simulation queued. Poll the show endpoint or subscribe to webhooks for completion.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationResponse'
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '429': { $ref: '#/components/responses/MonthlyLimitReached' }
        '503': { $ref: '#/components/responses/LlmUnavailable' }

  /simulations/{public_id}:
    parameters:
      - name: public_id
        in: path
        required: true
        schema: { type: string, example: '01HZX7K2VGQ9N4F3T8M5RPDKBC' }
    get:
      tags: [Simulations]
      summary: Get simulation detail (full payload)
      operationId: getSimulation
      responses:
        '200':
          description: Simulation including result_segments + cluster snapshot versions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SimulationResponse'
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Simulations]
      summary: Soft-delete (status → cancelled). Audit trail preserved 6 months.
      operationId: deleteSimulation
      responses:
        '200':
          description: Simulation cancelled.
        '409':
          description: Cannot delete a running or queued simulation.

  /simulations/{public_id}/report:
    parameters:
      - name: public_id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Simulations]
      summary: Download watermarked PDF report
      operationId: downloadReport
      description: |
        Generates (and caches) the simulation PDF. Triple-watermark applied:
        diagonal wash + bottom-right brand stamp + steganographic token.
        Response includes `X-Watermark-Token` header for forensic reference.
      responses:
        '200':
          description: PDF binary.
          headers:
            X-Watermark-Token:
              schema: { type: string, format: uuid }
              description: UUID7 stego token id; provable provenance via HMAC.
          content:
            application/pdf: {}
        '409': { description: "Report only available for completed simulations." }

  /simulations/{public_id}/counter-narrative:
    parameters:
      - name: public_id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Simulations]
      summary: Module F — alternative framings for the highest-anger segment
      description: Saha tier and above. Spawns a child simulation; poll to retrieve variants.
      operationId: triggerCounterNarrative
      responses:
        '201':
          description: Child simulation queued.
        '403': { description: "Module F requires Saha tier or higher." }

  /simulations/{public_id}/optimizer:
    parameters:
      - name: public_id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Simulations]
      summary: Module I — generate 6 ranked policy variants
      description: Saha+. Single final variant returned (no segment-targeted versions, by design).
      operationId: triggerOptimizer
      responses:
        '201': { description: "Optimizer simulation queued." }
        '403': { description: "Module I requires Saha tier or higher." }

  /simulations/{public_id}/crisis-break:
    parameters:
      - name: public_id
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Simulations]
      summary: Module B — defensive vulnerability analysis
      description: Karargah+. PDF expires 30 days after generation.
      operationId: triggerCrisisBreak
      responses:
        '201': { description: "Crisis-break simulation queued." }
        '403': { description: "Module B requires Karargah tier or higher." }

  /personas:
    get:
      tags: [Personas]
      summary: List workspace personas
      operationId: listPersonas
      responses:
        '200':
          description: All personas in draft / awaiting_approval / approved status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/PersonaSummary' }
    post:
      tags: [Personas]
      summary: Create a customer persona
      operationId: createPersona
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, axes]
              properties:
                name: { type: string, maxLength: 120 }
                short_code: { type: string, maxLength: 16, pattern: '^[A-Z0-9_]+$' }
                is_known: { type: boolean, default: false }
                axes:
                  type: object
                  required: [economic, cultural, religious, nationalist]
                  properties:
                    economic: { type: integer, minimum: 1, maximum: 5 }
                    cultural: { type: integer, minimum: 1, maximum: 5 }
                    religious: { type: integer, minimum: 1, maximum: 5 }
                    nationalist: { type: integer, minimum: 1, maximum: 5 }
                tagline_tr: { type: string, maxLength: 280 }
                tagline_en: { type: string, maxLength: 280 }
                constituency: { type: string, maxLength: 1000 }
                wikipedia_url_tr: { type: string, format: uri }
                wikipedia_url_en: { type: string, format: uri }
            example:
              name: "Sosyal Demokrat Aday"
              axes: { economic: 2, cultural: 4, religious: 2, nationalist: 3 }
              tagline_tr: "Eşitlik ve hukuk devleti"
      responses:
        '201':
          description: Persona created and auto-approved.

  /personas/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      tags: [Personas]
      summary: Get persona detail (full self-report, identity vector)
      operationId: getPersona
      responses:
        '200':
          description: Persona with self_report axes + tagline + identity_vector.
        '404': { $ref: '#/components/responses/NotFound' }

  /clusters:
    get:
      tags: [Clusters]
      summary: List 12 political clusters (taxonomy snapshot)
      operationId: listClusters
      responses:
        '200':
          description: Active taxonomy version's cluster definitions + estimated_share.

  /clusters/{slug}:
    parameters:
      - { name: slug, in: path, required: true, schema: { type: string }, example: 'sosyal_demokrat' }
    get:
      tags: [Clusters]
      summary: Cluster detail + latest echo-chamber snapshot
      operationId: getCluster
      responses:
        '200': { description: "Cluster with axes_prior, narrative_anchor, codenames, latest snapshot." }

  /export/simulations:
    get:
      tags: [Exports]
      summary: Stream simulations CSV (UTF-8 BOM)
      description: Saha+. Memory-safe streaming for large datasets.
      operationId: exportSimulations
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: module, in: query, schema: { type: string, enum: [a, b, d, f, g, h, i, j] } }
      responses:
        '200':
          description: CSV stream.
          content:
            text/csv: {}
        '403': { description: "Saha+ required." }

  /export/personas:
    get:
      tags: [Exports]
      summary: Stream personas CSV
      operationId: exportPersonas
      responses:
        '200': { description: "CSV stream.", content: { text/csv: {} } }

  /export/llm-audit:
    get:
      tags: [Exports]
      summary: Stream LLM call audit log CSV (Karargah+, hash-only)
      description: |
        EU AI Act 6-month retention log. Hash-only — raw prompt/response
        content NOT included (provider TOS + workspace isolation).
      operationId: exportLlmAudit
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
      responses:
        '200': { description: "CSV stream.", content: { text/csv: {} } }
        '403': { description: "Karargah+ required." }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List workspace webhook endpoints (Karargah+)
      operationId: listWebhooks
      responses:
        '200': { description: "Endpoint list with masked secrets." }
    post:
      tags: [Webhooks]
      summary: Register a webhook endpoint
      description: |
        The full plaintext `secret` is returned ONCE in the create response.
        Use it server-side to verify HMAC-SHA256 signatures of incoming
        deliveries.

        Signature format (Stripe-style, replay-protected):
        `X-VoxSim-Signature: t=<unix>,v1=<hex>`
        where `<hex> = HMAC-SHA256(secret, "<unix>." + raw_body)`.

        Consumer must:
        1. Parse `t` and `v1` from the header.
        2. Reject if `|now - t| > 300s` (5-minute replay tolerance).
        3. Recompute `HMAC-SHA256(secret, t + "." + raw_body)` and `hash_equals` against `v1`.

        `X-VoxSim-Timestamp` mirrors `t` for convenience.
      operationId: createWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url, events]
              properties:
                name: { type: string, maxLength: 120 }
                url: { type: string, format: uri, pattern: '^https://' }
                events:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum: [simulation.completed, simulation.failed, academic.verified, academic.rejected, plan.changed]
                active: { type: boolean, default: true }
      responses:
        '201': { description: "Endpoint registered. Plaintext secret in `data.secret` — store it now." }

  /webhooks/{id}:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      tags: [Webhooks]
      summary: Endpoint detail
      operationId: getWebhook
      responses:
        '200': { description: "Endpoint detail." }
    put:
      tags: [Webhooks]
      summary: Update endpoint (subset of fields)
      operationId: updateWebhook
      responses:
        '200': { description: "Updated endpoint." }
    delete:
      tags: [Webhooks]
      summary: Delete endpoint (deliveries cascade)
      operationId: deleteWebhook
      responses:
        '200': { description: "Deleted." }

  /webhooks/{id}/rotate-secret:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    post:
      tags: [Webhooks]
      summary: Rotate signing secret (old invalidated immediately)
      operationId: rotateWebhookSecret
      responses:
        '200': { description: "New plaintext secret returned in `data.secret`." }

  /webhooks/{id}/deliveries:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      tags: [Webhooks]
      summary: Last 100 delivery attempts (audit)
      operationId: webhookDeliveries
      responses:
        '200': { description: "Delivery attempts with status, attempt count, response_status." }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Token
      description: |
        Sanctum personal access token. Token must include the
        `workspace:{id}` ability for the workspace it should operate on.

  responses:
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Plan tier insufficient or workspace inactive.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource not found in this workspace.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationError:
      description: Request validation failed.
    MonthlyLimitReached:
      description: Atelier/Akademi monthly Module A limit (5) reached.
    LlmUnavailable:
      description: LLM gateway not configured; pass `mock=true` for synthetic data.

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: 'plan_locked' }
            message: { type: string }
            details: { type: object }

    Meta:
      type: object
      properties:
        api_version: { type: string, example: 'v1' }
        generated_at: { type: string, format: date-time }
        pagination:
          type: object
          properties:
            total: { type: integer }
            per_page: { type: integer }
            current_page: { type: integer }
            last_page: { type: integer }

    MeResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            user:
              type: object
              properties:
                id: { type: integer }
                name: { type: string }
                email: { type: string, format: email }
                role_in_workspace: { type: string, enum: [owner, admin, analyst, viewer] }
            workspace:
              type: object
              properties:
                id: { type: integer }
                name: { type: string }
                slug: { type: string }
                plan: { type: string, enum: [atolye, saha, karargah, karargah_plus, akademi] }
                modules_unlocked: { type: array, items: { type: string } }
                seats_used: { type: integer }
                seats_max: { type: integer }
                status: { type: string, enum: [trialing, active, past_due, suspended, cancelled] }
            token:
              type: object
              properties:
                name: { type: string }
                abilities: { type: array, items: { type: string } }
                last_used_at: { type: string, format: date-time, nullable: true }
        meta: { $ref: '#/components/schemas/Meta' }

    UsageResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            workspace:
              type: object
              properties:
                slug: { type: string }
                name: { type: string }
                plan: { type: string }
            cycle:
              type: object
              properties:
                starts_at: { type: string, format: date-time }
                ends_at: { type: string, format: date-time }
                days_remaining: { type: integer }
            simulations:
              type: object
              properties:
                total: { type: integer }
                completed: { type: integer }
                by_module: { type: object, additionalProperties: { type: object } }
            limits:
              type: object
              properties:
                module_a_per_month: { type: integer, nullable: true }
                module_a_used: { type: integer }
                module_a_remaining: { type: integer, nullable: true }
            tokens:
              type: object
              properties:
                tokens_in: { type: integer }
                tokens_out: { type: integer }
                tokens_total: { type: integer }
            cost:
              type: object
              properties:
                eur: { type: number, format: float }

    SimulationSummary:
      type: object
      properties:
        public_id: { type: string }
        title: { type: string }
        module: { type: string, enum: [a, b, d, f, g, h, i, j] }
        status: { type: string, enum: [queued, running, completed, failed, cancelled] }
        persona:
          type: object
          nullable: true
          properties:
            id: { type: integer }
            name: { type: string }
            short_code: { type: string }
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time, nullable: true }
        cost_eur: { type: number, format: float }
        tokens_total: { type: integer }
        accuracy_score: { type: number, format: float, nullable: true }

    SimulationResponse:
      type: object
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/SimulationSummary'
            - type: object
              properties:
                policy_text: { type: string }
                cluster_taxonomy_version: { type: string }
                cluster_snapshot_versions: { type: object, additionalProperties: { type: integer } }
                result_segments: { type: object, additionalProperties: true }
                tokens_in: { type: integer }
                tokens_out: { type: integer }

    SimulationListResponse:
      type: object
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/SimulationSummary' }
        meta: { $ref: '#/components/schemas/Meta' }

    PersonaSummary:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        short_code: { type: string }
        is_known: { type: boolean }
        status: { type: string, enum: [draft, awaiting_approval, approved] }
        axes:
          type: object
          properties:
            economic: { type: integer, minimum: 1, maximum: 5 }
            cultural: { type: integer, minimum: 1, maximum: 5 }
            religious: { type: integer, minimum: 1, maximum: 5 }
            nationalist: { type: integer, minimum: 1, maximum: 5 }
        created_at: { type: string, format: date-time }
