openapi: 3.1.0
info:
  title: TicketZero Public API
  version: 1.0.0
  description: >-
    DSGVO-natives Customer-Messaging für DACH. Public-REST-API geschützt durch API-Keys (Bearer-Token) oder
    Session-Cookies für interne Web-App-Routen. Rate-Limit: 60 Requests/Minute pro API-Key (`429` mit
    `Retry-After`-Header bei Überschreitung). Diese Spec wird automatisch aus den Zod-Schemas der NestJS-Controller
    generiert (`pnpm --filter @sg/api openapi:emit`); Hand-Edits an dieser Datei werden beim nächsten Emit
    überschrieben.
servers:
  - url: https://ticketzero.de
    description: Production
  - url: http://localhost:3001
    description: Dev
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: hc_live_*
      description: >-
        API-Key mit Prefix `hc_live_`. Header: `Authorization: Bearer hc_live_xxx`. Es gibt aktuell keinen separaten
        Sandbox-Modus/-Prefix; alle Keys wirken live auf den Tenant-Daten.
    sessionCookie:
      type: apiKey
      in: cookie
      name: sg.session_token
      description: >-
        Better-Auth-Session-Cookie. Wird via `POST /auth/sign-in/magic-link` + Verify-Link gesetzt (HttpOnly,
        SameSite=Lax, 30d).
  schemas:
    ConversationListItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        contact_id:
          type: string
          format: uuid
        contact_name:
          type:
            - string
            - 'null'
        contact_email:
          type:
            - string
            - 'null'
        contact_phone:
          type:
            - string
            - 'null'
        channel:
          type: string
        status:
          type: string
          enum:
            - open
            - snoozed
            - done
        assignee_user_id:
          type:
            - string
            - 'null'
          format: uuid
        assignee_user_ids:
          type: array
          items:
            type: string
            format: uuid
        subject:
          type:
            - string
            - 'null'
        inbox_id:
          type:
            - string
            - 'null'
          format: uuid
        last_message_at:
          type:
            - string
            - 'null'
          format: date-time
        metadata:
          type:
            - object
            - 'null'
          additionalProperties: {}
        created_at:
          type: string
          format: date-time
        unread:
          type: boolean
      required:
        - id
        - contact_id
        - contact_name
        - contact_email
        - contact_phone
        - channel
        - status
        - assignee_user_id
        - assignee_user_ids
        - subject
        - inbox_id
        - last_message_at
        - metadata
        - created_at
        - unread
    ConversationListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ConversationListItem'
        meta:
          type: object
          properties:
            next_cursor:
              type:
                - string
                - 'null'
          required:
            - next_cursor
      required:
        - data
        - meta
    Conversation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        contact_id:
          type: string
          format: uuid
        contact:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type:
                - string
                - 'null'
            email:
              type:
                - string
                - 'null'
            phone:
              type:
                - string
                - 'null'
            consent_status:
              type:
                - string
                - 'null'
          required:
            - id
            - name
            - email
            - phone
            - consent_status
        channel:
          type: string
        status:
          type: string
          enum:
            - open
            - snoozed
            - done
        assignee_user_id:
          type:
            - string
            - 'null'
          format: uuid
        assignee_user_ids:
          type: array
          items:
            type: string
            format: uuid
        subject:
          type:
            - string
            - 'null'
        inbox_id:
          type:
            - string
            - 'null'
          format: uuid
        last_message_at:
          type:
            - string
            - 'null'
          format: date-time
        snoozed_until:
          type:
            - string
            - 'null'
          format: date-time
        metadata:
          type:
            - object
            - 'null'
          additionalProperties: {}
        ai_agent_state:
          type: string
          enum:
            - idle
            - active
            - paused_by_human
            - completed
            - expired
        active_automation_id:
          type:
            - string
            - 'null'
          format: uuid
        active_agent_name:
          type:
            - string
            - 'null'
        ai_session_expires_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - contact_id
        - contact
        - channel
        - status
        - assignee_user_id
        - assignee_user_ids
        - subject
        - inbox_id
        - last_message_at
        - snoozed_until
        - metadata
        - ai_agent_state
        - active_automation_id
        - active_agent_name
        - ai_session_expires_at
        - created_at
        - updated_at
    ConversationContactPanel:
      type: object
      properties:
        contact:
          type: object
          properties:
            id:
              type: string
              format: uuid
            display_name:
              type: string
            name:
              type:
                - string
                - 'null'
            email:
              type:
                - string
                - 'null'
            phone:
              type:
                - string
                - 'null'
          required:
            - id
            - display_name
            - name
            - email
            - phone
        attributes:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
              value:
                type: string
            required:
              - key
              - value
        recent_conversations:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              channel:
                type: string
              preview:
                type: string
              status:
                type: string
              last_message_at:
                type:
                  - string
                  - 'null'
                format: date-time
            required:
              - id
              - channel
              - preview
              - status
              - last_message_at
        automation_runs:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              automation_id:
                type: string
                format: uuid
              automation_name:
                type:
                  - string
                  - 'null'
              status:
                type: string
                enum:
                  - running
                  - failed
                  - completed
              started_at:
                type:
                  - string
                  - 'null'
                format: date-time
              finished_at:
                type:
                  - string
                  - 'null'
                format: date-time
            required:
              - id
              - automation_id
              - automation_name
              - status
              - started_at
              - finished_at
        notes:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              body:
                type: string
              author_name:
                type:
                  - string
                  - 'null'
              created_at:
                type:
                  - string
                  - 'null'
                format: date-time
            required:
              - id
              - body
              - author_name
              - created_at
        attachments:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              mime_type:
                type:
                  - string
                  - 'null'
              url:
                type:
                  - string
                  - 'null'
              created_at:
                type:
                  - string
                  - 'null'
                format: date-time
            required:
              - id
              - name
              - mime_type
              - url
              - created_at
        contact_lists:
          type: object
          properties:
            all:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  name:
                    type: string
                required:
                  - id
                  - name
            member_ids:
              type: array
              items:
                type: string
                format: uuid
          required:
            - all
            - member_ids
        whatsapp_opt_ins:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              opt_in_at:
                type:
                  - string
                  - 'null'
                format: date-time
              opt_in_source:
                type: string
              opt_in_text:
                type:
                  - string
                  - 'null'
              revoked_at:
                type:
                  - string
                  - 'null'
                format: date-time
              active:
                type: boolean
            required:
              - id
              - opt_in_at
              - opt_in_source
              - opt_in_text
              - revoked_at
              - active
        whatsapp_connected:
          type: boolean
        hat_whatsapp_bezug:
          type: boolean
        crm_lead:
          type:
            - object
            - 'null'
          properties:
            id:
              type: string
              format: uuid
          required:
            - id
      required:
        - contact
        - attributes
        - recent_conversations
        - automation_runs
        - notes
        - attachments
        - contact_lists
        - whatsapp_opt_ins
        - whatsapp_connected
        - hat_whatsapp_bezug
        - crm_lead
    CreateConversationRequest:
      type: object
      properties:
        contact_id:
          type: string
          format: uuid
        channel:
          type: string
          enum:
            - whatsapp
            - email
            - webchat
            - facebook
            - instagram
            - sms
            - telegram
        channel_account_id:
          type: string
          format: uuid
        inbox_id:
          type: string
          format: uuid
        subject:
          type: string
          minLength: 1
          maxLength: 500
        metadata:
          type: object
          additionalProperties: {}
      required:
        - contact_id
        - channel
      description: Erstellt eine neue Konversation. Der Kontakt muss bereits im Tenant existieren.
    PatchConversationRequest:
      type: object
      properties:
        status:
          type: string
          enum:
            - open
            - snoozed
            - done
        assignee_user_id:
          type:
            - string
            - 'null'
          format: uuid
        assignee_user_ids:
          type: array
          items:
            type: string
            format: uuid
          maxItems: 50
        add_assignee_user_id:
          type: string
          format: uuid
        remove_assignee_user_id:
          type: string
          format: uuid
        snoozed_until:
          type:
            - string
            - 'null'
          format: date-time
        snooze_remind_user_id:
          type:
            - string
            - 'null'
          format: uuid
        inbox_id:
          type:
            - string
            - 'null'
          format: uuid
        starred:
          type: boolean
        add_tag:
          type: string
          minLength: 1
          maxLength: 50
        remove_tag:
          type: string
          minLength: 1
          maxLength: 50
      description: >-
        Partial-Update einer Konversation. Status-Übergänge done|snoozed -> open feuern automation triggers
        (conversation.reopened).
    Message:
      type: object
      properties:
        id:
          type: string
          format: uuid
        direction:
          type: string
          enum:
            - inbound
            - outbound
        sender_type:
          type: string
          enum:
            - contact
            - user
            - ai_agent
            - system
        content_type:
          type: string
          enum:
            - text
            - email_html
        body:
          type:
            - string
            - 'null'
        channel_status:
          type:
            - string
            - 'null'
        channel_msg_id:
          type:
            - string
            - 'null'
        metadata:
          type:
            - object
            - 'null'
          additionalProperties: {}
        created_at:
          type: string
          format: date-time
        hinweise:
          type: array
          items:
            type: string
      required:
        - id
        - direction
        - sender_type
        - content_type
        - body
        - channel_status
        - channel_msg_id
        - metadata
        - created_at
        - hinweise
    CreateMessageRequest:
      type: object
      properties:
        direction:
          type: string
          enum:
            - outbound
          default: outbound
        sender_type:
          type: string
          enum:
            - user
            - ai_agent
            - system
          default: user
        sender_user_id:
          type: string
          format: uuid
        content_type:
          type: string
          enum:
            - text
            - email_html
          default: text
        body:
          type: string
          maxLength: 50000
          default: ''
        subject:
          type: string
          minLength: 1
          maxLength: 500
        attachments:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
                minLength: 1
                maxLength: 400
              name:
                type: string
                minLength: 1
                maxLength: 160
              mime:
                type: string
                minLength: 1
                maxLength: 100
            required:
              - key
              - name
              - mime
          maxItems: 3
        whatsapp_template:
          type: object
          properties:
            template_id:
              type: string
              format: uuid
            variables:
              type: object
              additionalProperties:
                type: string
                maxLength: 1000
              default: {}
          required:
            - template_id
        metadata:
          type: object
          additionalProperties: {}
        trotzdem:
          type: boolean
      description: >-
        Sendet eine Nachricht in einer Konversation. Outbound-Messages werden vom Channel-Dispatcher an den Adapter
        (SMTP/WhatsApp/Webchat) weitergereicht. AI-generated Messages tragen automatisch das
        AI-Act-Article-50-Disclosure-Flag.
    PublicContactAttributeDefinition:
      type: object
      properties:
        id:
          type: string
          format: uuid
        key:
          type: string
        label:
          type: string
        type:
          type: string
        options:
          type:
            - array
            - 'null'
          items:
            type: string
        required:
          type: boolean
        sort_order:
          type: integer
      required:
        - id
        - key
        - label
        - type
        - options
        - required
        - sort_order
      description: >-
        Eine Custom-Attribut-Definition (Feld-Metadaten). `options` ist nur bei type=select/multiselect gesetzt, sonst
        null.
    PublicContactAttributeDefinitionsListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicContactAttributeDefinition'
      required:
        - data
    PublicContactList:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        member_count:
          type: integer
      required:
        - id
        - name
        - description
        - member_count
      description: Eine Kontaktliste (Segment) mit Mitglieder-Anzahl.
    PublicContactListsResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicContactList'
      required:
        - data
    PublicAddListMemberBody:
      type: object
      properties:
        contact_id:
          type: string
          format: uuid
      required:
        - contact_id
      additionalProperties: false
      description: Fügt den Kontakt der Liste hinzu (idempotent - schon vorhandene bleiben).
    PublicContactSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type:
            - string
            - 'null'
        name:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        consent_status:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        attributes:
          type: object
          additionalProperties: {}
      required:
        - id
        - external_id
        - name
        - email
        - phone
        - consent_status
        - created_at
      description: >-
        Public-API-Sicht auf einen Kontakt. Email/Phone sind serverseitig FLE-decrypted und im Klartext enthalten - der
        Aufrufer ist verantwortlich für DSGVO-konformen Umgang.
    PublicContactListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicContactSummary'
        next_cursor:
          type:
            - string
            - 'null'
          description: 'Undurchsichtiger Cursor: next_cursor der vorigen Antwort unverändert zurückgeben.'
      required:
        - data
        - next_cursor
    PublicContactDetail:
      allOf:
        - $ref: '#/components/schemas/PublicContactSummary'
        - type: object
          properties:
            attributes:
              type:
                - object
                - 'null'
              additionalProperties: {}
            metadata:
              type:
                - object
                - 'null'
              additionalProperties: {}
            updated_at:
              type: string
              format: date-time
          required:
            - attributes
            - metadata
            - updated_at
      description: >-
        Public-API-Detailsicht auf einen Kontakt. Email/Phone sind serverseitig FLE-decrypted und im Klartext enthalten
        - der Aufrufer ist verantwortlich für DSGVO-konformen Umgang. `metadata` ist auf eine Allowlist reduziert
        (aktuell nur `source`) - interne Marker (z.B. welcher KI-Agent/API-Key den Kontakt angelegt hat) sind NICHT
        enthalten.
    PublicCreateContactBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        email:
          type: string
          format: email
        phone:
          type: string
          pattern: ^\+[1-9]\d{1,14}$
        external_id:
          type: string
          minLength: 1
          maxLength: 200
        attributes:
          type: object
          additionalProperties:
            anyOf:
              - type: string
              - type: number
              - type: boolean
              - type: array
                items:
                  type: string
              - type: 'null'
      additionalProperties: false
      description: >-
        Anlage eines neuen Kontakts. Mindestens eines von email/phone/external_id ist Pflicht, sonst 400 - `name` allein
        reicht NICHT (kein Lookup-Pfad, wäre über keinen Weg wiederauffindbar).
    PublicPatchContactBody:
      type: object
      properties:
        name:
          type:
            - string
            - 'null'
          minLength: 1
          maxLength: 200
        email:
          type:
            - string
            - 'null'
          format: email
        phone:
          type:
            - string
            - 'null'
          pattern: ^\+[1-9]\d{1,14}$
        consent_status:
          type:
            - string
            - 'null'
          enum:
            - opted_in
            - opted_out
            - pending_double_optin
        notes:
          type:
            - string
            - 'null'
          maxLength: 20000
        attributes:
          type: object
          additionalProperties:
            anyOf:
              - type: string
              - type: number
              - type: boolean
              - type: array
                items:
                  type: string
              - type: 'null'
      additionalProperties: false
      description: >-
        Partial-Update eines Kontakts: Name/E-Mail/Telefon/Consent ändern und Custom-Attribute setzen (null entfernt).
        Personenbezogene Felder werden serverseitig FLE-verschlüsselt.
    PublicConversationSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        contact_id:
          type: string
          format: uuid
        channel:
          type: string
        status:
          type: string
          enum:
            - open
            - snoozed
            - done
        subject:
          type:
            - string
            - 'null'
        inbox_id:
          type:
            - string
            - 'null'
          format: uuid
        last_message_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
      required:
        - id
        - contact_id
        - channel
        - status
        - subject
        - inbox_id
        - last_message_at
        - created_at
      description: >-
        Public-API-Sicht auf eine Konversation. Enthält KEINE PII des Kontakts (Name/Email/Telefon) - dafür separat
        /v1/api-public/contacts/:id abrufen.
    PublicConversationActionResult:
      allOf:
        - $ref: '#/components/schemas/PublicConversationSummary'
        - type: object
          properties:
            assignee_user_id:
              type:
                - string
                - 'null'
              format: uuid
            assignee_user_ids:
              type: array
              items:
                type: string
                format: uuid
            snoozed_until:
              type:
                - string
                - 'null'
              format: date-time
          required:
            - assignee_user_id
            - assignee_user_ids
            - snoozed_until
      description: Zusammenfassung der Konversation nach einer Bearbeiten-Aktion (PATCH).
    PublicPatchConversationBody:
      type: object
      properties:
        status:
          type: string
          enum:
            - open
            - snoozed
            - done
        assignee_user_id:
          type:
            - string
            - 'null'
          format: uuid
        add_assignee_user_id:
          type: string
          format: uuid
        remove_assignee_user_id:
          type: string
          format: uuid
        snoozed_until:
          type:
            - string
            - 'null'
          format: date-time
        inbox_id:
          type:
            - string
            - 'null'
          format: uuid
        add_tag:
          type: string
          minLength: 1
          maxLength: 50
          pattern: ^[A-Za-z0-9_-]+$
        remove_tag:
          type: string
          minLength: 1
          maxLength: 50
          pattern: ^[A-Za-z0-9_-]+$
      additionalProperties: false
      description: >-
        Partial-Update einer Konversation: Status ändern, zuweisen/entziehen, Snooze setzen, ins Postfach verschieben
        oder einen Tag setzen/entfernen. Status-Übergänge (done|snoozed -> open) sowie Tag-/Zuweisungs-Änderungen feuern
        dieselben Automation-Trigger wie die Web-App.
    PublicNote:
      type: object
      properties:
        id:
          type: string
          format: uuid
        body:
          type: string
        created_at:
          type: string
          format: date-time
      required:
        - id
        - body
        - created_at
      description: Eine interne Notiz an einer Konversation.
    PublicAddNoteBody:
      type: object
      properties:
        body:
          type: string
          minLength: 1
          maxLength: 8000
      required:
        - body
      additionalProperties: false
      description: >-
        Fügt eine INTERNE Notiz zur Konversation hinzu. Für den Kunden unsichtbar (channel_status=internal), wird nicht
        versendet.
    PublicNotesListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicNote'
      required:
        - data
    PublicConversationListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicConversationSummary'
        next_cursor:
          type:
            - string
            - 'null'
          description: 'Undurchsichtiger Cursor: next_cursor der vorigen Antwort unverändert zurückgeben.'
      required:
        - data
        - next_cursor
    PublicMessage:
      type: object
      properties:
        id:
          type: string
          format: uuid
        direction:
          type: string
          enum:
            - inbound
            - outbound
        sender_type:
          type: string
          enum:
            - contact
            - user
            - ai_agent
            - system
        content_type:
          type: string
        body:
          type:
            - string
            - 'null'
        ai_generated:
          type: boolean
        created_at:
          type: string
          format: date-time
      required:
        - id
        - direction
        - sender_type
        - content_type
        - body
        - ai_generated
        - created_at
      description: Eine Nachricht innerhalb einer Konversation. Body ist Klartext (serverseitig FLE-decrypted vor Auslieferung).
    PublicConversationDetail:
      allOf:
        - $ref: '#/components/schemas/PublicConversationSummary'
        - type: object
          properties:
            snoozed_until:
              type:
                - string
                - 'null'
              format: date-time
            assignee_user_id:
              type:
                - string
                - 'null'
              format: uuid
            assignee_user_ids:
              type: array
              items:
                type: string
                format: uuid
            metadata:
              type:
                - object
                - 'null'
              additionalProperties: {}
            messages:
              type: array
              items:
                $ref: '#/components/schemas/PublicMessage'
          required:
            - snoozed_until
            - assignee_user_id
            - assignee_user_ids
            - metadata
            - messages
      description: >-
        Detail-Sicht einer Konversation inklusive der letzten 50 Nachrichten. Für ältere Nachrichten `GET
        /v1/api-public/conversations/:id/messages` benutzen.
    PublicFile:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        mime_type:
          type: string
        size_bytes:
          type: number
        is_public:
          type: boolean
        folder_id:
          type:
            - string
            - 'null'
          format: uuid
        public_url:
          type:
            - string
            - 'null'
        created_at:
          type:
            - string
            - 'null'
      required:
        - id
        - name
        - mime_type
        - size_bytes
        - is_public
        - folder_id
        - public_url
        - created_at
      description: Eine Datei im Workspace-Datei-Manager.
    PublicFilesListResponse:
      type: object
      properties:
        folders:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              name:
                type: string
              created_at:
                type:
                  - string
                  - 'null'
            required:
              - id
              - name
              - created_at
        files:
          type: array
          items:
            $ref: '#/components/schemas/PublicFile'
        total_folders:
          type: integer
        total_files:
          type: integer
        truncated:
          type: boolean
      required:
        - folders
        - files
        - total_folders
        - total_files
        - truncated
    PublicFileDownload:
      type: object
      properties:
        url:
          type: string
      required:
        - url
      description: Kurzlebige (5 Min) presignte Download-URL.
    PublicInbox:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        is_default:
          type: boolean
      required:
        - id
        - name
        - is_default
      description: Ein Postfach (Inbox) des Workspaces. `is_default=true` markiert das Standard-Postfach.
    PublicInboxesListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicInbox'
      required:
        - data
    PublicMe:
      type: object
      properties:
        workspace_id:
          type: string
          format: uuid
        name:
          type:
            - string
            - 'null'
        plan:
          type:
            - string
            - 'null'
      required:
        - workspace_id
        - name
        - plan
      description: Info über den mit dem API-Key verbundenen Workspace (Tenant).
    PublicMember:
      type: object
      properties:
        user_id:
          type: string
          format: uuid
        display_name:
          type:
            - string
            - 'null'
        role:
          type:
            - string
            - 'null'
      required:
        - user_id
        - display_name
        - role
      description: >-
        Ein Workspace-Mitglied. `display_name` ist serverseitig FLE-decrypted (Klartext). KEINE E-Mail-Adresse (Audit
        AM, F3b, 2026-09-06: ein API-Key konnte damit unbegrenzt Klarname+E-Mail aller Mitarbeiter abziehen -
        Vertragsänderung, `email` ist ab jetzt fest entfernt statt nur optional).
    PublicMembersListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicMember'
      required:
        - data
    PublicSendMessageBody:
      type: object
      properties:
        body:
          type: string
          minLength: 1
          maxLength: 50000
        content_type:
          type: string
          enum:
            - text
            - email_html
        subject:
          type: string
          minLength: 1
          maxLength: 500
      required:
        - body
      additionalProperties: false
      description: >-
        Sendet eine outbound Nachricht in der Konversation. `content_type=text` (Default) für normale Nachrichten,
        `email_html` für eine HTML-E-Mail (optional mit `subject`). Der Channel-Dispatcher verschickt über den Kanal der
        Konversation (SMTP für E-Mail-Konversationen).
    PublicMessagesListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicMessage'
        next_cursor:
          type:
            - string
            - 'null'
      required:
        - data
        - next_cursor
    PublicTag:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        label:
          type: string
        color:
          type: string
      required:
        - id
        - name
        - label
        - color
      description: >-
        Ein Tag aus dem Workspace-Tag-Dictionary. `name` = stabiler Slug-Key, `label` = Anzeige, `color` = Paletten-Slug
        oder Hex.
    PublicTagsListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PublicTag'
      required:
        - data
    WebhookSubscription:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
        status:
          type: string
          enum:
            - active
            - disabled
            - auto_disabled
        last_delivery_at:
          type:
            - string
            - 'null'
          format: date-time
        last_status_code:
          type:
            - integer
            - 'null'
        created_at:
          type: string
          format: date-time
      required:
        - id
        - name
        - url
        - events
        - status
        - last_delivery_at
        - last_status_code
        - created_at
      description: >-
        Abonniert Domain-Events für ein Tenant und postet sie HMAC-SHA256-signiert an `url`. Das Secret wird beim Create
        einmalig im Klartext zurückgegeben - der Caller MUSS es kopieren, danach ist nur noch eine Rotation per PATCH
        möglich (in der Web-App: Knopf "Secret neu erzeugen" am Webhook).
    WebhookSubscriptionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/WebhookSubscription'
      required:
        - data
    WebhookSubscriptionWithSecret:
      allOf:
        - $ref: '#/components/schemas/WebhookSubscription'
        - type: object
          properties:
            secret:
              type: string
          required:
            - secret
      description: >-
        Antwort des Create- oder Rotate-Endpoints. `secret` ist EINMALIG enthalten und muss vom Caller gespeichert
        werden - er taucht nirgendwo sonst nochmal auf.
    WebhookEventType:
      type: string
      enum:
        - message.received
        - message.sent
        - conversation.created
        - conversation.status_changed
        - conversation.assigned
        - contact.created
        - webhook.test
        - '*'
      description: >-
        Typ eines abonnierbaren Domain-Events. `*` ist Wildcard und matched alle Event-Typen. `webhook.test` wird nur
        vom Test-Fire-Endpoint emittiert.
    CreateWebhookSubscription:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        url:
          type: string
          maxLength: 2048
          format: uri
          pattern: ^https:\/\/
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          minItems: 1
          maxItems: 20
      required:
        - name
        - url
        - events
      description: >-
        Neue Subscription anlegen. `events` darf `["*"]` enthalten um auf alle Events zu hören. `url` muss `https:`
        verwenden (die Nutzlast trägt Klartext-PII).
    PatchWebhookSubscription:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        url:
          type: string
          maxLength: 2048
          format: uri
          pattern: ^https:\/\/
        events:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEventType'
          minItems: 1
          maxItems: 20
        status:
          type: string
          enum:
            - active
            - disabled
        rotate_secret:
          type: boolean
      description: >-
        `rotate_secret: true` erzeugt ein neues Secret und gibt es einmalig im Response zurück (Schema
        `WebhookSubscriptionWithSecret`). Sonst Schema `WebhookSubscription`. `url` muss `https:` verwenden.
    WebhookDelivery:
      type: object
      properties:
        id:
          type: string
          format: uuid
        subscription_id:
          type: string
          format: uuid
        event_type:
          type: string
        status:
          type: string
          enum:
            - pending
            - delivered
            - failed
        attempts:
          type: integer
        next_attempt_at:
          type: string
          format: date-time
        last_status_code:
          type:
            - integer
            - 'null'
        last_error:
          type:
            - string
            - 'null'
        response_body_snippet:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        delivered_at:
          type:
            - string
            - 'null'
          format: date-time
      required:
        - id
        - subscription_id
        - event_type
        - status
        - attempts
        - next_attempt_at
        - last_status_code
        - last_error
        - response_body_snippet
        - created_at
        - delivered_at
      description: Eine Delivery-Versuch-Aufzeichnung. Hilfreich für Debugging - das UI listet die letzten 50 pro Subscription.
    AuditEvent:
      type: object
      properties:
        id:
          type: string
        occurred_at:
          type: string
          format: date-time
        actor_type:
          type: string
          enum:
            - user
            - ai_agent
            - system
            - channel
        actor_id:
          type:
            - string
            - 'null'
          format: uuid
        action:
          type: string
        resource_type:
          type:
            - string
            - 'null'
        resource_id:
          type:
            - string
            - 'null'
        diff:
          type:
            - object
            - 'null'
          additionalProperties: {}
        prev_hash_b64:
          type:
            - string
            - 'null'
        row_hash_b64:
          type:
            - string
            - 'null'
        request_id:
          type:
            - string
            - 'null'
      required:
        - id
        - occurred_at
        - actor_type
        - actor_id
        - action
        - resource_type
        - resource_id
        - diff
        - prev_hash_b64
        - row_hash_b64
        - request_id
    AuditEventsResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/AuditEvent'
        meta:
          type: object
          properties:
            next_before:
              type:
                - string
                - 'null'
          required:
            - next_before
      required:
        - data
        - meta
    AuditVerifyResponse:
      type: object
      properties:
        status:
          type: string
          enum:
            - intact
            - broken
            - empty
        rows_checked:
          type: integer
          minimum: 0
        first_id:
          type:
            - string
            - 'null'
        last_id:
          type:
            - string
            - 'null'
        break:
          type:
            - object
            - 'null'
          properties:
            kind:
              type: string
              enum:
                - row_hash_missing
                - first_row_has_prev_hash
                - prev_hash_mismatch
                - row_hash_mismatch
            row_id:
              type: string
            occurred_at:
              type: string
              format: date-time
            action:
              type: string
            expected_hash_b64:
              type:
                - string
                - 'null'
            actual_hash_b64:
              type:
                - string
                - 'null'
          required:
            - kind
            - row_id
            - occurred_at
            - action
            - expected_hash_b64
            - actual_hash_b64
      required:
        - status
        - rows_checked
        - first_id
        - last_id
        - break
    Contact:
      type: object
      properties:
        id:
          type: string
          format: uuid
        external_id:
          type:
            - string
            - 'null'
        name:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        consent_status:
          type:
            - string
            - 'null'
        metadata:
          type:
            - object
            - 'null'
          additionalProperties: {}
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - external_id
        - name
        - email
        - phone
        - consent_status
        - metadata
        - created_at
        - updated_at
    ContactListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Contact'
        meta:
          type: object
          properties:
            next_cursor:
              type:
                - string
                - 'null'
          required:
            - next_cursor
      required:
        - data
        - meta
    ContactDetail:
      allOf:
        - $ref: '#/components/schemas/Contact'
        - type: object
          properties:
            consent_given_at:
              type:
                - string
                - 'null'
              format: date-time
            notes:
              type:
                - string
                - 'null'
          required:
            - consent_given_at
            - notes
    CreateContactRequest:
      type: object
      properties:
        external_id:
          type: string
          minLength: 1
          maxLength: 200
        phone:
          type: string
          pattern: ^\+[1-9]\d{1,14}$
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 1
          maxLength: 200
        consent_status:
          type: string
          enum:
            - opted_in
            - opted_out
            - pending_double_optin
        metadata:
          type: object
          additionalProperties: {}
      description: >-
        Anlegen eines neuen Kontakts. Telefon muss E.164-formatiert sein (z.B. +491701234567). Personenbezogene Felder
        werden serverseitig FLE-verschlüsselt.
    FolderItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type:
            - string
            - 'null'
          format: date-time
      required:
        - id
        - name
        - created_at
    DateiVerwendungKurz:
      type: object
      properties:
        automationen_aktiv:
          type: number
        wartende_kette:
          type: boolean
        beileger:
          type: boolean
        geprueft:
          type: boolean
      required:
        - automationen_aktiv
        - wartende_kette
        - beileger
        - geprueft
      description: >-
        Knapper Verwendungs-Hinweis für die Liste (ohne Namen): wie viele AKTIVEN Automationen/wartenden Läufe an dieser
        Datei hängen, ob eine wartende Kette darunter ist, und ob sie in der Beileger-Liste steht. `geprüft=false` heißt
        NICHT "keine Bindung", sondern "das Ermitteln ist gerade gescheitert" (nie als "frei" werten). Namen liefert GET
        /v1/files/{id}/verwendung.
    FileItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        mime_type:
          type: string
        size_bytes:
          type: number
        is_public:
          type: boolean
        ki_freigabe:
          type: boolean
        ki_beschreibung:
          type:
            - string
            - 'null'
        folder_id:
          type:
            - string
            - 'null'
          format: uuid
        public_url:
          type:
            - string
            - 'null'
        thumb_url:
          type:
            - string
            - 'null'
        verwendung:
          $ref: '#/components/schemas/DateiVerwendungKurz'
        created_at:
          type:
            - string
            - 'null'
          format: date-time
      required:
        - id
        - name
        - mime_type
        - size_bytes
        - is_public
        - ki_freigabe
        - ki_beschreibung
        - folder_id
        - public_url
        - thumb_url
        - verwendung
        - created_at
    FileListResponse:
      type: object
      properties:
        folders:
          type: array
          items:
            $ref: '#/components/schemas/FolderItem'
        files:
          type: array
          items:
            $ref: '#/components/schemas/FileItem'
      required:
        - folders
        - files
    LinkedAsset:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        kind:
          type: string
          enum:
            - logo
            - knowledge
        mime_type:
          type:
            - string
            - 'null'
        size_bytes:
          type:
            - number
            - 'null'
        usage_label:
          type: string
        manage_href:
          type: string
        download_url:
          type:
            - string
            - 'null'
        created_at:
          type:
            - string
            - 'null'
      required:
        - id
        - name
        - kind
        - mime_type
        - size_bytes
        - usage_label
        - manage_href
        - download_url
        - created_at
    LinkedAssetsResponse:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/LinkedAsset'
      required:
        - items
    FileUploadRequest:
      type: object
      properties:
        file:
          type: string
          format: binary
        name:
          type: string
        folder_id:
          type: string
          format: uuid
      required:
        - file
    PatchFileRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        folder_id:
          type:
            - string
            - 'null'
          format: uuid
        is_public:
          type: boolean
        ki_freigabe:
          type: boolean
        ki_beschreibung:
          type: string
          maxLength: 500
      description: >-
        Partial-Update einer Datei: umbenennen (name), verschieben (folder_id, null = Wurzel), öffentlichen Link
        an-/abschalten (is_public) oder den KI-Agenten freigeben (ki_freigabe, ki_beschreibung).
    AutomationMitDatei:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        enabled:
          type: boolean
        wartend:
          type: boolean
        wartend_verworfen:
          type: boolean
      required:
        - id
        - name
        - enabled
        - wartend
        - wartend_verworfen
    DateiVerwendungResponse:
      type: object
      properties:
        automationen:
          type: array
          items:
            $ref: '#/components/schemas/AutomationMitDatei'
        beileger:
          type: boolean
        ki_freigabe:
          type: boolean
        loeschen_gesperrt:
          type: boolean
      required:
        - automationen
        - beileger
        - ki_freigabe
        - loeschen_gesperrt
    FileDownloadResponse:
      type: object
      properties:
        url:
          type: string
      required:
        - url
    Folder:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        parent_id:
          type:
            - string
            - 'null'
          format: uuid
        created_at:
          type:
            - string
            - 'null'
          format: date-time
      required:
        - id
        - name
        - parent_id
        - created_at
    CreateFolderRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        parent_id:
          type:
            - string
            - 'null'
          format: uuid
      required:
        - name
      description: Legt einen Ordner an. parent_id weglassen oder null = Wurzel.
    PatchFolderRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        parent_id:
          type:
            - string
            - 'null'
          format: uuid
      description: >-
        Partial-Update eines Ordners: umbenennen (name) oder verschieben (parent_id, null = Wurzel). Verschieben in den
        eigenen Subtree wird mit 400 abgelehnt.
    OutboundAttachment:
      type: object
      properties:
        key:
          type: string
        kind:
          type: string
          enum:
            - image
            - pdf
            - audio
            - video
        name:
          type: string
        mime:
          type: string
        size:
          type: integer
      required:
        - key
        - kind
        - name
        - mime
        - size
  parameters: {}
paths:
  /v1/conversations:
    get:
      summary: Konversationen auflisten
      description: >-
        Liste der Konversationen des aktuellen Tenants, gefiltert nach Status/Tag/Sentiment/Channel/Assignee. Sortiert
        nach last_message_at DESC. Postfach-ACL gilt: Agents/Viewer sehen nur Konversationen ihrer sichtbaren Inboxes.
      tags:
        - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            enum:
              - open
              - snoozed
              - done
          required: false
          name: status
          in: query
        - schema:
            type: string
          required: false
          name: tag
          in: query
        - schema:
            type: string
            enum:
              - positive
              - neutral
              - negative
          required: false
          name: sentiment
          in: query
        - schema:
            type: string
          required: false
          name: intent
          in: query
        - schema:
            type: string
          required: false
          name: channel
          in: query
        - schema:
            type: string
            enum:
              - 'true'
              - '1'
          required: false
          name: mine
          in: query
        - schema:
            type: string
            format: uuid
          required: false
          name: assignee_user_id
          in: query
        - schema:
            type: string
            format: uuid
          required: false
          name: contact_id
          in: query
        - schema:
            type: string
            format: uuid
          required: false
          name: inbox_id
          in: query
        - schema:
            type: string
            enum:
              - 'true'
              - '1'
          required: false
          name: starred
          in: query
        - schema:
            type: string
            enum:
              - newest
              - oldest
              - waiting
          required: false
          name: sort
          in: query
        - schema:
            type: string
            pattern: ^\d{1,6}$
          required: false
          name: limit
          in: query
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationListResponse'
    post:
      summary: Konversation anlegen
      description: Legt eine neue Konversation für einen existierenden Kontakt an. Capability `conversations:write` erforderlich.
      tags:
        - Conversations
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateConversationRequest'
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '400':
          description: Validation failed or contact not found
  /v1/conversations/{id}:
    get:
      summary: Konversation abrufen
      description: Detail-Sicht inkl. Kontaktdaten + Inbox-Zuordnung.
      tags:
        - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '404':
          description: Conversation not found
    patch:
      summary: Konversation aktualisieren
      description: >-
        Partial-Update von Status, Assignee, Snooze-Time oder Tags. Status-Transitions feuern automation triggers
        (conversation.reopened, conversation.done, conversation.assigned, contact.tag_added/removed).
      tags:
        - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchConversationRequest'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '404':
          description: Conversation not found
  /v1/conversations/{id}/contact-panel:
    get:
      summary: Kontakt-Info-Sidebar (aggregiert)
      description: >-
        Aggregierter Daten-Drop für den Kontakt-Info-Drawer im Konversations-Detail-View: Kontakt (entschlüsselt) +
        Attribute + andere Konversationen des Kontakts + Automation-Runs dieser Konversation + interne Notizen + Anhänge
        (alle Konversationen des Kontakts) + Kontakt-Listen. Ein Request statt sieben. Capability `conversations:read`.
      tags:
        - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationContactPanel'
        '404':
          description: Conversation not found
  /v1/conversations/{id}/stop-automation:
    post:
      summary: Laufende KI-Automation beenden
      description: >-
        Stoppt den aktuellen Automation-Run auf der Konversation (state=active → idle). Pending-Actions werden geräumt.
        KI bleibt grundsätzlich bereit - bei einem neuen Inbound startet die Automation wieder normal
        (Superchat-Modell). Capability `conversations:write` erforderlich. Liefert 409 wenn keine aktive Session läuft.
      tags:
        - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Stopped
        '404':
          description: Conversation not found
        '409':
          description: No active AI session to stop
  /v1/conversations/{id}/read:
    post:
      summary: Konversation als gelesen markieren
      description: >-
        Setzt den last_read_at des aktuellen Users auf jetzt (Upsert auf conversation_reads). Danach gilt die
        Konversation als gelesen, bis eine neuere eingehende Nachricht eintrifft. Capability `conversations:read`
        erforderlich.
      tags:
        - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
  /v1/conversations/read-all:
    post:
      summary: Alle Konversationen als gelesen markieren
      description: >-
        Markiert alle nicht-gelöschten Konversationen des Tenants als gelesen für den aktuellen User (Set-basierter
        Upsert auf conversation_reads). Capability `conversations:read` erforderlich.
      tags:
        - Conversations
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
  /v1/conversations/{conversationId}/messages:
    post:
      summary: Nachricht senden
      description: >-
        Persistiert + dispatched eine Nachricht in einer Konversation. Outbound: Channel-Dispatcher wird angestoßen.
        Capability `conversations:write` erforderlich.
      tags:
        - Messages
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: conversationId
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMessageRequest'
      responses:
        '200':
          description: Sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        '400':
          description: Validation failed or conversation not found
  /v1/api-public/contact-attribute-definitions:
    get:
      summary: Kontakt-Attribut-Definitionen auflisten (Public-API)
      description: >-
        Alle Custom-Attribut-Definitionen des Workspaces (Feld-Metadaten: key/label/type/options/required/sort_order).
        Die Werte pro Kontakt liegen separat in `contacts.attributes`.
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicContactAttributeDefinitionsListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/contact-lists:
    get:
      summary: Kontaktlisten auflisten (Public-API)
      description: Alle Kontaktlisten (Segmente) des Tenants inkl. Mitglieder-Anzahl.
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicContactListsResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/contact-lists/{id}/members:
    post:
      summary: Kontakt zu Liste hinzufügen (Public-API)
      description: Fügt einen Kontakt der Liste hinzu (idempotent). Audit `actorType=api_token`.
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicAddListMemberBody'
      responses:
        '200':
          description: Hinzugefügt (added=1 neu, 0 = war schon drin)
        '400':
          description: Validation failed
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Contact list not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/contact-lists/{id}/members/{contactId}:
    delete:
      summary: Kontakt aus Liste entfernen (Public-API)
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Entfernt
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Contact list not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/contacts:
    get:
      summary: Kontakte auflisten (Public-API)
      description: >-
        Cursor-paginierte Liste. `?email=` matched per blind-index (HMAC) direkt im Index, ohne jede Row zu
        entschlüsseln. `?q=` sucht per Freitext über Name/E-Mail/Telefon (durchsucht die neuesten 1000 Kontakte, keine
        Cursor-Pagination) - ideal für Kontakt-Picker/Dropdowns. `?attribute=<key>` filtert auf ein gesetztes
        Custom-Attribut, zusammen mit `?attribute_value=<wert>` auf einen konkreten Wert. `?include=attributes` nimmt
        die Custom-Attribute mit in die Antwort - ohne das bleibt die Liste unverändert schlank. Beispiel für einen
        Partner-Abgleich: `?attribute=partner_code&attribute_value=PARTNER-7&include=attributes`.
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            description: 'Undurchsichtiger Cursor: next_cursor der vorigen Antwort unverändert zurückgeben.'
          required: false
          description: 'Undurchsichtiger Cursor: next_cursor der vorigen Antwort unverändert zurückgeben.'
          name: cursor
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          required: false
          name: limit
          in: query
        - schema:
            type: string
            format: email
          required: false
          name: email
          in: query
        - schema:
            type: string
            minLength: 1
            maxLength: 200
          required: false
          name: q
          in: query
        - schema:
            type: string
            minLength: 1
            maxLength: 80
            pattern: ^[A-Za-z0-9_]+$
          required: false
          name: attribute
          in: query
        - schema:
            type: string
            maxLength: 500
          required: false
          name: attribute_value
          in: query
        - schema:
            type: string
            enum:
              - attributes
          required: false
          name: include
          in: query
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicContactListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
    post:
      summary: Kontakt anlegen (Public-API)
      description: >-
        Erzeugt einen neuen Kontakt im Tenant. Email/Phone werden FLE-verschlüsselt persistiert + ein blind-index für
        Suche/Dedup angelegt. Audit-Eintrag mit `actorType=api_token`.
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicCreateContactBody'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicContactDetail'
        '400':
          description: Validation failed
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/contacts/{id}:
    get:
      summary: Kontakt abrufen (Public-API)
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicContactDetail'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Contact not found
        '429':
          description: Rate-Limit überschritten
    patch:
      summary: Kontakt aktualisieren (Public-API)
      description: >-
        Partial-Update von Name/E-Mail/Telefon/Consent + Custom-Attributen (null entfernt ein Attribut).
        FLE-verschlüsselt, Audit `actorType=api_token`. consent_status=opted_in wird ABGEWIESEN (409,
        code=consent_opted_out, field=consent_status), wenn der Kontakt bereits opted_out ist - eine Integration darf
        eine Abmeldung nicht per API aufheben, das kann nur ein Mensch im Dashboard.
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicPatchContactBody'
      responses:
        '200':
          description: Aktualisiert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicContactDetail'
        '400':
          description: Validation failed
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Contact not found
        '409':
          description: >-
            Kontakt ist opted_out (code=consent_opted_out, field=consent_status) - eine Integration darf eine Abmeldung
            nicht per API aufheben.
        '429':
          description: Rate-Limit überschritten
    delete:
      summary: Kontakt löschen (Public-API)
      description: >-
        Soft-Delete: der Kontakt wird ausgeblendet (deleted_at gesetzt), Konversationen bleiben erhalten. Audit
        `actorType=api_token`.
      tags:
        - Public API - Contacts
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Geloescht
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Contact not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/conversations/{id}:
    patch:
      summary: Konversation bearbeiten (Public-API)
      description: >-
        Partial-Update: Status ändern, zuweisen/entziehen, Snooze setzen, ins Postfach verschieben oder Tag
        setzen/entfernen. Feuert dieselben Webhook-Events + Automation-Trigger wie die Web-App.
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicPatchConversationBody'
      responses:
        '200':
          description: Aktualisiert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicConversationActionResult'
        '400':
          description: Validation failed
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Conversation not found
        '429':
          description: Rate-Limit überschritten
    delete:
      summary: Konversation löschen (Public-API)
      description: 'Soft-Delete: die Konversation wird ausgeblendet (deleted_at gesetzt).'
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Geloescht
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Conversation not found
        '429':
          description: Rate-Limit überschritten
    get:
      summary: Konversation abrufen (Public-API)
      description: >-
        Detail-Sicht inklusive der letzten 50 Nachrichten (inline). Ältere Nachrichten werden in Phase 2 über einen
        Cursor-Endpoint nachladbar.
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicConversationDetail'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Conversation not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/conversations/{id}/notes:
    post:
      summary: Interne Notiz hinzufügen (Public-API)
      description: Legt eine interne Notiz an der Konversation an - für den Kunden unsichtbar, wird nicht versendet.
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicAddNoteBody'
      responses:
        '201':
          description: Angelegt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicNote'
        '400':
          description: Validation failed
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Conversation not found
        '429':
          description: Rate-Limit überschritten
    get:
      summary: Interne Notizen auflisten (Public-API)
      description: Listet die internen Notizen einer Konversation (neueste zuerst).
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicNotesListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Conversation not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/conversations/{id}/notes/{noteId}:
    delete:
      summary: Interne Notiz löschen (Public-API)
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: noteId
          in: path
      responses:
        '200':
          description: Geloescht
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Note not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/conversations:
    get:
      summary: Konversationen auflisten (Public-API, Bearer-Token)
      description: >-
        Gibt die Konversationen des Tenants zurück, sortiert nach created_at DESC. Cursor-Pagination über `cursor`: den
        `next_cursor`-Wert der vorigen Antwort unverändert zurückgeben (undurchsichtig; ältere reine ISO-8601-Cursor
        werden weiter angenommen).
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            enum:
              - open
              - snoozed
              - done
          required: false
          name: status
          in: query
        - schema:
            type: string
            format: uuid
          required: false
          name: inbox_id
          in: query
        - schema:
            type: string
            format: uuid
          required: false
          name: contact_id
          in: query
        - schema:
            type: string
            description: 'Undurchsichtiger Cursor: next_cursor der vorigen Antwort unverändert zurückgeben.'
          required: false
          description: 'Undurchsichtiger Cursor: next_cursor der vorigen Antwort unverändert zurückgeben.'
          name: cursor
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          required: false
          name: limit
          in: query
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicConversationListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/files:
    get:
      summary: Dateien auflisten (Public-API)
      description: Ordner + Dateien im angegebenen Ordner (folder_id weglassen = Wurzel).
      tags:
        - Public API - Files
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFilesListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/files/upload:
    post:
      summary: Datei hochladen (Public-API)
      description: Multipart-Upload (Feld `file`, optional `name`, `folder_id`). Max 50 MB.
      tags:
        - Public API - Files
      security:
        - bearerAuth: []
      responses:
        '201':
          description: Hochgeladen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFile'
        '400':
          description: Ungültige Datei
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Plan-Speicherlimit erreicht
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/files/{id}/download:
    get:
      summary: Download-Link erzeugen (Public-API)
      tags:
        - Public API - Files
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicFileDownload'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: File not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/files/{id}:
    delete:
      summary: Datei löschen (Public-API)
      tags:
        - Public API - Files
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Geloescht
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: File not found
        '409':
          description: File still in use - an active automation, a pending run or the print-attachment list still needs this file.
        '429':
          description: Rate-Limit überschritten
        '503':
          description: File usage could not be checked right now. Please retry shortly.
  /v1/api-public/inboxes:
    get:
      summary: Postfächer auflisten (Public-API)
      description: >-
        Alle Postfächer (Inboxes) des Workspaces. Das Standard-Postfach hat `is_default=true`. Kein Sichtbarkeitsfilter
        - der API-Key sieht alle.
      tags:
        - Public API - Workspace
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicInboxesListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/me:
    get:
      summary: Workspace-Info abrufen (Public-API)
      description: 'Infos über den mit dem API-Key verbundenen Workspace (Tenant): id, Name, Plan.'
      tags:
        - Public API - Workspace
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicMe'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/members:
    get:
      summary: Workspace-Mitglieder auflisten (Public-API)
      description: >-
        Alle Mitglieder des Workspaces (user_id, Anzeigename, Rolle). KEINE E-Mail-Adresse (Vertragsänderung 2026-09-06,
        Audit AM F3b). Anzeigename ist serverseitig FLE-decrypted - der Aufrufer ist für DSGVO-konformen Umgang
        verantwortlich. Temporäre Plattform-Support-Zugriffe sind ausgefiltert.
      tags:
        - Public API - Workspace
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicMembersListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/conversations/{id}/messages:
    post:
      summary: Nachricht in Konversation senden (Public-API)
      description: >-
        Sendet eine outbound-Nachricht. Channel-Dispatcher wird angestoßen (SMTP/WhatsApp/Webchat). Audit-Eintrag mit
        `actorType=api_token` wird automatisch geschrieben.
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicSendMessageBody'
      responses:
        '201':
          description: Sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicMessage'
        '400':
          description: Validation failed
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Conversation not found
        '429':
          description: Rate-Limit überschritten
    get:
      summary: Nachrichten-Historie blättern (Public-API)
      description: >-
        Cursor-paginierte Nachrichten-Historie, neueste zuerst. Ohne `cursor` die neuesten `limit` Nachrichten (Default
        50); `next_cursor` der Antwort geht weiter in die Vergangenheit. Notizen sind nie enthalten.
      tags:
        - Public API - Conversations
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
        - schema:
            type: string
          required: false
          name: cursor
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
          required: false
          name: limit
          in: query
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicMessagesListResponse'
        '400':
          description: Ungültiger limit- oder cursor-Parameter
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Conversation not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/tags:
    get:
      summary: Tags auflisten (Public-API)
      description: >-
        Alle Tags des Tag-Dictionarys des Workspaces. `name` ist der stabile Slug-Key, `label` die Anzeige, `color` ein
        Paletten-Slug oder Hex.
      tags:
        - Public API - Workspace
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicTagsListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/webhooks:
    get:
      summary: Webhook-Subscriptions auflisten
      tags:
        - Public API - Webhooks
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Liste aller aktiven + disabled Subscriptions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionListResponse'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
    post:
      summary: Neue Webhook-Subscription anlegen
      tags:
        - Public API - Webhooks
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookSubscription'
      responses:
        '201':
          description: >-
            Subscription erzeugt. `secret` ist EINMALIG enthalten - bitte kopieren, der Klartext ist danach nicht mehr
            abrufbar.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionWithSecret'
        '400':
          description: Validation failed
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/webhooks/{id}:
    get:
      summary: Eine Subscription auslesen
      tags:
        - Public API - Webhooks
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Subscription not found
        '429':
          description: Rate-Limit überschritten
    patch:
      summary: Subscription updaten / Secret rotieren / disabilen
      tags:
        - Public API - Webhooks
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchWebhookSubscription'
      responses:
        '200':
          description: >-
            Updated Subscription. Bei `rotate_secret: true` enthält der Response zusätzlich `secret` (Schema
            `WebhookSubscriptionWithSecret`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscription'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
    delete:
      summary: Subscription löschen
      tags:
        - Public API - Webhooks
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Geloescht
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Subscription not found
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/webhooks/{id}/deliveries:
    get:
      summary: Letzte 50 Delivery-Versuche
      tags:
        - Public API - Webhooks
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Liste der letzten Deliveries (sortiert nach created_at DESC).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookDelivery'
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '429':
          description: Rate-Limit überschritten
  /v1/api-public/webhooks/{id}/test:
    post:
      summary: Test-Event feuern
      tags:
        - Public API - Webhooks
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: >-
            Erzeugt einen `webhook.test`-Event und queued ihn als Pending-Delivery. Der Scheduler liefert ihn in <30s
            aus.
        '401':
          description: API-Key fehlt oder ungültig
        '403':
          description: Workspace gesperrt/gelöscht/nicht verfügbar oder Modul für diesen Workspace deaktiviert
        '404':
          description: Subscription not found
        '429':
          description: Rate-Limit überschritten
  /v1/audit/events:
    get:
      summary: Audit-Events auflisten
      description: >-
        Cursor-paginierte Liste der Audit-Events des aktuellen Tenants, neueste zuerst (Kettenreihenfolge). `before` ist
        ein undurchsichtiger Cursor: `meta.next_before` der vorigen Antwort zurückgeben. Hash-Chain-Felder
        (prev_hash_b64, row_hash_b64) erlauben Tamper-Detection. Capability `audit:read` erforderlich.
      tags:
        - Audit
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: false
          name: limit
          in: query
        - schema:
            type: string
          required: false
          name: before
          in: query
        - schema:
            type: string
          required: false
          name: action_prefix
          in: query
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditEventsResponse'
  /v1/audit/verify:
    post:
      summary: Audit-Hash-Chain verifizieren
      description: >-
        Walked die per-tenant Audit-Chain in chronologischer Reihenfolge und rechnet jede Row-Hash neu nach. Liefert die
        erste Divergenz oder "intact". Bounded by `?max_rows=` (Default 5000, Hard-Cap 50000). Tooling für
        DSGVO/AI-Act-Audit-Reviews.
      tags:
        - Audit
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: false
          name: max_rows
          in: query
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditVerifyResponse'
  /v1/contacts:
    get:
      summary: Kontakte auflisten
      description: Liste der Kontakte des aktuellen Tenants, sortiert nach Anlage-Datum DESC. Limit 50.
      tags:
        - Contacts
      security:
        - bearerAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactListResponse'
    post:
      summary: Kontakt anlegen
      description: Legt einen neuen Kontakt an. Capability `contacts:edit` erforderlich.
      tags:
        - Contacts
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContactRequest'
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactDetail'
        '400':
          description: Validation failed
  /v1/files:
    get:
      summary: Ordner-Inhalt auflisten
      description: >-
        Listet Unterordner + Dateien im angegebenen Ordner (folder_id weglassen = Wurzel). Capability `files:read`.
        public_url ist nur bei öffentlichen Dateien gesetzt.
      tags:
        - Files
      security:
        - sessionCookie: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: false
          name: folder_id
          in: query
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileListResponse'
  /v1/files/linked:
    get:
      summary: Verknüpfte Assets auflisten
      description: >-
        Read-only-Aggregation der an anderer Stelle hochgeladenen Workspace-Assets (Workspace-Logo +
        KI-Wissens-Dokumente vom Typ Datei). Jedes Item trägt ein usage_label („Verwendet von") und einen manage_href
        zum Verwaltungsort. Diese Assets werden hier NICHT verwaltet. Capability `files:read`.
      tags:
        - Files
      security:
        - sessionCookie: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LinkedAssetsResponse'
  /v1/files/upload:
    post:
      summary: Datei hochladen
      description: >-
        Multipart-Upload (max 50MB). Felder: file (binär), optional name (Anzeigename), optional folder_id (Ziel-Ordner,
        sonst Wurzel). Capability `files:write`.
      tags:
        - Files
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/FileUploadRequest'
      responses:
        '200':
          description: Hochgeladen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileItem'
        '400':
          description: Validierung fehlgeschlagen / Datei zu groß
  /v1/files/{id}:
    patch:
      summary: Datei aktualisieren
      description: Umbenennen, verschieben oder öffentlichen Link an-/abschalten. Capability `files:write`.
      tags:
        - Files
      security:
        - sessionCookie: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchFileRequest'
      responses:
        '200':
          description: Aktualisiert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileItem'
        '400':
          description: Ziel-Ordner nicht gefunden
        '404':
          description: Datei nicht gefunden
    delete:
      summary: Datei löschen
      description: >-
        Soft-Delete + Storage-Blob-Löschung (best-effort). Gesperrt (409) während eine AKTIVE Automation, ein wartender
        Lauf oder die Beileger-Liste an der Datei hängt - siehe GET {id}/verwendung. Capability `files:write`.
      tags:
        - Files
      security:
        - sessionCookie: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Gelöscht
        '404':
          description: Datei nicht gefunden
        '409':
          description: Wird noch benutzt (aktive Automation, wartender Lauf oder Beileger)
        '503':
          description: Verwendung konnte gerade nicht geprüft werden - bitte kurz erneut versuchen
  /v1/files/{id}/verwendung:
    get:
      summary: Verwendung einer Datei ermitteln
      description: >-
        Woran hängt diese Datei (Automationen inkl. wartender Läufe, Beileger-Liste, KI-Freigabe) - für den
        Lösch-Dialog. `loeschen_gesperrt` ist dasselbe Urteil, das DELETE unten durchsetzt. Capability `files:read`.
      tags:
        - Files
      security:
        - sessionCookie: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DateiVerwendungResponse'
        '404':
          description: Datei nicht gefunden
  /v1/files/{id}/download:
    get:
      summary: Download-Link erzeugen
      description: Liefert eine kurzlebige (300s) Download-URL für die Datei. Capability `files:read`.
      tags:
        - Files
      security:
        - sessionCookie: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileDownloadResponse'
        '404':
          description: Datei nicht gefunden
  /v1/folders:
    post:
      summary: Ordner anlegen
      description: Legt einen Ordner an (parent_id weglassen = Wurzel). Capability `files:write`.
      tags:
        - Files
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateFolderRequest'
      responses:
        '200':
          description: Angelegt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '400':
          description: Eltern-Ordner nicht gefunden
  /v1/folders/{id}:
    patch:
      summary: Ordner aktualisieren
      description: >-
        Umbenennen oder verschieben. Verschieben in den eigenen Subtree wird mit 400 abgelehnt (Zyklus-Schutz).
        Capability `files:write`.
      tags:
        - Files
      security:
        - sessionCookie: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PatchFolderRequest'
      responses:
        '200':
          description: Aktualisiert
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Folder'
        '400':
          description: Ungültiges Ziel
        '404':
          description: Ordner nicht gefunden
    delete:
      summary: Ordner löschen
      description: >-
        Rekursiver Cascade-Soft-Delete: Ordner + alle Unterordner + enthaltene Dateien (Storage-Blobs best-effort).
        Capability `files:write`.
      tags:
        - Files
      security:
        - sessionCookie: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: id
          in: path
      responses:
        '200':
          description: Gelöscht
        '404':
          description: Ordner nicht gefunden
        '409':
          description: >-
            Ordner enthält noch benutzte Dateien (aktive Automation, wartender Lauf oder Beileger) - nichts wurde
            gelöscht
        '503':
          description: Verwendung konnte gerade nicht geprüft werden - bitte kurz erneut versuchen
  /v1/public/files/{token}:
    get:
      summary: Öffentliche Datei ausliefern
      description: >-
        Liefert eine öffentlich freigegebene Datei über einen permanenten, signierten Token aus. Der Tenant wird
        ausschließlich aus dem Token gezogen. Liefert nur is_public=true + nicht gelöschte Dateien; bei manipuliertem
        Token oder privater/gelöschter Datei kommt 404.
      tags:
        - Files
      security: []
      parameters:
        - schema:
            type: string
          required: true
          name: token
          in: path
      responses:
        '200':
          description: Datei-Inhalt (inline)
        '404':
          description: Nicht gefunden
  /v1/conversations/{conversationId}/attachments:
    post:
      summary: Anhang für eine ausgehende Nachricht hochladen
      description: >-
        Multipart-Upload (Feld `file`: Bild, PDF, Sprachnachricht oder Video; max 10 MB, Video 16 MB). Liefert eine
        Referenz, die beim Nachricht-Senden als `attachments[]` mitgegeben wird. Capability `conversations:write`
        erforderlich.
      tags:
        - Messages
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: conversationId
          in: path
      responses:
        '200':
          description: Hochgeladen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutboundAttachment'
        '400':
          description: Ungültige Datei (Typ/Größe) oder Konversation nicht gefunden
  /v1/outbound-attachments:
    post:
      summary: Anhang für die erste Nachricht einer neuen Konversation hochladen
      description: >-
        Wie `/v1/conversations/{conversationId}/attachments`, nur ohne bestehende Konversation (Neue Nachricht im
        Posteingang). Die Referenz wird beim ersten Senden mitgegeben; der Send prüft dort Mandanten-Prefix und
        Sichtbarkeit der Konversation. Capability `conversations:write` erforderlich.
      tags:
        - Messages
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Hochgeladen
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutboundAttachment'
        '400':
          description: Ungültige Datei (Typ/Größe)
webhooks: {}
