# q-api.de – sichere Klingel-API

`q-api.de` ist die eigenständige Server-zu-Server-API der Klingel-Plattform.
Sie ist von der Web-App unter `offlineklingel.de` und von allen nutzenden
Projekten getrennt. Projekte wie Bärenklingel und QR-Meldung sind API-Clients,
nicht Teil des Klingel-Deployments.

## Basisadressen und Versionierung

- REST: `https://q-api.de/platform/v1`
- Live-WebSocket: `wss://q-api.de/platform/v1/live`
- OpenAPI: `https://q-api.de/openapi.yaml`
- Entwicklerportal: `https://q-api.de/docs/`

Die Major-Version ist Bestandteil des Pfades. In `v1` werden Felder nur
abwärtskompatibel ergänzt. Entfernen, Umbenennen oder semantisch inkompatible
Änderungen erfordern `/platform/v2`.

## Sicherheitsmodell

API-Tokens sind ausschließlich für vertrauenswürdige Projekt-Backends gedacht.
Ein Token darf niemals in Browser-JavaScript, Apps, QR-Codes, Git, Logs oder
Support-Tickets gelangen. Ein Browser-Frontend ruft immer das eigene Backend
auf; dieses Backend ruft `q-api.de` auf. Für Live-Verbindungen kann das
Projekt-Backend den WebSocket transparent weiterleiten und serverseitig den
`Authorization`-Header ergänzen.

Jeder Klartext-Token wird nur einmal bei der Ausstellung angezeigt. Gespeichert
werden lediglich ein nicht geheimer Token-Identifier, die letzten vier Zeichen
und ein serverseitig gepepperter Hash. Ein Token ist immer an genau einen
Klingel-Benutzer (`subjectUserId`) gebunden. Daher kann ein kompromittierter
Mandanten-Token nicht auf Türen anderer Mandanten zugreifen.

Für Bärenklingel oder QR-Meldung gilt:

1. ein API-Client pro Projekt und Umgebung (`production`, `staging`),
2. ein separates Token pro angebundenem Klingel-Mandanten,
3. minimale Scopes pro Token,
4. kurze Laufzeit, wenn automatisierte Rotation möglich ist,
5. sofortiger Widerruf bei Verdacht auf Verlust.

## Authentifizierung

```http
Authorization: Bearer qapi_v1_<token-id>_<secret>
```

Beispiel:

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $Q_API_TOKEN" \
  -H "Accept: application/json" \
  https://q-api.de/platform/v1/doors
```

Mehrere `Authorization`-Header, andere Schemata, Tokens in Query-Parametern und
mehrdeutige Header werden abgewiesen. Die Plattform aktiviert bewusst kein
CORS für Service-Tokens.

## Scopes

| Scope | Erlaubnis |
| --- | --- |
| `dashboard:read` | Zusammengefassten Status lesen |
| `doors:read`, `doors:write` | Türen lesen bzw. verwalten |
| `qr:read`, `qr:write` | QR-Status/Artefakte lesen bzw. gestalten/rotieren |
| `rings:read`, `rings:respond`, `rings:write` | Klingeln lesen, annehmen/ablehnen/blockieren, Testklingeln erzeugen |
| `chat:read`, `chat:write` | Chats und private Fotometadaten/-dateien lesen bzw. Nachrichten und Fotos senden |
| `communications:write` | Chat-/Audio-/Video-Kommunikation starten und beenden |
| `calls:write` | WebRTC-Signale und Heartbeats senden |
| `events:read` | Live-WebSocket abonnieren |
| `devices:read`, `devices:write` | Geräte und Push-Zustände lesen/verwalten |
| `members:read`, `members:write` | Türmitglieder lesen/verwalten |
| `settings:read`, `settings:write` | Benutzereinstellungen lesen/verwalten |

`doors:*` und entsprechende Bereichs-Wildcards sind möglich, `*` nur für
besonders kontrollierte interne Integrationen. Client- und Token-Scopes werden
beide geprüft; ein Token kann die Rechte seines Clients niemals erweitern.

## Zeitliche Zugriffe und Widerruf

Client und Token besitzen unabhängig voneinander:

- `status`: `active`, `suspended` oder `revoked`,
- `validFrom` und optional `validUntil`,
- optionale `weeklyUtcWindows`.

Ein UTC-Fenster besteht aus `days` (`0` Sonntag bis `6` Samstag),
`startMinute` und `endMinute` (`0` bis `1440`, Ende exklusiv). Beispiel für
Montag bis Freitag 08:00–18:00 UTC:

```json
[{ "days": [1, 2, 3, 4, 5], "startMinute": 480, "endMinute": 1080 }]
```

Zugriff ist nur erlaubt, wenn Client und Token gleichzeitig aktiv, zeitlich
gültig und innerhalb eines konfigurierten Fensters sind. Der Live-WebSocket
prüft die Berechtigung mindestens alle 60 Sekunden erneut.

## Mandantentrennung

Das Token enthält serverseitig einen `subjectUserId`. Alle bestehenden
Klingel-Berechtigungsprüfungen für Türen, Ringe, Chats, Geräte und Mitglieder
laufen zusätzlich zur Tokenprüfung. Ressourcen-IDs sind keine Berechtigung.
Auch ein erratener oder geleakter Fremd-Identifier liefert keine fremden Daten.

## Idempotenz

Für `POST`, `PUT`, `PATCH` und fachlich wiederholbare Aktionen muss das
Projekt-Backend einen stabilen Schlüssel je Benutzeraktion senden:

```http
Idempotency-Key: qr-meldung-01J2M6P8X6AR8R5Q0M5V0T9M7A
```

Erlaubt sind 16–128 Zeichen aus `A-Z`, `a-z`, `0-9`, `_`, `.`, `:`, `-`.
Wird derselbe Schlüssel mit einem anderen Body erneut verwendet, antwortet die
API mit `409 idempotency-conflict`. Bei einem unklaren Ergebnis muss der Client
zuerst die Ressource neu lesen, statt die Aktion blind zu wiederholen.

## Fehlerformat

```json
{
  "message": "The token does not grant the required scope",
  "code": "api-scope-forbidden",
  "details": { "requestId": "..." }
}
```

Wichtige Statuscodes:

- `400`: ungültige Eingabe,
- `401`: fehlendes/ungültiges Token,
- `403`: Scope, Zeitfenster oder Clientstatus verweigert,
- `404`: Ressource innerhalb des Mandanten nicht vorhanden,
- `409`: Zustands- oder Idempotenzkonflikt,
- `413`: regulärer JSON-Body größer als 128 KiB oder Foto-Upload größer als 8 MiB,
- `429`: Client-, Token- oder IP-Limit erreicht,
- `503`: Plattform startet oder eine erforderliche Abhängigkeit ist nicht bereit.

Jede Antwort enthält `X-Request-Id`. Dieser Wert darf in Supportanfragen
verwendet werden; niemals das Token mitsenden.

## REST-Funktionsumfang

Die vollständigen Schemas stehen in `Q_API_OPENAPI.yaml`. Die wichtigsten
Ressourcen sind:

- `GET /dashboard`
- `GET|POST /doors`, `GET|PATCH|DELETE /doors/{doorId}`
- `GET /doors/{doorId}/qr`, QR-Artefakt, Health, Design und Rotation
- `GET /rings`, `GET /rings/{ringId}`, Response und Blockierung
- Chat, Audio und Video unter `/rings/{ringId}/communications`
- Paketfotos unter `GET|POST /rings/{ringId}/communications/{communicationId}/photos`
- privater Bildabruf unter `GET /media/{attachmentId}?variant=thumbnail|display`
- WebRTC-Signale und Heartbeats je Kommunikation
- `GET /messages`, `GET|POST /messages/{threadId}`
- Geräte inklusive Push-Registrierung und Test
- Mitglieder und Einladungen
- Benutzer-/Benachrichtigungs-/Datenschutzeinstellungen

QR-Bearer-Geheimnisse werden auch über die Service-API nicht als Klartext
ausgegeben. QR-Artefakte können kontrolliert heruntergeladen und Codes rotiert
werden.

Der QR-Design-Endpunkt unterstützt neben dem kompatiblen Basissatz
`color`/`shape`/`label` auch Anleitung, physisches Format, Sprache, Rahmen,
QR- und Eckenform, sichere Farbfelder, Logo-Modus und Sichtbarkeiten. Der
Artefakt-Endpunkt liefert den reinen QR-Code als `svg`/`png` sowie das
vollständige Türschild als `sign_svg` oder maßhaltige `pdf`. Für eine
Rastergrafik kann ein Partner das vollständige `sign_svg` lokal rendern; so
bleibt die Bildlast beim jeweiligen Projekt und nicht auf dem Kommunikationsserver.

## Paketfotos ohne Audio oder Video

Ein Paketfoto verwendet eine chatgebundene Kommunikation und öffnet weder
Mikrofon noch Videokanal. Ein Client startet dafür eine Kommunikation mit
`{"mode":"chat","intent":"photo"}`. Erst nach der fachlichen Zustimmung
der Gegenseite darf das Projekt-Backend das Bild hochladen.

Die öffentliche Konfiguration unter `GET /config` weist die Funktion und ihre
verbindlichen Clientgrenzen aus:

```json
{
  "features": { "photoAttachments": true },
  "photos": {
    "maxUploadBytes": 8388608,
    "maxPerCommunication": 5,
    "captionMaxLength": 500,
    "acceptedMimeTypes": ["image/jpeg", "image/png", "image/webp"]
  }
}
```

Der Upload ist `multipart/form-data` und benötigt `chat:write`. Der HTTP-Client
muss den Boundary-Parameter selbst erzeugen; `Content-Type` darf daher nicht
manuell ohne Boundary gesetzt werden. Pro Request sind exakt eine Datei sowie
die folgenden Formularfelder erlaubt:

| Feld | Pflicht | Grenze |
| --- | --- | --- |
| `file` | ja | JPEG, PNG oder WebP; maximal 8388608 Bytes |
| `caption` | nein | maximal 500 Zeichen |
| `clientMessageId` | ja | 16–120 Zeichen, stabil für Deduplizierung |

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $Q_API_TOKEN" \
  -F "file=@ablageort.jpg;type=image/jpeg" \
  -F "caption=Hinter der grauen Tonne" \
  -F "clientMessageId=photo-01J2M6P8X6AR8R5Q0M5V0T9M7A" \
  https://q-api.de/platform/v1/rings/RING_ID/communications/COMMUNICATION_ID/photos
```

Eine identische Wiederholung liefert dasselbe `PhotoMessage`-DTO. Eine andere
Datei oder Caption unter derselben `clientMessageId` antwortet mit `409`.
Fertige Fotos werden mit `chat:read` aufgelistet. Bilddaten sind ausschließlich
über `GET /media/{attachmentId}?variant=thumbnail|display` abrufbar; auch dort
werden Token-Mandant, Tür und Ring bei jedem Request erneut geprüft.

Der Server vertraut weder Dateiname noch Browser-MIME. Er prüft Magic Bytes und
Decodierbarkeit, richtet das Bild aus, begrenzt die lange Kante auf 2048 Pixel
und kodiert Display und Thumbnail als WebP neu. Dadurch werden EXIF-, GPS- und
Kamerametadaten entfernt. HEIC/HEIF, SVG, GIF, Video und nicht decodierbare
Dateien werden abgewiesen. Die Medienantwort ist privat, `no-store` und
`Content-Disposition: inline`; DTOs enthalten keine öffentliche Storage-URL.

## Live-WebSocket

Serverseitiger Verbindungsaufbau:

```http
GET /platform/v1/live?since=<sequence> HTTP/1.1
Host: q-api.de
Upgrade: websocket
Connection: Upgrade
Authorization: Bearer <token>
```

Nach erfolgreicher Verbindung kommt `live.ready`. Ereignisse besitzen:

```json
{
  "type": "ring.updated",
  "eventId": "...",
  "sequence": 123,
  "occurredAt": "2026-07-19T12:00:00.000Z",
  "resourceVersion": 1,
  "payload": {}
}
```

Der Client speichert `sequence` und verbindet mit `?since=<sequence>` erneut.
Bei `sync.required` werden die aktuellen REST-Ressourcen neu geladen. Ein
JSON-`{"type":"ping"}` erhält `pong`; zusätzlich sind WebSocket Ping/Pong-
Frames aktiv. Payload, Puffer, globale Verbindungen und Verbindungen pro
Token/IP sind begrenzt.

## Verbrauchsmessung und spätere Abrechnung

Jeder authentifizierte REST-Aufruf und jede Live-Verbindung erzeugt genau eine
abrechenbare Einheit. Dauer und Antwortbytes werden zusätzlich erfasst. Die
Plattform aggregiert nach UTC-Stunde, Client, Token, sicherem Routenschlüssel
und HTTP-Statusklasse. Pfadparameter und geheime QR-/Einladungswerte landen
nicht im Abrechnungsjournal.

Ein Client kann erhalten:

- `includedUnitsMonthly`,
- `pricePerUnitMicros`,
- getrennte Limits pro Client und pro Token.

Die monatliche Vorschau zieht Freieinheiten je UTC-Kalendermonat ab. Die
Messwerte sind eine technische Abrechnungsgrundlage; Rechnungserstellung und
Zahlungseinzug bleiben bewusst getrennte Folgeprozesse.

## Operative Client- und Tokenverwaltung

Nur ein angemeldeter Plattformadministrator verwaltet Clients über die
geschützte Portal-API auf der Klingel-Administrationsdomain:

- Oberfläche: `https://offlineklingel.de/admin/api-access`
- Navigation im Portal: `Admin` → `API-Zugänge`

Der sensible Bereich verlangt zusätzlich zur bestehenden Anmeldung einmalig
erneut das Passwort und erstellt daraus eine kurzlebige, Same-Origin-gebundene
Portal-Sitzung mit CSRF-Schutz. Das Formular „Neues Partnerprojekt sofort
bereitstellen“ erzeugt Anfrage, Freigabe, Client und Initialtoken in einem
Bedienvorgang. Danach zeigt es Basis-URL, Client-ID, Kurzname, Subject, Scopes,
Token und einen kopierfertigen Beispielaufruf gemeinsam an. Ein neu
ausgestelltes Token wird nur einmal angezeigt und muss sofort in den
Secret-Manager des Partner-Backends übernommen werden.

Ein alter Token-Klartext ist nicht wiederherstellbar, weil nur sein Hash
gespeichert wird. „Zugangsdaten erneut erzeugen“ verlangt das Admin-Passwort in
einer höchstens zwei Minuten alten Portal-Sitzung, stellt ein Ersatz-Token mit
denselben Grenzen aus und widerruft das ausgewählte alte Token atomar. Dadurch
lassen sich Zugangsdaten erneut anzeigen, ohne entschlüsselbare Token im System
vorzuhalten.

- `GET|POST /api/v1/admin/api-clients`
- `PATCH /api/v1/admin/api-clients/{clientId}`
- `POST /api/v1/admin/api-clients/{clientId}/tokens`
- `POST /api/v1/admin/api-tokens/{tokenId}/exchange`
- `DELETE /api/v1/admin/api-tokens/{tokenId}`
- `GET /api/v1/admin/api-usage?clientId=...&from=...&to=...`

Diese Endpunkte verwenden Portal-Sitzung, Origin-Prüfung und CSRF-Schutz und
sind auf `q-api.de` nicht öffentlich geroutet. Der Klartext eines neu
ausgestellten Tokens wird nie in einem Idempotenz- oder Wiederholungs-Cache
gespeichert.

## Produktionscheckliste für Partnerprojekte

1. Token nur in Secret-Manager/Dateisecret des Projekt-Backends speichern.
2. Pro Mandant und Umgebung getrenntes Token verwenden.
3. Mindestrechte und Ablaufdatum setzen.
4. Bei schreibenden Requests stabile Idempotency-Keys verwenden.
5. `401` nicht endlos wiederholen; Token rotieren oder Integration sperren.
6. `429` mit Backoff und `Retry-After` behandeln.
7. WebSocket mit Cursor wiederverbinden und `sync.required` behandeln.
8. Tokenrotation und Widerruf mindestens einmal vor Go-live testen.
9. Niemals Request-/Authorization-Header vollständig protokollieren.
10. Verbrauch gegen die Admin-Nutzungsübersicht abgleichen.
