Authentifizierung und Scopes
Die externe API kennt genau ein Authentifizierungsverfahren: den Benutzer-API-Key. Diese Seite beschreibt sein Format, wie du ihn sendest, woran er gebunden ist, welche Scopes es gibt, welche Endpunkte sie freischalten und wie die Fehlerbilder 401/403/404/429/503 zu lesen sind.
Wie du überhaupt an einen Key kommst und wie du ihn rotierst oder widerrufst, steht unter Schlüsselverwaltung.
Key-Format
usp_<env>_<publicId>_<secret>
| Segment | Inhalt |
|---|---|
usp | fester Präfix |
<env> | Umgebungskennung der ausstellenden Instanz, 1–16 Zeichen aus [a-z0-9-] (z. B. dev, prod) |
<publicId> | 26 Zeichen Crockford-Base32 — die öffentliche Kennung des Keys; sie steht in Audit-Log und Verwaltungs-UI und darf im Ticket genannt werden |
<secret> | 43 Zeichen Base64url — das eigentliche Geheimnis |
Der Server speichert nur einen gepfefferten HMAC des Secrets (secret_verifier), niemals das
Secret selbst. Der Klartext-Key wird bei der Erstellung genau einmal angezeigt und ist danach
nicht wiederherstellbar — verloren heißt neu ausstellen.
Ein Key trägt die Umgebung im Namen, damit ein Dev-Key in einem Prod-Ticket sofort auffällt. Er ist kein JWT: er ist undurchsichtig, hat keinen lesbaren Claim-Inhalt und lässt sich nicht lokal validieren.
Übertragung
Authorization: Bearer usp_dev_9F8A3K2M7Q4T6V1X5Z0C3BDEFG_5nJq…
Harte Regeln des Filters:
- Genau ein
Authorization-Header. Null, zwei oder ein anderes Schema alsBearer⇒ 401. - Kein Key im Query-String, im Pfad, in einem Cookie oder im Body — diese Wege existieren nicht.
- TLS ist Pflicht: der Key ist ein Bearer-Credential im Klartext.
Bei erfolgreicher Authentifizierung wird der last_used_at-Zeitstempel des Keys gedrosselt
fortgeschrieben — die Verwaltungsoberfläche zeigt damit, ob ein Key noch lebt.
Woran ein Key gebunden ist
Ein Key hängt an einer Kette, die bei jedem Request neu geprüft wird:
Daraus folgen drei Dinge, die im Alltag zählen:
- Effektive Scopes = Key ∩ Grant ∩ Policy. Wird ein Scope am Grant oder an der
Mandanten-Policy entfernt, verliert der Key ihn sofort — ohne dass jemand den Key anfasst.
Was aktuell wirkt, sagt dir
GET /me. - Ein Key = ein Mandant. Die Person hinter dem Grant darf mehreren Mandanten angehören; der
Key sieht trotzdem nur den einen Standort-Teilbaum, auf den er ausgestellt wurde. Der Vergleich
ist exakt: Mandant
musterhausumfasstmusterhausundmusterhaus/nord, aber nichtmusterhaus2. Für Fälle bestimmt der Typ des Mandanten die Sicht: ein Werkstatt-Mandant (musterhaus) bindet über den Standort des Falls, ein Fallabwickler-Mandant (musterhandler) über den zugewiesenen Fallabwickler (caseHandler) — dessen Key sieht also genau die ihm zugewiesenen Fälle, obwohl sie in fremden Werkstatt-Standorten liegen. Fälle in Eigenbearbeitung (OWN_PROCESSING) sind über die Fallabwickler-Sicht nie sichtbar, und ein Fallabwickler-Key kann einen Fall nie in einen anderen Standort verschieben. - Die Person bleibt im Spiel. Wird das Benutzerkonto deaktiviert, in einen technischen
Benutzer umgewandelt oder aus dem Mandanten entfernt, sind alle seine Keys sofort tot. Ein Key
sieht außerdem nie mehr Fälle als die Person selbst: die fachlichen Services scopen weiterhin auf
deren Standort-Mitgliedschaften. Genau daher kommt auch die Eigenbearbeitungs-Regel der
Fallabwickler-Sicht: schon die Personen-Schranke liefert Fallabwickler-Mitgliedern keine
OWN_PROCESSING-Fälle.
Selbstauskunft: GET /me
Der einzige Endpunkt ohne Scope-Anforderung — ein gültiger Key genügt. Er ist der Smoke-Test jeder Integration und der Weg, die tatsächlich wirksame Konfiguration zu sehen.
curl -sS https://dev.devlodge.site/api/external/v1/me \
-H "Authorization: Bearer $USP_API_KEY"
{
"publicId": "9F8A3K2M7Q4T6V1X5Z0C3BDEFG",
"username": "m.mustermann",
"tenantRoot": "musterhaus",
"scopes": ["cases:read", "cases:write", "attachments:read"],
"serverTime": "2026-07-20T09:14:22.481+02:00"
}
Die Antwort enthält nie das Secret, nie den Verifier und keine Fachdaten. scopes sind die
effektiven Scopes (siehe oben) — wundere dich also nicht, wenn dort weniger steht als bei der
Key-Erstellung ausgewählt wurde.
Scope-Katalog
Jeder Endpunkt trägt genau einen Scope. Es gibt kein Wildcard, keinen „oder"-Verbund und keinen
Scope, der einen anderen impliziert: cases:write beinhaltet nicht cases:read.
Die Risikostufe ist reine Anzeigeinformation für die Person, die den Key erzeugt (die UI warnt entsprechend) — sie ist nie Teil einer Autorisierungsprüfung.
| Scope | Risiko | Schaltet frei |
|---|---|---|
cases:read | normal | GET /cases, GET /cases/{caseId}, GET /cases/{caseId}/tags, GET /cases/{caseId}/evaluation-values |
cases:write | erhöht | PUT /cases/{caseId}, PUT /cases/{caseId}/tags, PUT /cases/{caseId}/evaluation-values |
cases:status | erhöht | PUT /cases/{caseId}/status |
attachments:read | erhöht | GET /cases/{caseId}/attachments, GET /attachments/{attachmentId}, GET /attachments/{attachmentId}/content |
attachments:write | erhöht | POST /cases/{caseId}/attachments, DELETE /attachments/{attachmentId} |
comments:read | normal | GET /cases/{caseId}/comments |
comments:write | normal | POST /cases/{caseId}/comments |
reference-data:read | normal | GET /locations, GET /locations/{locationCode}, GET /case-handlers, GET /users, GET /users/{username}, GET /insurances, GET /insurances/{id}, GET /legal-insurances, GET /legal-insurances/{id} |
reference-data:write | erhöht | POST /insurances, PUT /insurances/{id} — zusätzlich Operator-Konto nötig |
extraction:execute | privilegiert | Noch nicht verfügbar — der Scope existiert im Katalog, aber die externe Fläche hat aktuell keinen Extraktions-Endpunkt |
Warum attachments:read als „erhöht" gilt, obwohl es nur liest: Anhänge enthalten Vollmachten,
Gutachten und Rechnungen — also die personenbezogensten Daten eines Falls.
Warum reference-data:write teurer ist als jeder andere Schreib-Scope: der Versicherungskatalog ist
plattformweite Stammdatenhaltung ohne Mandantenschranke. Ein Schreibzugriff dort wirkt sofort
für alle Mandanten — anders als jeder mandantenlokale Schreibzugriff.
reference-data:write verlangt zwei BerechtigungenDer Scope allein reicht nicht: die Person, an deren Freigabe der Schlüssel hängt, muss zusätzlich
ein Operator-Konto sein (Mitgliedschaft in einem ADMIN-Standort) — dieselbe Schranke, die auch
intern für den Versicherungskatalog gilt. Ein Schlüssel aus einem Partner-Mandanten bekommt hier
403 ACCESS_DENIED, selbst wenn der Scope im Ceiling und in der Freigabe steht. Ein Integrator,
der den Katalog pflegen soll, muss deshalb an einem Operator-Konto hängen.
Scopes minimal wählen
Nimm für eine Leseanbindung cases:read + reference-data:read und ergänze nur, was du wirklich
zurückschreibst. Ein Key, der nichts löschen können muss, braucht kein attachments:write.
Statuscodes und ihre Bedeutung
| Status | code | Wann |
|---|---|---|
| 401 | API_KEY_INVALID | Kein/mehrfacher/falsch formatierter Header, unbekannte publicId, falsches Secret, Key abgelaufen/deaktiviert/widerrufen, Grant oder Mandanten-Policy nicht aktiv, Benutzer inaktiv oder nicht mehr im Mandanten |
| 403 | ACCESS_DENIED | Der Key ist gültig, trägt aber den für den Endpunkt nötigen Scope nicht — oder ein echter Rechtefehler nach bestandener Sichtbarkeitsprüfung |
| 404 | NOT_FOUND | Ressource unbekannt oder fremder Mandant oder außerhalb der Sichtbarkeit der Person — bewusst ununterscheidbar |
| 429 | RATE_LIMITED / API_RATE_LIMITED | Rate-Limit erreicht (siehe unten) |
| 503 | API_KEY_AUTH_UNAVAILABLE | Die Authentifizierung selbst ist gestört (Datenbank oder Schlüsselkonfiguration) |
401: eine Antwort für alle Ursachen
Alle oben genannten Ursachen liefern denselben Body und zusätzlich den Header
WWW-Authenticate: Bearer realm="usp-api", error="invalid_token":
{ "code": "API_KEY_INVALID", "message": "API-Zugangsdaten sind ungültig" }
Das ist Absicht: Aus der Antwort lässt sich nicht ableiten, ob eine publicId existiert oder ob nur
das Secret falsch war. Die interne Ursache steht ausschließlich im Server-Log (und im
Audit-Trail des Mandanten). Bei einem 401 hilft dir GET /me mit einem bekannt guten Key sowie ein
Blick in die Schlüsselverwaltung — nicht ein Rateversuch gegen die API.
Wichtig für dein Retry-Verhalten: 401 ist nie transient. Wiederhole nicht automatisch, sondern alarmiere — der Key ist tot oder falsch konfiguriert.
403 vs. 404: kein Existenz-Orakel
Die Trennung ist bewusst so gelegt:
- 403 darfst du erfahren, denn es geht um deine eigene Key-Konfiguration: der Scope fehlt.
Der Body ist
{ "code": "ACCESS_DENIED", "message": "Zugriff verweigert" }. Abhilfe: Key mit dem passenden Scope ausstellen lassen (oder Grant/Policy erweitern) — siehe Schlüsselverwaltung. - 404 verrät nichts. Ein Fall eines fremden Mandanten, eine unbekannte UUID und ein Fall
außerhalb der Sichtbarkeit der Grant-Person liefern identisch
{ "code": "NOT_FOUND", "message": "Ressource nicht gefunden" }bzw. für Fälle"Fall nicht gefunden". Du kannst mit IDs also nicht herausfinden, was es anderswo gibt.
Ein 403 nach bestandener Sichtbarkeitsprüfung gibt es trotzdem in wenigen Fällen — etwa wenn ein
PUT /cases/{caseId} den Fall auf einen Standort verschieben will, den die Grant-Person nicht
bedienen darf. Dort wäre ein 404 irreführend, und ein Orakel entsteht nicht: den Fall durftest du
ohnehin schon sehen.
429: Rate-Limits
Es gibt zwei Stufen. Beide antworten mit Retry-After (Sekunden bis zum Ende des laufenden
Minutenfensters).
| Stufe | Greift | Standardwert | code |
|---|---|---|---|
| Pro IP, vor der Authentifizierung | jeder Request an /api/external/v1/* | 300 Anfragen/Minute | RATE_LIMITED |
| Pro Key, nach der Authentifizierung | erfolgreich authentifizierte Requests | 120 Anfragen/Minute | API_RATE_LIMITED |
| Pro Benutzer, nach der Authentifizierung | alle Keys derselben Person zusammen | 300 Anfragen/Minute | API_RATE_LIMITED |
| Pro Mandant, nach der Authentifizierung | alle Keys des Mandanten zusammen | 1000 Anfragen/Minute | API_RATE_LIMITED |
Die Werte sind Deployment-Konfiguration (platform.api-keys.rateLimitPerMinute,
…rate-limit-key-per-minute, …rate-limit-user-per-minute, …rate-limit-tenant-per-minute) —
frag im Zweifel nach den Werten der Zielumgebung, statt sie zu erraten.
Alle Zähler laufen als festes Minutenfenster (kein gleitendes): der Zähler springt zum Minutenwechsel auf null. Zusätzliche Keys für dieselbe Person erhöhen die Benutzerquote nicht.
Beide Rate-Limit-Antworten tragen neben code und message das Feld retryAfterSeconds:
{ "code": "API_RATE_LIMITED", "message": "Zu viele Anfragen. Bitte versuche es in Kürze erneut.", "retryAfterSeconds": 37 }
Richte deinen Client so ein, dass er Retry-After respektiert und danach mit Backoff weitermacht.
429 ist der einzige 4xx, den du automatisch wiederholen darfst.
503: Auth ist gestört, nicht dein Key
{ "code": "API_KEY_AUTH_UNAVAILABLE", "message": "Die API-Authentifizierung ist vorübergehend nicht verfügbar" }
Kommt bei Datenbank-Ausfall oder fehlender Schlüsselkonfiguration auf dem Server. Bewusst nicht als 401 getarnt, damit du den Unterschied zwischen „mein Key ist kaputt" und „die Plattform ist kaputt" siehst. Behandle es wie ein 5xx: Backoff und erneut versuchen.
Was die externe Fläche nicht kann
Damit du nicht danach suchst — diese Dinge sind bewusst nicht per API-Key erreichbar und werden es auch nicht:
- Benutzer, Standorte, Themes, Tally-Zugangsdaten (alles unter
/api/admin/**) - API-Keys, Grants und Mandanten-Policies verwalten — das passiert ausschließlich in der Plattform-Oberfläche, siehe Schlüsselverwaltung
- Analytics, E-Mail-Log, Integrations-Monitoring, Signaturprozesse, Kundenportal-Verwaltung
- Fälle per Soft-Delete verstecken (
HIDDEN) — der Statusendpunkt lehnt das mit 400 ab - Der Kundenportal-Status
WAITING_FOR_CUSTOMERund die Übergänge dorthin