openapi: 3.1.0
info:
  title: Ragnova Customer API
  version: "1.0.0"
  summary: Programmatische Anbindung deines Ragnova-Bots.
  description: |
    Mit der Customer API bindest du deinen Bot programmatisch an: Wissen pushen,
    Anfragen (Leads) abrufen, Kennzahlen lesen und **Live-Tools** registrieren,
    die der Bot zur Laufzeit aufruft.

    ## Authentifizierung
    Alle Aufrufe brauchen einen API-Key im `Authorization`-Header:

    ```
    Authorization: Bearer rk_live_…
    ```

    Keys erzeugst du im Dashboard unter **Konnektoren → API-Zugriff**. Jeder Key
    ist an genau **einen Bot** gebunden und trägt **Scopes**, die festlegen, was
    er darf. Fehlt ein Scope, antwortet die API mit `403`.

    | Scope | Erlaubt |
    |-------|---------|
    | `knowledge:read` | Wissensquellen lesen |
    | `knowledge:write` | Wissen pushen/löschen |
    | `leads:read` | Anfragen lesen |
    | `analytics:read` | Kennzahlen lesen |
    | `tools:read` | Live-Tools lesen |
    | `tools:write` | Live-Tools anlegen/ändern/löschen |

    ## Limits
    - Rate-Limit: **120 Anfragen/Minute** pro Key (`429` bei Überschreitung).
    - Maximale Payload für `POST /v1/knowledge`: **256 KB**.

    ## Live-Tools (Webhooks)
    Registrierte Tools ruft Ragnova **zur Laufzeit signiert** an deinem Endpoint
    auf — siehe den `webhooks`-Abschnitt. Der Aufruf ist HMAC-signiert; verifiziere
    `X-Ragnova-Signature`, bevor du antwortest.
  contact:
    name: Ragnova Support
    email: support@ragnova.de
  license:
    name: Proprietär
servers:
  - url: https://{apiDomain}
    description: API-Host (gleicher Host wie das Widget, /v1-Präfix)
    variables:
      apiDomain:
        default: api.ragnova.de
        description: Deine API-Domain.

security:
  - apiKey: []

tags:
  - name: Knowledge
    description: Inhalte in die Wissensbasis pushen und verwalten.
  - name: Leads
    description: Erfasste Kontaktanfragen abrufen.
  - name: Analytics
    description: Aggregierte Kennzahlen des Bots.
  - name: Tools
    description: Live-Actions — Webhooks, die der Bot zur Laufzeit aufruft.

paths:
  /v1/knowledge:
    post:
      tags: [Knowledge]
      operationId: upsertKnowledge
      summary: Inhalt pushen oder syncen
      description: |
        Bettet den Inhalt synchron ein und macht ihn durchsuchbar. Mit `externalId`
        ist der Aufruf **idempotent**: ein erneuter Push mit derselben ID ersetzt die
        bestehende Quelle (alte Chunks werden entfernt).
      security:
        - apiKey: [knowledge:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/KnowledgeUpsert" }
            examples:
              produkt:
                summary: Produkt aus dem Shop
                value:
                  externalId: "prod-123"
                  title: "NodeMCU ESP8266"
                  content: "Der NodeMCU ist ein WLAN-Mikrocontroller … Lieferzeit 1–2 Tage."
                  url: "https://shop.de/p/nodemcu"
      responses:
        "200":
          description: Eingebettet und indexiert.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KnowledgeIngestResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Knowledge]
      operationId: listKnowledge
      summary: Wissensquellen listen
      security:
        - apiKey: [knowledge:read]
      responses:
        "200":
          description: Über die API angelegte Quellen.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/KnowledgeSource" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /v1/knowledge/{id}:
    parameters:
      - $ref: "#/components/parameters/KnowledgeId"
    get:
      tags: [Knowledge]
      operationId: getKnowledge
      summary: Quelle lesen
      security:
        - apiKey: [knowledge:read]
      responses:
        "200":
          description: Die Wissensquelle.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/KnowledgeSource" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Knowledge]
      operationId: deleteKnowledge
      summary: Quelle löschen
      security:
        - apiKey: [knowledge:write]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/leads:
    get:
      tags: [Leads]
      operationId: listLeads
      summary: Kontaktanfragen lesen
      security:
        - apiKey: [leads:read]
      parameters:
        - name: limit
          in: query
          description: Maximale Anzahl (1–200).
          schema: { type: integer, default: 100, minimum: 1, maximum: 200 }
      responses:
        "200":
          description: Anfragen des Bots, neueste zuerst.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Lead" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/analytics:
    get:
      tags: [Analytics]
      operationId: getAnalytics
      summary: Kennzahlen der letzten 30 Tage
      security:
        - apiKey: [analytics:read]
      responses:
        "200":
          description: Aggregierte Kennzahlen.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Analytics" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/tools:
    get:
      tags: [Tools]
      operationId: listTools
      summary: Live-Tools listen
      security:
        - apiKey: [tools:read]
      responses:
        "200":
          description: Registrierte Tools.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Tool" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Tools]
      operationId: createTool
      summary: Tool registrieren
      description: |
        Registriert eine Live-Action. Der Bot ruft den Endpoint zur Laufzeit
        plattform-vermittelt auf (SSRF-geprüft mit IP-Pinning, HMAC-signiert, kein
        Folgen von Redirects). Das zurückgegebene `signingSecret` wird **nur einmal**
        ausgegeben — damit verifiziert dein Endpoint die Echtheit der Aufrufe.
      security:
        - apiKey: [tools:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ToolInput" }
            examples:
              bestellstatus:
                summary: Bestellverfolgung
                value:
                  name: "bestellstatus"
                  description: "Verfolgt eine Bestellung anhand von Bestellnummer und E-Mail."
                  endpointUrl: "https://shop.de/api/ragnova/order"
                  authType: "bearer"
                  authSecret: "sk_shop_…"
                  parameters:
                    - { name: "order_id", description: "Bestellnummer", required: true }
                    - { name: "email", description: "E-Mail des Kunden", required: true }
      responses:
        "200":
          description: Angelegt. `signingSecret` jetzt sicher speichern.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ToolCreated" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
  /v1/tools/{id}:
    parameters:
      - $ref: "#/components/parameters/ToolId"
    get:
      tags: [Tools]
      operationId: getTool
      summary: Tool lesen
      security:
        - apiKey: [tools:read]
      responses:
        "200":
          description: Das Tool.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tool" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Tools]
      operationId: updateTool
      summary: Tool ändern
      description: Partielles Update; nur gesendete Felder werden geändert (inkl. `enabled`-Toggle).
      security:
        - apiKey: [tools:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ToolInput" }
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Tools]
      operationId: deleteTool
      summary: Tool löschen
      security:
        - apiKey: [tools:write]
      responses:
        "200": { $ref: "#/components/responses/Ok" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

webhooks:
  toolInvocation:
    post:
      summary: Tool-Aufruf (Ragnova → dein Endpoint)
      description: |
        Wenn der Bot ein registriertes Tool auslöst, sendet Ragnova diesen POST an
        deine `endpointUrl`. **Verifiziere die Signatur**, bevor du antwortest:

        `X-Ragnova-Signature = "sha256=" + HMAC_SHA256(signingSecret, timestamp + "." + body)`

        Antworte mit JSON (≤ 32 KB); der Inhalt wird dem Bot als geprüfter Kontext
        bereitgestellt. Antwortzeit ≤ Timeout (Default 5s).
      parameters:
        - name: X-Ragnova-Signature
          in: header
          required: true
          schema: { type: string, example: "sha256=9f86d0…" }
        - name: X-Ragnova-Timestamp
          in: header
          required: true
          schema: { type: string, example: "1751280000" }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                tool: { type: string, example: "bestellstatus" }
                arguments:
                  type: object
                  additionalProperties: { type: string }
                  example: { order_id: "1234", email: "kunde@example.com" }
      responses:
        "200":
          description: Ergebnis als JSON (wird dem Bot als Kontext gereicht).

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: rk_live_…
      description: API-Key aus dem Dashboard (Konnektoren → API-Zugriff).

  parameters:
    KnowledgeId:
      name: id
      in: path
      required: true
      description: Quellen-UUID, oder `ext:<externalId>` für die externe ID.
      schema: { type: string }
    ToolId:
      name: id
      in: path
      required: true
      description: Tool-UUID.
      schema: { type: string, format: uuid }

  responses:
    Ok:
      description: Erfolg.
      content:
        application/json:
          schema:
            type: object
            properties: { ok: { type: boolean, const: true } }
    BadRequest:
      description: Ungültige Eingabe.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Fehlender oder ungültiger API-Key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "unauthorized", message: "Fehlender oder ungültiger API-Key." }
    Forbidden:
      description: Dem Key fehlt der nötige Scope.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "forbidden", message: "Scope 'tools:write' fehlt." }
    NotFound:
      description: Ressource nicht gefunden (oder gehört nicht zu diesem Bot).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: Konflikt (z. B. Tool-Name bereits vergeben).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "name_taken" }
    PayloadTooLarge:
      description: Inhalt überschreitet 256 KB.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate-Limit überschritten (120/min).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: "rate_limited", message: "Zu viele Anfragen — bitte drosseln." }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, description: Maschinenlesbarer Fehlercode. }
        message: { type: string, description: Menschlich lesbare Erläuterung. }
    KnowledgeUpsert:
      type: object
      required: [content]
      properties:
        externalId:
          type: string
          description: ID im Kundensystem. Gleiche ID → idempotenter Upsert.
        title: { type: string }
        content:
          type: string
          description: Klartext, der eingebettet und durchsuchbar wird.
        url: { type: string, format: uri }
    KnowledgeIngestResult:
      type: object
      properties:
        id: { type: string, format: uuid }
        externalId: { type: string, nullable: true }
        status: { type: string, const: "ready" }
        chunks: { type: integer, description: Anzahl erzeugter Embeddings. }
    KnowledgeSource:
      type: object
      properties:
        id: { type: string, format: uuid }
        external_id: { type: string, nullable: true }
        url: { type: string, nullable: true }
        status: { type: string, example: "ready" }
        chunks: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Lead:
      type: object
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [message, callback, appointment] }
        name: { type: string, nullable: true }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        message: { type: string, nullable: true }
        preferred_time: { type: string, nullable: true }
        source_question: { type: string, nullable: true }
        status: { type: string, enum: [new, contacted, done] }
        created_at: { type: string, format: date-time }
    Analytics:
      type: object
      properties:
        rangeDays: { type: integer, example: 30 }
        conversations: { type: integer }
        answered: { type: integer }
        answeredRate: { type: integer, description: Prozent (0–100). }
        leads: { type: integer }
        csatRated: { type: integer }
        csatPositiveRate: { type: integer, description: Prozent (0–100). }
    ToolParameter:
      type: object
      required: [name, description]
      properties:
        name: { type: string, pattern: "^[a-z][a-z0-9_]{1,40}$" }
        description: { type: string }
        required: { type: boolean, default: true }
    ToolInput:
      type: object
      required: [name, description, endpointUrl]
      properties:
        name:
          type: string
          pattern: "^[a-z][a-z0-9_]{1,40}$"
          description: Slug, z. B. `bestellstatus`.
        description:
          type: string
          description: Wann das Tool greift — der Router entscheidet danach.
        endpointUrl: { type: string, format: uri, description: HTTPS-Webhook. }
        method: { type: string, enum: [POST, GET], default: POST }
        authType: { type: string, enum: [none, bearer, header], default: none }
        authHeaderName:
          type: string
          pattern: "^[A-Za-z0-9-]{1,64}$"
          description: Nur bei `authType=header`. Reservierte/Sicherheits-Header sind verboten.
        authSecret:
          type: string
          description: Secret für die Endpoint-Auth (verschlüsselt gespeichert, nie zurückgegeben).
        timeoutMs: { type: integer, default: 5000, minimum: 1000, maximum: 10000 }
        parameters:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/ToolParameter" }
    Tool:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        parameters:
          type: array
          items: { $ref: "#/components/schemas/ToolParameter" }
        endpoint_url: { type: string }
        method: { type: string }
        auth_type: { type: string, enum: [none, bearer, header] }
        auth_header_name: { type: string, nullable: true }
        timeout_ms: { type: integer }
        enabled: { type: boolean }
        created_at: { type: string, format: date-time }
    ToolCreated:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        signingSecret:
          type: string
          description: HMAC-Secret zur Verifikation — nur EINMAL sichtbar.
          example: "whsec_…"
