openapi: 3.1.0
info:
  title: Koivu Screening API
  version: '2026-10-15'
  summary: Sanktions-, PEP- und Adverse-Media-Signale mit Beweis.
  description: 'Die Koivu Screening API liefert **Signale, nie Entscheide**. Jede Antwort trägt `decision: "signal_only"`;

    die Entscheidung über einen Treffer bleibt bei Ihnen. Kein Treffer ist keine Freigabe.


    ## In fünf Minuten

    1. Schlüssel erhalten (`sk_test_…` zum Ausprobieren, `sk_live_…` für den Betrieb).

    2. `POST /v1/match` mit einem Namen aufrufen, Header `Authorization: Bearer <Schlüssel>`.

    3. `confidence_band` lesen: `high` = starkes Signal, `review` = prüfen. `explain` zeigt, warum.

    4. Bestände (Listen) über `POST /v1/upload` oder `POST /v1/batch` prüfen, Ergebnis über `/v1/batch/{job_id}/summary` und `/positives`.

    5. Dauerhaft überwachen: `POST /v1/monitors`.


    Ausführliche Anleitung mit curl und Python: Quickstart (`quickstart.md`).


    ## Begriffe

    * **Signal:** ein Hinweis, dass eine Person oder Organisation auf einer Liste steht oder dort sehr ähnlich geführt wird. Ein Signal ist nie
    der Entscheid.

    * **Bänder:** `high` (Name und Identitätsmerkmale stimmen überein) und `review` (Ähnlichkeit, zu prüfen). Unter `review` liegende Hinweise
    erscheinen nicht als Alarm.

    * **`suppressed_hits`:** Hinweise, die wir bewusst nicht als Alarm zählen (z. B. nur Namensähnlichkeit ohne Bindung an die Person). Sie sind
    sichtbar, damit nichts verborgen bleibt.

    * **Quellen und Nachweise:** Jeder Treffer nennt Quelle, Listenstand und Abrufzeit. Zu jeder Einzelprüfung gibt es einen signierten Nachweis
    (`/v1/evidence/{request_id}`).

    * **`checked_against`:** Liste der Quellen, gegen die geprüft wurde, mit Stand.


    ## Authentifizierung

    Jede Anfrage trägt den Schlüssel als `Authorization: Bearer sk_live_…` (oder `sk_test_…`). Cookies und andere Header gelten nicht.

    Schlüssel sind je Kundin und Zweck getrennt, können einen Ablauf haben und optional auf IP-Adressen eingeschränkt sein.

    Der Klartext wird nur einmal bei der Ausgabe gezeigt; wir speichern nur einen Hash. Rotation mit Überlappung (alter und neuer Schlüssel gelten

    parallel) und Widerruf veranlassen Sie bei uns. Fehlt der Schlüssel oder ist er widerrufen oder abgelaufen: `401`. Fehlt der Scope oder ist
    die Quell-IP nicht erlaubt: `403`.

    `sk_test_…`-Schlüssel haben engere Limits und markieren Antworten mit `Koivu-Environment: test`.


    ## Fehler

    Senden Sie `Koivu-Version: 2026-10-15`, antworten alle Fehler als `application/problem+json` (RFC 9457) mit stabilem `code`, `request_id`
    und bei

    Eingabefehlern `errors[]` (JSON-Pointer). Ohne diesen Header bleibt das bisherige Format `{"detail": …}`. Nennen Sie bei Rückfragen die `request_id`

    (auch im Header `X-Request-ID`). Der Fehlerkatalog steht unter `info.x-koivu-problem-catalog`.


    ## Wiederholbare Aufrufe

    `POST /v1/match` und `POST /v1/batch` akzeptieren `Idempotency-Key` (bis 255 Zeichen, 24 Stunden, je Schlüssel). Dieselbe Anfrage mit demselben
    Key liefert

    die gespeicherte Antwort (Header `Idempotent-Replayed: true`) und zählt nicht erneut; derselbe Key mit anderem Inhalt: `422 idempotency_key_reused`;

    läuft die erste Anfrage noch: `409 idempotency_in_progress`. Fehlerantworten werden nicht gespeichert.


    ## Limits

    Je Schlüssel gilt ein Minutenlimit und ein Tageskontingent (Vorgabe: 120 Anfragen pro Minute, vertraglich anpassbar). Jede Antwort trägt `X-RateLimit-Limit`,

    `X-RateLimit-Remaining` und `X-RateLimit-Reset`. Bei `429` gilt `Retry-After`. `POST /v1/match` zählt je Query, `POST /v1/batch` je Zeile.

    Grössen: `POST /v1/match` bis 512 KB, `POST /v1/batch` bis 32 MB, `POST /v1/upload` bis 50 MB.


    ## Versionierung

    Der Pfad `/v1` ist die Hauptversion. Bestehende Felder und Pfade ändern sich nicht; neue Felder kommen additiv hinzu. Clients ignorieren unbekannte
    Felder.

    '
  contact:
    name: Koivu
  x-koivu-problem-catalog:
  - code: validation_failed
    status: 422
    title: Eingabe ungültig
  - code: unauthorized
    status: 401
    title: Schlüssel fehlt, ist ungültig, widerrufen oder abgelaufen
  - code: forbidden_scope
    status: 403
    title: Der Schlüssel hat den Scope nicht
  - code: ip_not_allowed
    status: 403
    title: Zugriff von dieser IP-Adresse nicht erlaubt
  - code: forbidden
    status: 403
    title: Pfad nicht öffentlich
  - code: not_found
    status: 404
    title: Ressource nicht gefunden
  - code: method_not_allowed
    status: 405
    title: Methode nicht erlaubt
  - code: conflict
    status: 409
    title: Konflikt (z. B. veralteter Stand)
  - code: idempotency_in_progress
    status: 409
    title: Gleicher Idempotency-Key läuft noch
  - code: idempotency_key_reused
    status: 422
    title: Idempotency-Key mit anderen Parametern
  - code: payload_too_large
    status: 413
    title: Nutzlast zu gross
  - code: unsupported_format
    status: 415
    title: Format nicht unterstützt
  - code: rate_limited
    status: 429
    title: Minutenlimit erreicht
  - code: quota_exceeded
    status: 429
    title: Tageskontingent erreicht
  - code: list_stale_mandatory
    status: 503
    title: Pflichtliste nicht frisch
  - code: database_unavailable
    status: 503
    title: Datenbank nicht erreichbar
  - code: service_unavailable
    status: 503
    title: Dienst vorübergehend nicht verfügbar
  - code: internal_error
    status: 500
    title: Interner Fehler
servers:
- url: '{base_url}'
  description: Basis-URL der API; wir teilen sie Ihnen mit dem Schlüssel mit.
  variables:
    base_url:
      default: https://api.example.com
security:
- BearerAuth: []
tags:
- name: Screenings
  description: Einzelprüfung (`/v1/match`).
- name: Batches
  description: Bestand prüfen, Ergebnisse, Delta, Export.
- name: Sources
  description: Quellen und Listenstand.
- name: Evidence
  description: Signierte Nachweise und Berichte.
- name: Monitoring
  description: Dauerüberwachung (Monitore), Fälle und Webhooks.
- name: Account
  description: Konto, Plan, Scopes und Nutzung.
x-tagGroups:
- name: Prüfen
  tags:
  - Screenings
  - Batches
- name: Belege
  tags:
  - Evidence
  - Sources
- name: Überwachung und Konto
  tags:
  - Monitoring
  - Account
paths:
  /v1/match:
    post:
      operationId: matchScreening
      tags:
      - Screenings
      summary: Eine oder mehrere Personen/Organisationen prüfen (synchron)
      description: 'Prüft gegen alle aktiven Quellen (Sanktionen, PEP, Enforcement, Adverse-Media-Signale). Eingabe in der

        OpenSanctions-nahen Form (`queries` mit `properties`) **oder** in der Kurzform (`name`, `birth_year`, `country`).

        Bis zu 50 `queries` je Aufruf; jede wird für das Kontingent gewichtet. Scope `match`.


        Die Antwort liefert je Query `responses.<id>.results[]` zusammengeführte Personen mit `confidence_band` (`high` oder `review`),

        `explain` (Gründe mit Teilwerten), `source` und `records`. Ausgeblendete Hinweise stehen in `suppressed_hits`

        (nicht als Alarm gezählt). `checked_against` nennt Quellen und Listenstand, `evidence` ist der signierte Nachweis.


        **Haltung:** `decision` ist immer `signal_only`, `true_match` immer `false`. 0 Treffer ist **keine Freigabe**.

        Mit dem Header `Idempotency-Key` ist der Aufruf wiederholbar, ohne doppelt zu zählen.'
      parameters:
      - name: threshold
        in: query
        description: Mindest-Score 0,5 bis 1,0 (Body hat Vorrang). Standard 0,85.
        schema:
          type: number
          minimum: 0.5
          maximum: 1.0
      - name: limit
        in: query
        description: Treffer je Query, 1 bis 50 (Body hat Vorrang). Sanktions-Alarme werden nie durch `limit` gekappt.
        schema:
          type: integer
          minimum: 1
          maximum: 50
      - name: include_deceased
        in: query
        description: Verstorbene PEP/RCA als Treffer ausliefern (soft, `band_reason=deceased`).
        schema:
          type: boolean
          default: false
      - name: alert_policy
        in: query
        description: 'Alarm-Regeln je Quellenart: `standard` (Default), `strict`, `off`.'
        schema:
          $ref: '#/components/schemas/AlertPolicy'
      - $ref: '#/components/parameters/KoivuVersion'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MatchRequest'
            examples:
              kurzform:
                summary: Kurzform mit Geburtsjahr und Land
                value:
                  name: Vladimir Putin
                  birth_year: 1952
                  country: RU
                  limit: 5
              queries:
                summary: Mehrere Queries (OpenSanctions-nahe Form)
                value:
                  queries:
                    q1:
                      schema: Person
                      properties:
                        name:
                        - Bernard Madoff
                        birthDate:
                        - '1938'
                    q2:
                      schema: Person
                      properties:
                        name:
                        - Erika Mustermann
                        country:
                        - DE
                  threshold: 0.85
      responses:
        '200':
          description: 'Ergebnis je Query. Auch ohne Treffer 200 (dann `results: []`).'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchResponse'
              examples:
                mit_treffer:
                  summary: Beispielantwort (gekürzt)
                  value:
                    responses:
                      q1:
                        status: 200
                        results:
                        - id: ch-seco-49812
                          entity_id: ch-seco-49812
                          caption: Vladimir Putin
                          caption_original: null
                          score: 1.0
                          confidence_band: high
                          explain:
                            name:
                              query: Vladimir Putin
                              listed: Vladimir Putin
                              matched_via: primary
                              kind: exact
                              score: 1.0
                              tokens:
                                common:
                                - vladimir
                                - putin
                                query_only: []
                                listed_only: []
                            birth:
                              query: 1952
                              listed: '1952-10-07'
                              listed_years:
                              - 1952
                              result: confirmed
                            country:
                              query: RU
                              listed:
                              - RU
                              result: confirmed
                            id_number:
                              result: not_comparable
                            identity: confirmed
                            band_reason: name_exact_identity_confirmed
                            policy:
                              class: sanctions
                              mode: standard
                              action: keep
                          source:
                            id: ch_seco
                            label: SECO Sanktionsgesamtliste (SESAM)
                            country: CH
                            category: sanctions
                            list_date: '2026-10-07T08:00:09.759482+00:00'
                            list_date_basis: fetched
                            url: https://www.sesam.search.admin.ch/sesam-search-web/pages/search.xhtml
                            url_kind: list
                          records:
                          - source_id: ch_seco
                            entity_id: ch-seco-49812
                            list_version: ch-5f1315863aed
                            url: null
                          - source_id: jp_mof
                            entity_id: jp-mof-029-000001
                            list_version: jp-a20b1b4a428e
                            url: null
                          - source_id: ofac_sdn
                            entity_id: ofac-sdn-35096
                            list_version: ofac-b54e8f5535cd
                            url: https://sanctionssearch.ofac.treas.gov/Details.aspx?id=35096
                          deceased: null
                          death_date: null
                          is_rca: false
                          features:
                            birth_date: '1952-10-07'
                            birth_year: 1952
                            countries:
                            - RU
                            nationality: []
                            aliases:
                            - Putin Vladimir
                            - Vladimir Vladimirovich Putin
                          match: true
                          true_match: false
                          score_adjusted: 1.0
                        suppressed_hits: []
                        hit_count: 4
                        record_count: 14
                        suppressed:
                          deceased_pep: 0
                          phonetic_only: 0
                          over_limit: 1
                          unconfirmed_similar: 1
                        more_available: true
                        alert_policy: standard
                        total:
                          value: 4
                          relation: eq
                        query:
                          name: Vladimir Putin
                          schema: null
                          short_name: false
                          effective_threshold: 0.85
                          requested_threshold: 0.85
                          matcher_backend: rapidfuzz
                    matcher: mvp-rapidfuzz-v2
                    matcher_backend: rapidfuzz
                    config_hash: ee075db024bb7c71
                    stale: false
                    checked_against:
                      sources: 294
                      mandatory:
                      - id: ch_seco
                        label: SECO Sanktionsgesamtliste (SESAM)
                        list_date: '2026-10-07T08:00:09.759482+00:00'
                        list_date_basis: fetched
                      - id: eu_fsf
                        label: EU Financial Sanctions Files (FSF)
                        list_date: '2026-10-07T07:25:55.337128+00:00'
                        list_date_basis: fetched
                      oldest_list_date: '2026-10-07T07:25:55.337128+00:00'
                    suppressed:
                      over_limit: 1
                      unconfirmed_similar: 1
                    alert_policy: standard
                    threshold: 0.85
                    limit: 2
                    request_id: 34cfaf79-3cf1-4f8e-9c6a-55723dd979d1
                    audited_at: '2026-10-07T08:56:58.412326+00:00'
                    confidence_bands:
                      soft: 0.75-0.89
                      review: 0.90-0.97
                      high: '>=0.98'
                      auto_true_match: false
                    decision: signal_only
                    true_match: false
                ohne_treffer:
                  summary: Kein Treffer. Das ist ein Befund über Listenstand, keine Freigabe.
                  value:
                    responses:
                      q1:
                        status: 200
                        results: []
                        suppressed_hits: []
                        hit_count: 0
                        record_count: 0
                        suppressed: {}
                        more_available: false
                        alert_policy: standard
                        total:
                          value: 0
                          relation: eq
                    checked_against:
                      sources: 93
                      mandatory: []
                      oldest_list_date: '2026-10-05T03:00:00Z'
                    request_id: 3b0e1c64-8f55-4c1d-9a2f-0a77d1b9c2e4
                    decision: signal_only
                    true_match: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Invalid'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/match\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -H \"Content-Type: application/json\"\
          \ \\\n  -d '{\"name\": \"Vladimir Putin\", \"birth_year\": 1952, \"country\": \"RU\", \"limit\": 5}'"
  /v1/batch:
    post:
      operationId: createBatch
      tags:
      - Batches
      summary: Bestand prüfen (inline, bis 5000 Zeilen)
      description: 'Prüft eine Liste von Personen. Bis 50 Zeilen laufen synchron (`mode: sync`, Ergebnis in `responses`),

        mehr oder mit `async_mode: true` asynchron (`mode: async`, die Antwort enthält `poll`). Scope `batch`.

        Die Antwort ist immer `200` mit Body; bei asynchronen Läufen fragen Sie `poll` ab. Obergrenze 100 000 Zeilen je Aufruf.

        Für Dateien siehe `/v1/upload`. Mit dem Header `Idempotency-Key` ist der Aufruf wiederholbar, ohne einen zweiten Lauf zu starten.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchRequest'
            example:
              items:
              - name: Bernard Madoff
                birth_year: 1938
                country: US
              - name: Erika Mustermann
                country: DE
              threshold: 0.85
              limit: 10
              async_mode: true
      responses:
        '200':
          description: Job angelegt (async) oder fertig (sync).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchCreated'
              example:
                ok: true
                job_id: 6c0f2a6e-62d8-4a43-9b2e-7d2d3f6a9b10
                status: queued
                mode: async
                total: 2
                done: 0
                hit_count: 0
                poll: /v1/batch/6c0f2a6e-62d8-4a43-9b2e-7d2d3f6a9b10
                decision: signal_only
                true_match: false
                tenant_id: demo
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: 'Mehr als 5000 Zeilen (`detail: batch max 5000 items`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
        '422':
          $ref: '#/components/responses/Invalid'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      - $ref: '#/components/parameters/IdempotencyKey'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/batch\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -H \"Content-Type: application/json\"\
          \ \\\n  -d '{\"items\": [{\"name\": \"Bernard Madoff\", \"birth_year\": 1938, \"country\": \"US\"}, {\"name\": \"Erika Mustermann\"\
          , \"country\": \"DE\"}], \"threshold\": 0.85, \"limit\": 10, \"async_mode\": true}'"
  /v1/batch/{job_id}:
    get:
      operationId: getBatch
      tags:
      - Batches
      summary: Job-Status samt aller Items
      description: 'Liefert Status und **alle Items inline** (`items[]` mit Kurzsummary je Person). Für grosse Läufe ungeeignet:

        nutzen Sie `/summary`, `/positives` und `/breakdown`.'
      parameters:
      - $ref: '#/components/parameters/JobId'
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Job mit Items.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchJob'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/upload:
    post:
      operationId: uploadBatch
      tags:
      - Batches
      summary: Datei oder Text hochladen und asynchron prüfen
      description: 'CSV oder XLSX (Spalten werden über Header-Aliase erkannt) oder `paste` (Text, eine Person je Zeile).

        Grenzen: 50 MiB, 100 000 Zeilen, strikter Upload-Zähler je Schlüssel. Die Antwort ist immer asynchron (`mode: async`).

        Legacy-`.xls` wird mit 415 abgelehnt.'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: CSV oder XLSX
                paste:
                  type: string
                  description: 'Alternativ: Text, eine Person je Zeile'
                threshold:
                  type: number
                  minimum: 0.5
                  maximum: 1.0
                  default: 0.85
                limit:
                  type: integer
                  minimum: 1
                  maximum: 50
                  default: 10
      responses:
        '200':
          description: Job angelegt.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadAccepted'
              example:
                ok: true
                job_id: 6c0f2a6e-62d8-4a43-9b2e-7d2d3f6a9b10
                total: 1200
                mode: async
                threshold: 0.85
                limit: 10
                poll: /v1/batch/6c0f2a6e-62d8-4a43-9b2e-7d2d3f6a9b10
                note: Poll the poll URL for progress.
                tenant_id: demo
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: Datei zu gross, zu viele Zeilen oder Job über 5000 Zeilen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
        '415':
          description: Format nicht unterstützt (z. B. `.xls`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
        '422':
          $ref: '#/components/responses/Invalid'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/upload\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -F \"file=@bestand.csv\""
  /v1/batch/{job_id}/summary:
    get:
      operationId: getBatchSummary
      tags:
      - Batches
      summary: Triage-Zahlen und Herkunft des Jobs
      description: 'Personen mit Treffer (`positives`), nur Hinweise (`hints_only`), Zustände und Dispositionen, Review-Richtlinie

        (`review_policy`, enthält `four_eyes`), Herkunft (`result_version`, `code_commit`, `matcher_config_hash`,

        `checked_against`). Scope `batch`.

        '
      parameters:
      - $ref: '#/components/parameters/JobId'
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Zusammenfassung.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchSummary'
              example:
                job_id: 6c0f2a6e-62d8-4a43-9b2e-7d2d3f6a9b10
                status: completed
                total: 1200
                done: 1200
                positives: 14
                hints_only: 9
                hit_count: 21
                mode: async
                created_at: '2026-10-07T07:50:00+00:00'
                updated_at: '2026-10-07T07:58:41+00:00'
                dispositions:
                  open: 12
                  false_positive: 2
                  confirmed: 0
                states:
                  open: 12
                  proposed_false_positive: 0
                  proposed_confirmed: 0
                  escalated: 0
                  false_positive: 2
                  confirmed: 0
                awaiting_approval: 0
                review_policy:
                  four_eyes: true
                  min_reason_len: 10
                  identity: claimed_name
                result_version: 2
                code_commit: 3fa91c2
                matcher_config_hash: 7d2e51b0
                checked_against:
                  sources: 93
                  mandatory: []
                  oldest_list_date: '2026-10-05T03:00:00Z'
                decision: signal_only
                true_match: false
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/summary\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/positives:
    get:
      operationId: listBatchPositives
      tags:
      - Batches
      summary: Personen mit Treffer (Tabellenzeilen, Offset-Paginierung)
      description: 'Eine Zeile je Person mit `top_band`, `top_hit` (`Hit`), `hit_count`, `record_count`, Kategorien und Bearbeitungsstand.

        Standard: nur Alarme. `include=hints` ergänzt Hinweis-Zeilen (nur ausgeblendete Treffer), `include=hints_only` liefert nur diese.

        Paginierung über `offset` und `limit`.'
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 50
      - name: offset
        in: query
        schema:
          type: integer
          minimum: 0
          default: 0
      - name: q
        in: query
        description: Namens-Substring
        schema:
          type: string
      - name: disposition
        in: query
        schema:
          type: string
          enum:
          - open
          - false_positive
          - confirmed
      - name: state
        in: query
        schema:
          $ref: '#/components/schemas/TriageState'
      - name: min_score
        in: query
        schema:
          type: number
          minimum: 0
          maximum: 1
      - name: sort
        in: query
        schema:
          type: string
          enum:
          - score
          - name
          default: score
      - name: include
        in: query
        schema:
          type: string
          enum:
          - hints
          - hints_only
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Seite der Personenzeilen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositivesPage'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/positives\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/positives/{seq}:
    get:
      operationId: getBatchPositive
      tags:
      - Batches
      summary: Eine Person mit allen Treffern und Hinweisen
      description: Dieselbe Zeile wie in der Liste plus `hits[]` (alle, zusammengeführt, sortiert Band, dann Score) und `suppressed_hits[]` (höchstens
        20).
      parameters:
      - $ref: '#/components/parameters/JobId'
      - $ref: '#/components/parameters/Seq'
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Person im Detail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositiveRow'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/positives/<seq>\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/positives/{seq}/history:
    get:
      operationId: getBatchPositiveHistory
      tags:
      - Batches
      summary: Verlauf einer Person (append-only)
      description: Wer, wann, von nach, Begründung, Version. Älteste zuerst.
      parameters:
      - $ref: '#/components/parameters/JobId'
      - $ref: '#/components/parameters/Seq'
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Ereignisse.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositiveHistory'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/positives/<seq>/history\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/dispositions:
    get:
      operationId: getBatchDispositions
      tags:
      - Batches
      summary: Stand der Entscheide im Job
      description: Zähler je Disposition und Zustand, `awaiting_approval`, `review_policy`.
      parameters:
      - $ref: '#/components/parameters/JobId'
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Zähler.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchDispositions'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/dispositions\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/export:
    get:
      operationId: exportBatch
      tags:
      - Batches
      summary: Export (CSV oder JSON Lines)
      description: 'Eine Zeile je Person und Treffer mit lesbaren Spalten (Anfrage, Listenname, Stufe, Stufen-Grund, Quelle, Kategorie,

        Listenstand, URL, verstorben, Anzahl Einträge); technische IDs am Ende. CSV mit UTF-8-BOM; Zellen, die mit `= + - @`

        beginnen, werden mit `''` präfixt (Schutz vor Formel-Injektion in Tabellenkalkulationen).'
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: format
        in: query
        schema:
          type: string
          enum:
          - csv
          - jsonl
          default: csv
      - name: disposition
        in: query
        schema:
          type: string
          enum:
          - open
          - false_positive
          - confirmed
      - name: state
        in: query
        schema:
          $ref: '#/components/schemas/TriageState'
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Datei-Download.
          content:
            text/csv:
              schema:
                type: string
            application/x-ndjson:
              schema:
                type: string
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/export\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/breakdown:
    get:
      operationId: getBatchBreakdown
      tags:
      - Batches
      summary: Aufschlüsselung (Trichter, Stärke, Kategorie, Quellen, Überschneidungen)
      description: 'Zählt Personen nach Treffer-Vertrag §2 (zusammengeführt). Gleiche Operation unter dem Alias `/v1/jobs/{job_id}/breakdown`.

        Optionaler Block `adverse_media` (Kategorie `adverse_media_signal`, nur identitätsgebundene Verknüpfungen).

        '
      parameters:
      - $ref: '#/components/parameters/JobId'
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Aufschlüsselung.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Breakdown'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/breakdown\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/persons:
    get:
      operationId: listBatchPersons
      tags:
      - Batches
      summary: Drill-down auf Personen-IDs (Filter nach Stärke, Quelle, Kategorie, Listenanzahl)
      description: Alias `/v1/jobs/{job_id}/persons`. Offset-Paginierung; `person_id` ist die `seq` aus `/positives`.
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: strength
        in: query
        description: high,review,soft,below (Komma-Liste)
        schema:
          type: string
      - name: source
        in: query
        description: source_id (Komma-Liste)
        schema:
          type: string
      - name: category
        in: query
        description: Kategorie (Komma-Liste)
        schema:
          type: string
      - name: lists_min
        in: query
        schema:
          type: integer
          minimum: 0
      - name: lists_max
        in: query
        schema:
          type: integer
          minimum: 0
      - name: q
        in: query
        schema:
          type: string
      - name: limit
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 1000
          default: 100
      - name: offset
        in: query
        schema:
          type: integer
          minimum: 0
          default: 0
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Seite von Personen.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: '`job_id`, `total`, `count`, `offset`, `next_offset`, `person_ids[]`, `persons[]` (`person_id`, `seq`, `query_name`,
                  `strongest_band`, `source_count`, `sources`, `categories`), `filters`.'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/persons\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/delta:
    get:
      operationId: getBatchDelta
      tags:
      - Batches
      summary: Delta zweier Läufe (neu, weg, eskaliert, de-eskaliert, Quellenwechsel)
      description: 'Vergleicht diesen Lauf mit `against` (Standard: der per `rerun` verkettete Vorgänger). `warnings` nennt unfertige

        oder alte Läufe.'
      parameters:
      - $ref: '#/components/parameters/JobId'
      - name: against
        in: query
        description: Vorgänger-Job (UUID)
        schema:
          type: string
          format: uuid
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Delta.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchDelta'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/batch/<job_id>/delta\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/batch/{job_id}/rerun:
    post:
      operationId: rerunBatch
      tags:
      - Batches
      summary: Dieselben Zeilen neu prüfen (verketteter Job)
      description: Legt einen neuen asynchronen Job mit denselben Items an (`previous_job_id` gesetzt). Gleiche Grenzen wie `POST /v1/batch`.
      parameters:
      - $ref: '#/components/parameters/JobId'
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Neuer Job.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchCreated'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          description: Job grösser als 5000 Zeilen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
        '422':
          $ref: '#/components/responses/Invalid'
        '503':
          $ref: '#/components/responses/Unavailable'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/batch/<job_id>/rerun\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/sources/catalog:
    get:
      operationId: getSourcesCatalog
      tags:
      - Sources
      summary: Quellen-Katalog (schlanke Sicht)
      description: Id, Label, Land, Kategorie, Art, Listenstand (`list_date` + Basis), Entitätenzahl, URL. Scope `batch`.
      responses:
        '200':
          description: Katalog.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SourcesCatalog'
              example:
                count: 1
                sources:
                - id: ch_seco
                  label: SECO-Sanktionsliste (Schweiz)
                  country: CH
                  category: sanctions
                  kind: list
                  list_date: '2026-10-06T18:00:00Z'
                  list_date_basis: fetched
                  entity_count: 5300
                  url: https://www.seco.admin.ch/
                decision: signal_only
                true_match: false
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          $ref: '#/components/responses/Unavailable'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/sources/catalog\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/evidence/{request_id}:
    get:
      operationId: getEvidence
      tags:
      - Evidence
      summary: Signierter Nachweis zu einer Einzelprüfung
      description: 'Audit-Spur mit `list_versions` (inkl. `content_hash`, `fetched_at`), Kandidaten mit SHA-256, `input_sha256`,

        `code_commit`, `config_hash` und Ed25519-Signatur über kanonisches JSON. Scope `cases`.

        Der öffentliche Schlüssel zur Prüfung steht im Nachweis (`signature.public_key`).'
      parameters:
      - $ref: '#/components/parameters/RequestId'
      - name: format
        in: query
        schema:
          type: string
          enum:
          - json
          - markdown
          default: json
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Nachweis.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Evidence'
            text/markdown:
              schema:
                type: string
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '422':
          $ref: '#/components/responses/Invalid'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/evidence/<request_id>\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/evidence/{request_id}/report:
    get:
      operationId: getEvidenceReport
      tags:
      - Evidence
      summary: Beispielantwort (gekürzt)
      parameters:
      - $ref: '#/components/parameters/RequestId'
      - name: format
        in: query
        schema:
          type: string
          enum:
          - json
          - markdown
          default: json
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Bericht.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
            text/markdown:
              schema:
                type: string
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '422':
          $ref: '#/components/responses/Invalid'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/evidence/<request_id>/report\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/evidence/{request_id}/bundle:
    get:
      operationId: getEvidenceBundle
      tags:
      - Evidence
      summary: Audit-Bundle mit Reproduktionsschritten
      parameters:
      - $ref: '#/components/parameters/RequestId'
      - name: format
        in: query
        schema:
          type: string
          enum:
          - json
          - markdown
          default: json
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Bundle.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
            text/markdown:
              schema:
                type: string
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '422':
          $ref: '#/components/responses/Invalid'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/evidence/<request_id>/bundle\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/me:
    get:
      operationId: getMe
      tags:
      - Account
      summary: Beispielantwort (gekürzt)
      description: Mandant, Plan, Scopes, Kontingente und Präfix des verwendeten Schlüssels.
      responses:
        '200':
          description: Identität des Schlüssels.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Me'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/me\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/usage:
    get:
      operationId: getUsage
      tags:
      - Account
      summary: Nutzung und Kontingent
      responses:
        '200':
          description: Zähler.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/usage\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/monitors:
    get:
      operationId: listMonitors
      tags:
      - Monitoring
      summary: Monitore listen
      responses:
        '200':
          description: Monitore.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/monitors\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
    post:
      operationId: createMonitor
      tags:
      - Monitoring
      summary: Einzelperson dauerhaft überwachen (Monitor)
      description: 'Scope `monitor`. Cadence `on_list_update` (Standard), `hourly`, `daily`, `weekly`. Ein Monitor ist eine Einzelabfrage;

        gemeldet wird nur, was sich gegenüber der letzten Meldung geändert hat (neu, verändert, entfallen). Treffer des Monitors sind eine Kurzform.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MonitorCreate'
      responses:
        '200':
          description: Monitor angelegt.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: '`monitor`, `disclaimer`, `decision`.'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Invalid'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/monitors\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -H \"Content-Type: application/json\""
  /v1/monitors/{monitor_id}:
    get:
      operationId: getMonitor
      tags:
      - Monitoring
      summary: Monitor lesen
      responses:
        '200':
          description: Monitor.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/monitors/<monitor_id>\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
    patch:
      operationId: patchMonitor
      tags:
      - Monitoring
      summary: Monitor ändern
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MonitorCreate'
      responses:
        '200':
          description: Geändert.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '422':
          $ref: '#/components/responses/Invalid'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X PATCH \"$KOIVU_URL/v1/monitors/<monitor_id>\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -H \"Content-Type:\
          \ application/json\""
    delete:
      operationId: deleteMonitor
      tags:
      - Monitoring
      summary: Monitor löschen
      responses:
        '200':
          description: Gelöscht.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X DELETE \"$KOIVU_URL/v1/monitors/<monitor_id>\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
    parameters:
    - name: monitor_id
      in: path
      required: true
      schema:
        type: string
  /v1/webhooks:
    get:
      operationId: listWebhooksLegacy
      tags:
      - Monitoring
      summary: Webhooks listen (Altformat)
      responses:
        '200':
          description: Webhooks.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/webhooks\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
    post:
      operationId: createWebhookLegacy
      tags:
      - Monitoring
      summary: Webhook registrieren (Altformat)
      description: 'Header `X-Screening-Signature`, `X-Screening-Timestamp`, `X-Screening-Delivery-Id`; Signatur HMAC-SHA256 (hex) über

        `<timestamp>.<delivery-id>.<body>`; Events `monitor.hit`, `monitor.list_removed`, `list.updated`, `webhook.test`.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - url
              properties:
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                secret:
                  type: string
      responses:
        '200':
          description: Registriert.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Invalid'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/webhooks\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -H \"Content-Type: application/json\""
  /v1/webhooks/{webhook_id}/test:
    post:
      operationId: testWebhookLegacy
      tags:
      - Monitoring
      summary: Signiertes Test-Event senden (Altformat)
      parameters:
      - name: webhook_id
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/KoivuVersion'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - secret
              properties:
                secret:
                  type: string
      responses:
        '200':
          description: Gesendet.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '400':
          description: Test fehlgeschlagen.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegacyError'
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/webhooks/<webhook_id>/test\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -H \"Content-Type:\
          \ application/json\""
  /v1/webhooks/{webhook_id}/rotate:
    post:
      operationId: rotateWebhookLegacy
      tags:
      - Monitoring
      summary: Webhook-Secret rotieren (Altformat)
      parameters:
      - name: webhook_id
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Neues Secret.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/webhooks/<webhook_id>/rotate\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
  /v1/cases:
    get:
      operationId: listCases
      tags:
      - Monitoring
      summary: Fälle listen
      parameters:
      - name: status
        in: query
        schema:
          type: string
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Fälle.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
        '422':
          $ref: '#/components/responses/Invalid'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/cases\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
    post:
      operationId: createCase
      tags:
      - Monitoring
      summary: Fall aus einer Prüfung anlegen
      description: Scope `cases`. Mindestens eines von `request_id`, `match_ref`, `query_name`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                request_id:
                  type: string
                match_ref:
                  type: string
                query_name:
                  type: string
                monitor_id:
                  type: string
                assignee:
                  type: string
                note:
                  type: string
                actor_person:
                  type: string
                hit_summary:
                  type: object
                  additionalProperties: true
                list_versions:
                  type: object
                  additionalProperties: true
                screening_roles:
                  type: array
                  items:
                    type: string
                ubo_declaration:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Fall.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                description: '`case`, `decision`, `true_match`, `disclaimer`.'
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/Invalid'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X POST \"$KOIVU_URL/v1/cases\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -H \"Content-Type: application/json\""
  /v1/cases/{case_id}:
    get:
      operationId: getCase
      tags:
      - Monitoring
      summary: Fall lesen
      responses:
        '200':
          description: Fall.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/cases/<case_id>\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
    patch:
      operationId: patchCase
      tags:
      - Monitoring
      summary: Fall bearbeiten (Status, Zuständigkeit, Notiz)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Geändert.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Invalid'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      parameters:
      - $ref: '#/components/parameters/KoivuVersion'
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X PATCH \"$KOIVU_URL/v1/cases/<case_id>\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\" \\\n  -H \"Content-Type: application/json\""
    parameters:
    - name: case_id
      in: path
      required: true
      schema:
        type: string
  /v1/cases/{case_id}/events:
    get:
      operationId: listCaseEvents
      tags:
      - Monitoring
      summary: Verlauf eines Falls
      parameters:
      - name: case_id
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/KoivuVersion'
      responses:
        '200':
          description: Ereignisse.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
          headers:
            X-Request-ID:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/XRateLimitRemaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/XRateLimitReset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooMany'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
      - BearerAuth: []
      x-codeSamples:
      - lang: Shell
        label: curl
        source: "curl -s -X GET \"$KOIVU_URL/v1/cases/<case_id>/events\" \\\n  -H \"Authorization: Bearer $KOIVU_KEY\""
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live_… / sk_test_…
      description: 'Ihr Schlüssel als `Authorization: Bearer sk_live_…`. `sk_test_…` zum Ausprobieren (engere Limits).'
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: 'Eindeutiger Wert (1 bis 255 sichtbare ASCII-Zeichen). Die erste erfolgreiche Antwort wird 24 Stunden gespeichert und bei Wiederholung
        ausgeliefert (Header `Idempotent-Replayed: true`). Anderer Inhalt mit demselben Key: 422 `idempotency_key_reused`; läuft die erste Anfrage
        noch: 409 `idempotency_in_progress`.'
      schema:
        type: string
        minLength: 1
        maxLength: 255
    JobId:
      name: job_id
      in: path
      required: true
      description: Batch-Job-ID (UUID).
      schema:
        type: string
        format: uuid
    KoivuVersion:
      name: Koivu-Version
      in: header
      required: false
      description: Datumsbasierte API-Version, überschreibt den Mandanten-Pin je Aufruf. Unbekannte Felder müssen Clients ignorieren.
      schema:
        type: string
        pattern: ^\d{4}-\d{2}-\d{2}$
        examples:
        - '2026-10-15'
    RequestId:
      name: request_id
      in: path
      required: true
      description: '`request_id` aus der Antwort von `/v1/match` (UUID).'
      schema:
        type: string
    Seq:
      name: seq
      in: path
      required: true
      description: Zeilennummer der Person im Job (`batch_items.seq`); gilt nur je Job.
      schema:
        type: integer
        minimum: 0
  headers:
    XRequestId:
      description: Korrelations-ID der Anfrage, auch bei Fehlern. Bitte bei Rückfragen nennen.
      schema:
        type: string
    RetryAfter:
      description: Sekunden bis zum nächsten sinnvollen Versuch.
      schema:
        type: integer
    XRateLimitLimit:
      description: Anfragen pro Minute für diesen Schlüssel.
      schema:
        type: integer
    XRateLimitRemaining:
      description: Verbleibende Anfragen im laufenden Minutenfenster.
      schema:
        type: integer
    XRateLimitReset:
      description: Unix-Zeit, zu der das Minutenfenster endet.
      schema:
        type: integer
  responses:
    Unauthorized:
      description: 'Schlüssel fehlt, ist ungültig, widerrufen oder abgelaufen (401). Mit `Koivu-Version: 2026-10-15` als `application/problem+json`,
        sonst als `{"detail": …}`.'
      headers:
        X-Request-ID:
          $ref: '#/components/headers/XRequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
        application/json:
          schema:
            $ref: '#/components/schemas/LegacyError'
    Forbidden:
      description: 'Der Schlüssel hat den Scope nicht oder die Quell-IP ist nicht erlaubt (403). Mit `Koivu-Version: 2026-10-15` als `application/problem+json`,
        sonst als `{"detail": …}`.'
      headers:
        X-Request-ID:
          $ref: '#/components/headers/XRequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
        application/json:
          schema:
            $ref: '#/components/schemas/LegacyError'
    NotFound:
      description: 'Ressource nicht gefunden oder gehört einem anderen Mandanten (404). Mit `Koivu-Version: 2026-10-15` als `application/problem+json`,
        sonst als `{"detail": …}`.'
      headers:
        X-Request-ID:
          $ref: '#/components/headers/XRequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
        application/json:
          schema:
            $ref: '#/components/schemas/LegacyError'
    Invalid:
      description: 'Eingabe ungültig (422/400). Mit `Koivu-Version: 2026-10-15` als `application/problem+json`, sonst als `{"detail": …}`.'
      headers:
        X-Request-ID:
          $ref: '#/components/headers/XRequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
        application/json:
          schema:
            $ref: '#/components/schemas/LegacyError'
    TooMany:
      description: 'Minutenlimit oder Tageskontingent erreicht (429). Mit `Koivu-Version: 2026-10-15` als `application/problem+json`, sonst als
        `{"detail": …}`.'
      headers:
        X-Request-ID:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
        application/json:
          schema:
            $ref: '#/components/schemas/LegacyError'
    Unavailable:
      description: 'Dienst, Datenbank oder Pflichtliste vorübergehend nicht verfügbar (503). Mit `Koivu-Version: 2026-10-15` als `application/problem+json`,
        sonst als `{"detail": …}`.'
      headers:
        X-Request-ID:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
        application/json:
          schema:
            $ref: '#/components/schemas/LegacyError'
    Conflict:
      description: 'Konflikt: Idempotency-Key läuft noch (409) oder wurde mit anderen Parametern benutzt (422). Mit `Koivu-Version: 2026-10-15`
        als `application/problem+json`, sonst als `{"detail": …}`.'
      headers:
        X-Request-ID:
          $ref: '#/components/headers/XRequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
        application/json:
          schema:
            $ref: '#/components/schemas/LegacyError'
  schemas:
    AlertPolicy:
      type: string
      enum:
      - standard
      - strict
      - 'off'
      default: standard
      description: 'Alarm-Regeln je Quellenart. `standard`: Sanktionen immer; Tier 2 (PEP national/ausländisch, Enforcement) ab Namens-Score 0,95

        oder bestätigter Identität; Tier 3 (PEP subnational, RCA) nur bei Score ≥ 0,95 **und** bestätigter Identität. `off` = altes Verhalten.

        '
    BatchCreated:
      type: object
      description: 'Antwort von `POST /v1/batch` und `/rerun`. `sync`: mit `responses` (Map wie bei `/v1/match`); `async`: mit `poll`.'
      required:
      - ok
      - job_id
      - status
      - mode
      - total
      - decision
      - true_match
      properties:
        ok:
          type: boolean
        job_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
          - queued
          - running
          - completed
          - partial
          - failed
        mode:
          type: string
          enum:
          - sync
          - async
        total:
          type: integer
        done:
          type: integer
        hit_count:
          type: integer
        poll:
          type: string
          description: Pfad der Statusabfrage (`/v1/batch/{job_id}`).
        note:
          type: string
        responses:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/QueryResult'
        child_request_ids:
          type: array
          items:
            type: string
        job_audit_request_id:
          type: string
        latency_ms:
          type: integer
        previous_job_id:
          type: string
          format: uuid
        disclaimer:
          type: string
        matcher:
          type: string
        tenant_id:
          type: string
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
    BatchDelta:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
        previous_job_id:
          type: string
          format: uuid
        current:
          type: object
          additionalProperties: true
          description: '`job_id`, `status`, `total`, `created_at`, `finished_at`.'
        previous:
          type: object
          additionalProperties: true
        warnings:
          type: array
          items:
            type: string
            examples:
            - current_job_not_finished
            - previous_job_not_finished
            - current_job_legacy_format
        summary:
          type: object
          description: Zähler je Kategorie plus `unchanged`, `current_persons`, `previous_persons`.
          additionalProperties:
            type: integer
        new:
          type: array
          items:
            type: object
            additionalProperties: true
        gone:
          type: array
          items:
            type: object
            additionalProperties: true
        escalated:
          type: array
          items:
            type: object
            additionalProperties: true
        deescalated:
          type: array
          items:
            type: object
            additionalProperties: true
        changed_sources:
          type: array
          items:
            type: object
            additionalProperties: true
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
    BatchDispositions:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
        total_positives:
          type: integer
        dispositions:
          type: object
          additionalProperties:
            type: integer
        states:
          type: object
          additionalProperties:
            type: integer
        awaiting_approval:
          type: integer
        review_policy:
          type: object
          additionalProperties: true
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
    BatchJob:
      type: object
      description: Lauf mit **allen Items inline** (für grosse Läufe `/summary`, `/positives` und `/breakdown` nutzen).
      properties:
        ok:
          type: boolean
        job_id:
          type: string
          format: uuid
        tenant_id:
          type: string
        status:
          type: string
        mode:
          type: string
          enum:
          - sync
          - async
        total:
          type: integer
        done:
          type: integer
        hit_count:
          type: integer
        threshold:
          type:
          - number
          - 'null'
        created_at:
          type:
          - string
          - 'null'
          format: date-time
        finished_at:
          type:
          - string
          - 'null'
          format: date-time
        error:
          type:
          - string
          - 'null'
        result_version:
          type: integer
          description: 1 = ältere Kurzform, 2 = Treffer-Vertrag v2.
        code_commit:
          type:
          - string
          - 'null'
        matcher_config_hash:
          type:
          - string
          - 'null'
        checked_against:
          anyOf:
          - $ref: '#/components/schemas/CheckedAgainst'
          - type: 'null'
        items:
          type: array
          items:
            type: object
            properties:
              seq:
                type: integer
              request_id:
                type:
                - string
                - 'null'
              hit_count:
                type: integer
              status:
                type:
                - string
                - 'null'
              summary:
                type: object
                additionalProperties: true
        poll:
          type: string
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
    BatchRequest:
      type: object
      required:
      - items
      properties:
        items:
          type: array
          maxItems: 5000
          description: 'Zeilen wie `MatchRequest` (Kurzform): `name`, `birth_year`, `country`, `schema`, `id_number`.'
          items:
            type: object
            additionalProperties: true
        threshold:
          type: number
          minimum: 0.5
          maximum: 1.0
        limit:
          type: integer
          minimum: 1
          maximum: 50
        datasets:
          type: array
          items:
            type: string
        async_mode:
          type: boolean
          description: Auch bis 50 Zeilen asynchron erzwingen.
    BatchSummary:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
        status:
          type: string
        total:
          type: integer
        done:
          type: integer
        positives:
          type: integer
          description: Personen mit mindestens einem Alarm.
        hints_only:
          type: integer
          description: Personen nur mit Hinweisen (ohne Alarm).
        hit_count:
          type: integer
        mode:
          type: string
        created_at:
          type: string
        updated_at:
          type: string
        dispositions:
          type: object
          additionalProperties:
            type: integer
          description: Zähler `open`, `false_positive`, `confirmed`.
        states:
          type: object
          additionalProperties:
            type: integer
        awaiting_approval:
          type: integer
        review_policy:
          type: object
          description: Maschinenlesbare Richtlinie. Enthält `four_eyes`, `states`, `actions`, `reason_codes`, `min_reason_len`, `transitions`,
            `identity` (der Name ist angegeben, nicht per Login geprüft).
          additionalProperties: true
        result_version:
          type: integer
        code_commit:
          type:
          - string
          - 'null'
        matcher_config_hash:
          type:
          - string
          - 'null'
        checked_against:
          anyOf:
          - $ref: '#/components/schemas/CheckedAgainst'
          - type: 'null'
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
    Breakdown:
      type: object
      description: Aufschlüsselung eines Jobs (Personen nach Vertrag §2, nicht Rohdatensätze).
      required:
      - job_id
      - decision
      - true_match
      - funnel
      - by_strength
      - by_category
      - sources
      properties:
        job_id:
          type: string
        status:
          type:
          - string
          - 'null'
        decision:
          type: string
        true_match:
          type: boolean
        result_version:
          type: integer
          description: 1 = ältere Kurzform, 2 = Vertrag v2.
        code_commit:
          type:
          - string
          - 'null'
        matcher_config_hash:
          type:
          - string
          - 'null'
        checked_against:
          anyOf:
          - $ref: '#/components/schemas/CheckedAgainst'
          - type: 'null'
        funnel:
          type: object
          properties:
            total:
              type: integer
              description: Eingereichte Namen.
            done:
              type: integer
            with_hit:
              type: integer
            without_hit:
              type: integer
            hints_only:
              type: integer
            hits:
              type: integer
            hits_reported:
              type: integer
            truncated_persons:
              type: integer
            record_count:
              type: integer
        by_strength:
          type: array
          items:
            type: object
            properties:
              band:
                type: string
                enum:
                - high
                - review
                - soft
                - below
              label_de:
                type: string
              persons:
                type: integer
              hits:
                type: integer
        by_category:
          type: array
          items:
            type: object
            properties:
              category:
                type: string
              persons:
                type: integer
              hits:
                type: integer
        by_pep_category:
          type: array
          items:
            type: object
            properties:
              pep_category:
                type: string
              persons:
                type: integer
              hits:
                type: integer
        persons_by_source_count:
          type: array
          items:
            type: object
            properties:
              bucket:
                type: string
                description: 0, 1, 2, 3-5, 6+
              min:
                type:
                - integer
                - 'null'
              max:
                type:
                - integer
                - 'null'
              persons:
                type: integer
        sources:
          type: array
          items:
            type: object
            properties:
              source_id:
                type: string
              category:
                type: string
              persons:
                type: integer
              hits:
                type: integer
        overlaps:
          type: array
          items:
            type: object
            properties:
              categories:
                type: array
                items:
                  type: string
                minItems: 2
                maxItems: 2
              persons:
                type: integer
        adverse_media:
          anyOf:
          - type: object
            description: Additiver Block; nur identitätsgebundene Verknüpfungen.
            properties:
              category:
                type: string
                const: adverse_media_signal
              available:
                type: boolean
              persons:
                type: integer
              links:
                type: integer
              clusters:
                type: integer
              domains:
                type: array
                items:
                  type: object
                  properties:
                    domain:
                      type: string
                    links:
                      type: integer
              signal_only:
                type: boolean
              true_match:
                type: boolean
          - type: 'null'
    CheckedAgainst:
      type: object
      description: Wogegen geprüft wurde (Treffer-Vertrag §5).
      properties:
        sources:
          type: integer
          description: Anzahl Quellen im Index zum Zeitpunkt der Prüfung.
        mandatory:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              label:
                type:
                - string
                - 'null'
              list_date:
                type:
                - string
                - 'null'
                format: date-time
              list_date_basis:
                type:
                - string
                - 'null'
                enum:
                - published
                - fetched
                - null
        oldest_list_date:
          type:
          - string
          - 'null'
          format: date-time
    ConfidenceBand:
      type: string
      enum:
      - high
      - review
      - soft
      description: Stufe nach Treffer-Vertrag v2 §3. `high` ≥ 0,98 Namens-Score, `review` 0,90 bis 0,97, `soft` darunter. Nie ein Entscheid.
    Evidence:
      type: object
      description: 'Selbsttragender, signierter Nachweis (Ed25519 über kanonisches JSON). Enthält Eingabe-Hash, Kandidaten mit SHA-256,

        `list_versions` (inkl. `content_hash`, `fetched_at`), `code_commit`, `config_hash`. Der öffentliche Schlüssel steht im Nachweis.'
      properties:
        schema_version:
          type: integer
        generated_at:
          type: string
          format: date-time
        signed_scope:
          type: object
          description: 'Signierter Umfang: `kind`, `request_id`, `tenant_id`, `ts_utc`, `code_commit`, `config_hash`, `matcher_config_version`,
            `matcher`, `threshold_requested`, `effective_thresholds`, `list_versions`, Kandidaten, `input_sha256`.'
          additionalProperties: true
        hash_algorithm:
          type: string
          examples:
          - sha256
        canonicalization:
          type: string
          description: 'Kanonisierung des signierten JSON: `json.dumps(sort_keys=True, separators=('','','':''))`.'
        excluded_from_hash:
          type: array
          items:
            type: string
        report_hash:
          type: string
          description: '`sha256(canonical_json(signed_scope))`.'
        signature:
          type: object
          properties:
            algorithm:
              type: string
              examples:
              - ed25519
            status:
              type: string
              enum:
              - signed
              - unsigned
              description: '`signed`, oder `unsigned`, wenn der Dienst keinen Signierschlüssel hat. Ein unsignierter Nachweis ist kein Beleg.'
            public_key:
              type:
              - string
              - 'null'
              description: Hex. Öffentlicher Schlüssel zur Prüfung der Signatur.
            signature:
              type:
              - string
              - 'null'
              description: Base64.
            payload_sha256:
              type: string
        verify_hint:
          type: string
    Hit:
      type: object
      description: '**Ein Treffer, überall gleich** (Treffer-Vertrag v2 §1): `/v1/match`, Batch-Positives, Export, Alerts und Webhooks tragen
        dieselbe Form.

        Dubletten derselben gelisteten Person sind zu **einem** `Hit` zusammengeführt (§2); die Rohdatensätze stehen in `records`.

        Ein `Hit` ist ein Signal, kein Befund: `confidence_band` und `explain` erklären, warum er erscheint.

        '
      required:
      - entity_id
      - caption
      - score
      - confidence_band
      - explain
      - source
      - records
      properties:
        id:
          type: string
          description: Heute identisch zu `entity_id` (Altfeld, bleibt).
        entity_id:
          type: string
          examples:
          - ch-seco-12345
        match:
          type: boolean
          description: Altfeld des Matchers (Schwelle erreicht). Kein Entscheid.
        true_match:
          type: boolean
          const: false
          description: 'Immer `false`: Koivu stellt nie einen Treffer als bestätigt fest.'
        score_adjusted:
          type: number
          description: Altfeld, Score nach Boni.
        profile_id:
          type:
          - string
          - 'null'
          description: Zusammenführungs-Schlüssel (Treffer-Vertrag §2).
        also_on:
          type: array
          items:
            type: string
          description: Weitere Quellen derselben Person (Altfeld).
        risk_score:
          type:
          - number
          - 'null'
          description: Risikomodell (Zusatz, additiv); `risk_band`, `risk_reasons`, `risk_contributions`, `risk_model_version` folgen demselben
            Muster.
        risk_band:
          type:
          - string
          - 'null'
        caption:
          type: string
          description: Anzeigename.
        caption_original:
          type:
          - string
          - 'null'
          description: Originalschreibweise, wenn abweichend.
        score:
          type: number
          minimum: 0
          maximum: 1.5
          description: Ranking-Score; darf Boni enthalten (Geburtsjahr +0,05, ID +0,10). Die Stufe hängt nicht an den Boni.
        confidence_band:
          $ref: '#/components/schemas/ConfidenceBand'
        explain:
          $ref: '#/components/schemas/HitExplain'
        source:
          $ref: '#/components/schemas/HitSource'
        records:
          type: array
          description: Alle Listeneinträge dieser gelisteten Person.
          items:
            type: object
            properties:
              source_id:
                type: string
              entity_id:
                type: string
              list_version:
                type: string
              url:
                type:
                - string
                - 'null'
        deceased:
          type:
          - boolean
          - 'null'
          description: '`null` = unbekannt (nie `false`, wenn nicht berechnet).'
        death_date:
          type:
          - string
          - 'null'
        deceased_basis:
          type:
          - string
          - 'null'
          examples:
          - wikidata_p570
        pep_category:
          type:
          - string
          - 'null'
        pep_status:
          type:
          - string
          - 'null'
        is_rca:
          type:
          - boolean
          - 'null'
        features:
          type: object
          properties:
            birth_date:
              type:
              - string
              - 'null'
            birth_year:
              type:
              - integer
              - 'null'
            countries:
              type: array
              items:
                type: string
            nationality:
              type: array
              items:
                type: string
            aliases:
              type: array
              items:
                type: string
    HitExplain:
      type: object
      description: Gründe mit Teilwerten („Name exakt, Geburtsjahr passt, Land passt“).
      properties:
        name:
          type: object
          properties:
            query:
              type: string
            listed:
              type: string
              description: Der Name, der tatsächlich gepasst hat (nie das Skelett).
            matched_via:
              type: string
              enum:
              - primary
              - alias
            kind:
              type: string
              enum:
              - exact
              - near
              - reordered
              - partial
              - initials
              - transliteration
              - phonetic
              - split
            score:
              type: number
              description: Reiner Namens-Score ohne Boni.
            tokens:
              type: object
              properties:
                common:
                  type: array
                  items:
                    type: string
                query_only:
                  type: array
                  items:
                    type: string
                listed_only:
                  type: array
                  items:
                    type: string
        birth:
          type: object
          properties:
            query:
              type:
              - integer
              - string
              - 'null'
            listed:
              type:
              - string
              - 'null'
            listed_years:
              type: array
              items:
                type: integer
              description: Alle Geburtsjahre der Listung.
            result:
              type: string
              enum:
              - confirmed
              - not_comparable
              - conflict
        country:
          type: object
          properties:
            query:
              type:
              - string
              - 'null'
            listed:
              type: array
              items:
                type: string
            result:
              type: string
              enum:
              - confirmed
              - not_comparable
              - conflict
        id_number:
          type: object
          properties:
            result:
              type: string
              enum:
              - confirmed
              - not_comparable
              - conflict
        identity:
          type: string
          enum:
          - confirmed
          - not_checkable
          - conflict
        band_reason:
          type: string
          description: Code nach Treffer-Vertrag §3, z. B. `name_exact_identity_confirmed`, `name_near_unconfirmed`, `phonetic_only`, `country_conflict`,
            `deceased`, `unconfirmed_similar`, `low_risk_unconfirmed`.
        policy:
          type: object
          properties:
            class:
              type: string
              description: Quellenart, z. B. `sanctions`.
            mode:
              $ref: '#/components/schemas/AlertPolicy'
            action:
              type: string
              description: '`keep` oder `suppress` (bei ausgeblendeten Hinweisen).'
    HitSource:
      type: object
      required:
      - id
      - category
      properties:
        id:
          type: string
          examples:
          - ch_seco
        label:
          type: string
        country:
          type:
          - string
          - 'null'
        category:
          type: string
          description: Dieselben Kategorien wie `Breakdown.by_category` (`sanctions`, `pep_core`, `enforcement`, `adverse_media_signal`, `ubo`,
            `kyb`, `unknown`).
        list_date:
          type:
          - string
          - 'null'
          format: date-time
        list_date_basis:
          type:
          - string
          - 'null'
          enum:
          - published
          - fetched
          - null
        url:
          type:
          - string
          - 'null'
          description: Eintrag, sonst Liste; null wenn unbekannt.
        url_kind:
          type: string
          enum:
          - entry
          - list
          description: Ob `url` auf den Eintrag oder die Liste zeigt.
    LegacyError:
      type: object
      description: 'Fehlerformat ohne Opt-in (ohne Header `Koivu-Version: 2026-10-15`). Varianten: `{detail}` und

        `{detail, error, request_id}` (503/500). Mit dem Opt-in antworten alle Fehler als `Problem`.'
      properties:
        detail:
          description: Text oder Objekt `{code, message}`.
          oneOf:
          - type: string
          - type: object
            properties:
              code:
                type: string
              message:
                type: string
          - type: array
            items:
              type: object
              additionalProperties: true
        error:
          type: string
          description: z. B. `database_unavailable`, `internal_error`.
        request_id:
          type:
          - string
          - 'null'
    MatchRequest:
      type: object
      description: 'Entweder `queries` (OpenSanctions-nahe Form, höchstens 50) oder die Kurzform (`name` …). Grenzen: `limit` 1 bis 50, `threshold`
        0,5 bis 1,0.'
      properties:
        queries:
          type: object
          maxProperties: 50
          description: Map Query-ID auf Query.
          additionalProperties:
            type: object
            properties:
              schema:
                type: string
                examples:
                - Person
                - Organization
                description: Schema-Name (Person oder Organisation).
              properties:
                type: object
                additionalProperties:
                  type: array
                  items:
                    type: string
                description: z. B. `name`, `birthDate`, `country`, `idNumber`.
              id:
                type: string
        name:
          type: string
        schema:
          type: string
        datasets:
          type: array
          items:
            type: string
          description: Quellen-IDs; leer = alle aktiven.
        threshold:
          type: number
          minimum: 0.5
          maximum: 1.0
        limit:
          type: integer
          minimum: 1
          maximum: 50
        birth_year:
          type: integer
        country:
          type: string
          description: ISO-3166-Alpha-2
        id_number:
          type: string
        role:
          type: string
          description: Rolle nach Art. 13 Abs. 5 GwV-FINMA.
        roles:
          type: array
          items:
            type: string
        include_context:
          type: boolean
          default: false
          description: Adverse-Media-Kontextsignale ohne Personen-ID-Link separat als `context_signals[]` (nie Alert).
        include_deceased:
          type: boolean
          default: false
        alert_policy:
          $ref: '#/components/schemas/AlertPolicy'
        adverse_media_elevates_risk:
          type: boolean
          default: false
        ubo_declaration:
          type: object
          description: Erklärter wirtschaftlich Berechtigter (nie vom System ermittelt).
          required:
          - person_name
          - declared_at
          - declarant
          properties:
            person_name:
              type: string
            declared_at:
              type: string
              format: date
            declarant:
              type: string
        customer_type:
          type: string
        art7a_exemption:
          type: boolean
          default: false
        art7a_reason:
          type: string
        art7a_amount:
          type: number
          minimum: 0
        art7a_currency:
          type: string
    MatchResponse:
      type: object
      required:
      - responses
      - request_id
      - decision
      - true_match
      properties:
        responses:
          type: object
          description: Map Query-ID auf Ergebnis.
          additionalProperties:
            $ref: '#/components/schemas/QueryResult'
        matcher:
          type: string
        matcher_backend:
          type: string
        config_hash:
          type: string
        list_versions:
          type: object
          additionalProperties: true
          description: Quelle auf Version, Stand, `content_hash`.
        stale:
          type: boolean
          description: Eine Pflichtliste ist nicht frisch.
        list_freshness:
          type: object
          additionalProperties: true
        checked_against:
          $ref: '#/components/schemas/CheckedAgainst'
        suppressed:
          type: object
          additionalProperties:
            type: integer
        alert_policy:
          $ref: '#/components/schemas/AlertPolicy'
        record_count:
          type: integer
        threshold:
          type: number
        limit:
          type: integer
        latency_ms:
          type: integer
        request_id:
          type: string
          description: Schlüssel für `/v1/evidence/{request_id}`.
        audited_at:
          type: string
          format: date-time
        effective_thresholds:
          type: object
          additionalProperties: true
        evidence:
          $ref: '#/components/schemas/Evidence'
        confidence_bands:
          type: object
          additionalProperties: true
        disclaimer:
          type: string
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
        screening_roles:
          type: array
          items:
            type: string
        customer_country_risk:
          type: object
          additionalProperties: true
    Me:
      type: object
      properties:
        tenant_id:
          type: string
        email:
          type:
          - string
          - 'null'
        name:
          type:
          - string
          - 'null'
        plan:
          type: string
        status:
          type: string
        scopes:
          type: array
          items:
            type: string
            examples:
            - match
            - batch
            - cases
            - monitor
            - lists
            - keys:manage
        quota_per_day:
          type: integer
        quota_per_minute:
          type: integer
        key_prefix:
          type: string
        created_at:
          type:
          - string
          - 'null'
          format: date-time
    MonitorCreate:
      type: object
      description: 'Monitor (Einzelabfrage). `query` oder `name` ist Pflicht. `cadence`: `on_list_update`, `hourly`, `daily`, `weekly`.'
      properties:
        name:
          type: string
        query:
          type: string
        entity_ref:
          type: string
        schema:
          type: string
        birth_year:
          type: integer
        country:
          type: string
        datasets:
          type: array
          items:
            type: string
        threshold:
          type: number
          default: 0.85
        cadence:
          type: string
          enum:
          - on_list_update
          - hourly
          - daily
          - weekly
          default: on_list_update
        active:
          type: boolean
        webhook_url:
          type: string
          format: uri
        webhook_secret:
          type: string
          writeOnly: true
        meta:
          type: object
          additionalProperties: true
    PositiveHistory:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
        seq:
          type: integer
        state:
          $ref: '#/components/schemas/TriageState'
        version:
          type: integer
        events:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              action:
                type: string
              from_state:
                type: string
              to_state:
                type: string
              reviewer:
                type:
                - string
                - 'null'
              reason_code:
                type:
                - string
                - 'null'
              reason_text:
                type:
                - string
                - 'null'
              version:
                type: integer
              at:
                type: string
                format: date-time
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
    PositiveRow:
      type: object
      description: Eine Person (Treffer-Vertrag §4). Im Detail zusätzlich `hits[]` und `suppressed_hits[]`.
      properties:
        seq:
          type: integer
        query:
          type: object
          description: Aus der Anlieferung.
          properties:
            name:
              type: string
            birth_year:
              type:
              - integer
              - 'null'
            country:
              type:
              - string
              - 'null'
            schema:
              type:
              - string
              - 'null'
        top_band:
          anyOf:
          - $ref: '#/components/schemas/ConfidenceBand'
          - type: 'null'
          description: Stärkstes Band über alle Treffer; `null` bei reinen Hinweis-Zeilen.
        top_hit:
          anyOf:
          - $ref: '#/components/schemas/Hit'
          - type: 'null'
        hit_count:
          type: integer
          description: Zusammengeführte Personen.
        record_count:
          type: integer
        categories_top:
          type: array
          items:
            type: string
        categories_other:
          type: array
          items:
            type: string
        sources_top:
          type: array
          items:
            type: string
        sources_other:
          type: array
          items:
            type: string
        suppressed:
          type: object
          additionalProperties:
            type: integer
        alert:
          type: boolean
          description: '`false` bei Hinweis-Zeilen.'
        hint_count:
          type: integer
        hint_reasons:
          type: object
          additionalProperties:
            type: integer
        disposition:
          type: string
          enum:
          - open
          - false_positive
          - confirmed
        state:
          $ref: '#/components/schemas/TriageState'
        version:
          type: integer
        reviewer:
          type:
          - string
          - 'null'
        reason_code:
          type:
          - string
          - 'null'
        reason_text:
          type:
          - string
          - 'null'
        hits:
          type: array
          items:
            $ref: '#/components/schemas/HitSummary'
        suppressed_hits:
          type: array
          maxItems: 20
          items:
            $ref: '#/components/schemas/Hit'
        result_version:
          type: integer
    PositivesPage:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
        count:
          type: integer
        next_offset:
          type:
          - integer
          - 'null'
        items:
          type: array
          items:
            $ref: '#/components/schemas/PositiveRow'
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
    Problem:
      type: object
      description: Fehler nach RFC 9457 (`application/problem+json`) mit stabilem `code`. Fehlerantworten tragen immer `X-Request-ID`.
      required:
      - type
      - title
      - status
      - code
      properties:
        type:
          type: string
          format: uri
          description: Stabile Kennung `urn:koivu:problem:<code>`.
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        code:
          type: string
          description: Stabiler Code, snake_case. Katalog siehe `info.x-koivu-problem-catalog`.
          enum:
          - validation_failed
          - unauthorized
          - forbidden_scope
          - ip_not_allowed
          - forbidden
          - not_found
          - method_not_allowed
          - conflict
          - idempotency_in_progress
          - idempotency_key_reused
          - payload_too_large
          - unsupported_format
          - rate_limited
          - quota_exceeded
          - list_stale_mandatory
          - database_unavailable
          - service_unavailable
          - internal_error
        request_id:
          type: string
        retry_after:
          type: integer
          description: Sekunden
        errors:
          type: array
          description: Eingabefehler je Feld.
          items:
            type: object
            required:
            - pointer
            - code
            properties:
              pointer:
                type: string
                description: JSON-Pointer auf das Feld
              code:
                type: string
              detail:
                type: string
    QueryResult:
      type: object
      description: Ergebnis einer Query.
      properties:
        status:
          type: integer
          examples:
          - 200
        results:
          type: array
          items:
            $ref: '#/components/schemas/Hit'
        suppressed_hits:
          type: array
          maxItems: 20
          description: Von den Alarm-Regeln ausgeblendete Hinweise (gleiche Form wie `Hit`). Zählen **nicht** als Treffer.
          items:
            $ref: '#/components/schemas/Hit'
        hit_count:
          type: integer
          description: Zusammengeführte Personen vor der Kappung durch `limit`.
        record_count:
          type: integer
          description: Rohdatensätze hinter den Treffern.
        suppressed:
          type: object
          additionalProperties:
            type: integer
          description: Zähler, z. B. `deceased_pep`, `phonetic_only`, `over_limit`.
        more_available:
          type: boolean
        alert_policy:
          $ref: '#/components/schemas/AlertPolicy'
        total:
          type: object
          properties:
            value:
              type: integer
            relation:
              type: string
        query:
          type: object
          additionalProperties: true
        context_signals:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Nur mit `include_context=true`; `context_only=true`, `person_linked=false`.
        disclaimer:
          type: string
    SourcesCatalog:
      type: object
      properties:
        count:
          type: integer
        sources:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              label:
                type: string
              country:
                type:
                - string
                - 'null'
              category:
                type: string
              kind:
                type: string
              list_date:
                type:
                - string
                - 'null'
                format: date-time
              list_date_basis:
                type:
                - string
                - 'null'
                enum:
                - published
                - fetched
                - null
              entity_count:
                type:
                - integer
                - 'null'
              url:
                type:
                - string
                - 'null'
        decision:
          type: string
          const: signal_only
        true_match:
          type: boolean
          const: false
    TriageState:
      type: string
      enum:
      - open
      - proposed_false_positive
      - proposed_confirmed
      - escalated
      - false_positive
      - confirmed
      description: Bearbeitungsstand einer Person im Lauf. Der Entscheid bleibt Handlung der Kundin.
    UploadAccepted:
      type: object
      required:
      - ok
      - job_id
      - total
      - mode
      - poll
      properties:
        ok:
          type: boolean
        job_id:
          type: string
          format: uuid
        total:
          type: integer
        mode:
          type: string
          const: async
        threshold:
          type: number
        limit:
          type: integer
        poll:
          type: string
        note:
          type: string
        disclaimer:
          type: string
        matcher:
          type: string
        tenant_id:
          type: string
    HitSummary:
      type: object
      description: Kurzform eines Treffers in Tabellenzeilen. `explain`, `source`, `records`, `features` sind hier `null`; die Volldaten stehen
        in `top_hit` und im Detail der Person.
      required:
      - entity_id
      - caption
      - score
      - confidence_band
      properties:
        id:
          type: string
          description: Heute identisch zu `entity_id` (Altfeld, bleibt).
        entity_id:
          type: string
          examples:
          - ch-seco-12345
        match:
          type: boolean
          description: Altfeld des Matchers (Schwelle erreicht). Kein Entscheid.
        true_match:
          type: boolean
          const: false
          description: 'Immer `false`: Koivu stellt nie einen Treffer als bestätigt fest.'
        score_adjusted:
          type: number
          description: Altfeld, Score nach Boni.
        profile_id:
          type:
          - string
          - 'null'
          description: Zusammenführungs-Schlüssel (Treffer-Vertrag §2).
        also_on:
          type: array
          items:
            type: string
          description: Weitere Quellen derselben Person (Altfeld).
        risk_score:
          type:
          - number
          - 'null'
          description: Risikomodell (Zusatz, additiv); `risk_band`, `risk_reasons`, `risk_contributions`, `risk_model_version` folgen demselben
            Muster.
        risk_band:
          type:
          - string
          - 'null'
        caption:
          type: string
          description: Anzeigename.
        caption_original:
          type:
          - string
          - 'null'
          description: Originalschreibweise, wenn abweichend.
        score:
          type: number
          minimum: 0
          maximum: 1.5
          description: Ranking-Score; darf Boni enthalten (Geburtsjahr +0,05, ID +0,10). Die Stufe hängt nicht an den Boni.
        confidence_band:
          $ref: '#/components/schemas/ConfidenceBand'
        explain:
          oneOf:
          - $ref: '#/components/schemas/HitExplain'
          - type: 'null'
        source:
          oneOf:
          - $ref: '#/components/schemas/HitSource'
          - type: 'null'
        records:
          oneOf:
          - type: array
            description: Alle Listeneinträge dieser gelisteten Person.
            items:
              type: object
              properties:
                source_id:
                  type: string
                entity_id:
                  type: string
                list_version:
                  type: string
                url:
                  type:
                  - string
                  - 'null'
          - type: 'null'
        deceased:
          type:
          - boolean
          - 'null'
          description: '`null` = unbekannt (nie `false`, wenn nicht berechnet).'
        death_date:
          type:
          - string
          - 'null'
        deceased_basis:
          type:
          - string
          - 'null'
          examples:
          - wikidata_p570
        pep_category:
          type:
          - string
          - 'null'
        pep_status:
          type:
          - string
          - 'null'
        is_rca:
          type:
          - boolean
          - 'null'
        features:
          oneOf:
          - type: object
            properties:
              birth_date:
                type:
                - string
                - 'null'
              birth_year:
                type:
                - integer
                - 'null'
              countries:
                type: array
                items:
                  type: string
              nationality:
                type: array
                items:
                  type: string
              aliases:
                type: array
                items:
                  type: string
          - type: 'null'
