# Koivu Screening API — Quickstart

Diese Anleitung bringt Sie in unter einer Stunde von «kein Zugang» zu einer produktiven Anbindung.
Die vollständige Referenz steht in [`openapi.yaml`](openapi.yaml) (Ansicht: [`index.html`](index.html)).

**Grundsatz:** Die API liefert **Signale, nie Entscheide.** Jede Antwort trägt `decision: "signal_only"`. Ob ein Treffer zutrifft, entscheiden Sie.
**Kein Treffer ist keine Freigabe**: Er heisst nur, dass die geprüften Listen in ihrem Stand nichts Passendes enthalten.

## 1. Zugang

Sie erhalten von uns einen Schlüssel und die Basis-URL:

| Schlüssel | Zweck |
|---|---|
| `sk_test_…` | Ausprobieren und Integrieren (engere Limits, Antworten tragen `Koivu-Environment: test`) |
| `sk_live_…` | Betrieb |

Der Klartext wird nur einmal übergeben und bei uns nur als Hash gespeichert. Bewahren Sie ihn wie ein Passwort auf (Secret-Store, nicht im Code).
Optional beschränken wir einen Schlüssel auf Ihre festen IP-Adressen (CIDR); Rotation mit Überlappung und Widerruf veranlassen Sie bei uns.

```bash
export KOIVU_URL="<Basis-URL, die Sie von uns erhalten haben>"
export KOIVU_KEY="sk_test_…"
```

Jede Anfrage trägt den Schlüssel im Header `Authorization: Bearer …`. Cookies und andere Header gelten nicht.

## 2. Erste Prüfung

```bash
curl -s -X POST "$KOIVU_URL/v1/match" \
  -H "Authorization: Bearer $KOIVU_KEY" \
  -H "Content-Type: application/json" \
  -H "Koivu-Version: 2026-10-15" \
  -d '{"name": "Vladimir Putin", "birth_year": 1952, "country": "RU", "limit": 3}'
```

Aufbau der Antwort (gekürzt):

```json
{
  "decision": "signal_only",
  "request_id": "…",
  "responses": {
    "q1": {
      "status": 200,
      "results": [
        {
          "caption": "Vladimir Putin",
          "confidence_band": "high",
          "score": 1.0,
          "explain": {"name": {"kind": "exact"}, "birth": {"result": "confirmed"}, "country": {"result": "confirmed"}},
          "source": {"id": "ch_seco", "label": "SECO Sanktionsgesamtliste (SESAM)", "list_date": "2026-10-07T08:00:09+00:00"}
        }
      ],
      "suppressed_hits": []
    }
  },
  "checked_against": {"sources": 0, "mandatory": []},
  "evidence": {"signature": {"status": "signed"}}
}
```

So lesen Sie sie:

* **`results[]`** sind zusammengeführte Personen. **`confidence_band`**: `high` = Name und Identitätsmerkmale stimmen überein, `review` = ähnlich, bitte prüfen.
* **`explain`** nennt die Gründe mit Teilwerten (Name exakt oder ähnlich, Geburtsjahr passt oder nicht, Land passt oder nicht).
* **`source`** nennt Liste, Listenstand und Abrufzeit. Das ist Ihr Beleg.
* **`suppressed_hits`** sind Hinweise, die wir bewusst nicht als Alarm zählen (z. B. nur Namensähnlichkeit). Sie sind sichtbar, damit nichts verborgen bleibt.
* **`checked_against`** sagt, gegen welche Quellen in welchem Stand geprüft wurde.
* **`evidence`** ist der signierte Nachweis. Den vollständigen holen Sie mit `GET /v1/evidence/{request_id}`.

Mehrere Personen auf einmal (bis 50): Feld `queries` statt `name`, siehe Referenz `POST /v1/match`.

## 3. Python

```python
import os, uuid, requests

BASE = os.environ["KOIVU_URL"].rstrip("/")
S = requests.Session()
S.headers.update({
    "Authorization": f"Bearer {os.environ['KOIVU_KEY']}",
    "Koivu-Version": "2026-10-15",          # Fehler als problem+json
})

def screen(name, birth_year=None, country=None):
    r = S.post(f"{BASE}/v1/match",
               json={"name": name, "birth_year": birth_year, "country": country},
               headers={"Idempotency-Key": str(uuid.uuid4())},   # Wiederholung zählt nicht doppelt
               timeout=60)
    if r.status_code == 429:
        raise RuntimeError(f"Limit erreicht, warten {r.headers.get('Retry-After')} s")
    r.raise_for_status()
    return r.json()

res = screen("Vladimir Putin", 1952, "RU")
for q, body in res["responses"].items():
    for hit in body["results"]:
        print(q, hit["confidence_band"], hit["caption"], hit["source"]["label"])
    print(q, "ausgeblendet:", len(body["suppressed_hits"]))
```

Wiederholungen: Nur bei `429`, `503` und Netzwerkfehlern wiederholen, mit `Retry-After` und demselben `Idempotency-Key`.
Der zweite Aufruf liefert die gespeicherte Antwort (`Idempotent-Replayed: true`) und zählt nicht erneut.

## 4. Bestand prüfen (Datei)

```bash
# CSV mit Kopfzeile (Spalten wie name, vorname/nachname, geburtsjahr, land werden über Aliase erkannt)
curl -s -X POST "$KOIVU_URL/v1/upload" \
  -H "Authorization: Bearer $KOIVU_KEY" \
  -F "file=@bestand.csv"
# → {"ok": true, "job_id": "…", "mode": "async", "poll": "/v1/batch/…"}

JOB=<job_id>
curl -s "$KOIVU_URL/v1/batch/$JOB/summary"    -H "Authorization: Bearer $KOIVU_KEY"   # Fortschritt und Zähler
curl -s "$KOIVU_URL/v1/batch/$JOB/positives"  -H "Authorization: Bearer $KOIVU_KEY"   # Personen mit Alarm
curl -s "$KOIVU_URL/v1/batch/$JOB/export?format=csv" -H "Authorization: Bearer $KOIVU_KEY" -o ergebnis.csv
```

* Bis 50 Zeilen können Sie direkt mit `POST /v1/batch` synchron prüfen; grössere Läufe sind asynchron (bis 100 000 Zeilen).
* Fragen Sie `summary` ab, bis `status` `completed` ist, dann `positives` (Seiten mit `offset` und `limit`).
* **Delta:** Prüfen Sie denselben Bestand später erneut (`POST /v1/batch/{job_id}/rerun`), zeigt `GET /v1/batch/{job_id}/delta` nur, was neu, weggefallen oder anders eingestuft ist.

## 5. Dauerüberwachung

```bash
curl -s -X POST "$KOIVU_URL/v1/monitors" \
  -H "Authorization: Bearer $KOIVU_KEY" -H "Content-Type: application/json" \
  -d '{"query": "Max Muster", "birth_year": 1970, "country": "CH", "cadence": "daily",
       "webhook_url": "https://ihr-system.example/hooks/koivu"}'
```

Wir melden nur Änderungen: **neu**, **verändert** (andere Stufe oder andere Belege), **entfallen**. Bei Webhooks prüfen Sie die Signatur:

```python
import hashlib, hmac

def valid(secret: str, headers: dict, body: bytes) -> bool:
    msg = f"{headers['X-Screening-Timestamp']}.{headers['X-Screening-Delivery-Id']}.".encode() + body
    mac = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, headers["X-Screening-Signature"])
```

Zustellung wird bei Fehlern mehrfach wiederholt (über rund drei Tage). Antworten Sie mit `2xx`, sobald Sie das Ereignis gespeichert haben.

## 6. Fehler

Mit dem Header `Koivu-Version: 2026-10-15` antworten alle Fehler als `application/problem+json`:

```json
{"type": "urn:koivu:problem:unauthorized", "title": "Schlüssel fehlt oder ungültig", "status": 401,
 "code": "unauthorized", "detail": "invalid or missing api key", "request_id": "9d1c7a52…"}
```

| Status | `code` | Bedeutung und Reaktion |
|---|---|---|
| 401 | `unauthorized` | Schlüssel fehlt, ungültig, widerrufen oder abgelaufen. Nicht wiederholen. |
| 403 | `forbidden_scope`, `ip_not_allowed` | Dem Schlüssel fehlt der Scope, oder die Quell-IP ist nicht erlaubt. |
| 404 | `not_found` | Gibt es nicht oder gehört einem anderen Mandanten. |
| 409 | `idempotency_in_progress` | Erste Anfrage mit diesem Key läuft noch: kurz warten, wiederholen. |
| 413 | `payload_too_large` | Nutzlast zu gross (match 512 KB, batch 32 MB, upload 50 MB). |
| 422 | `validation_failed`, `idempotency_key_reused` | Eingabe ungültig (`errors[]` mit JSON-Pointer), oder derselbe Key mit anderem Inhalt. |
| 429 | `rate_limited`, `quota_exceeded` | Minutenlimit oder Tageskontingent. `Retry-After` abwarten. |
| 503 | `service_unavailable`, `database_unavailable`, `list_stale_mandatory` | Vorübergehend: mit Pause wiederholen. |

Nennen Sie uns bei Rückfragen immer die `request_id` (auch im Header `X-Request-ID`).

## 7. Limits

* Vorgabe: **120 Anfragen pro Minute** je Schlüssel (vertraglich anpassbar) plus ein Tageskontingent. Jede Antwort trägt `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`.
* `POST /v1/match` zählt je Query, `POST /v1/batch` je Zeile.
* Test-Schlüssel haben engere Limits.

## 8. Checkliste vor dem Livegang

- [ ] Schlüssel im Secret-Store, nicht im Code oder Log.
- [ ] `Koivu-Version: 2026-10-15` gesetzt, Fehlerbehandlung über `code`.
- [ ] `Idempotency-Key` bei `match` und `batch`, Wiederholung nur bei 429/503/Netzfehler.
- [ ] `decision: signal_only` verstanden: Ihr System oder Ihre Prüferin entscheidet. Keine automatische Freigabe bei «kein Treffer».
- [ ] `suppressed_hits` und `checked_against` im eigenen Protokoll mitgespeichert (Nachweis).
- [ ] Webhook-Signatur geprüft, Antwort `2xx` erst nach dem Speichern.
- [ ] IP-Beschränkung für den Live-Schlüssel mit uns vereinbart.
