API-Konventionen
Die kanonische Referenz für alle Querschnitts-Konzepte der WOHNO-REST-API: Authentifizierung, Scopes, Fehler-Format, Pagination, Idempotenz, Caching/ETag, Rate-Limits und Quotas.
Dies ist die zentrale Referenz für alle übergreifenden Konzepte der öffentlichen
REST-API (/api/v1/*). Jeder Guide verlinkt hierher, statt diese Konzepte zu
wiederholen — ändert sich eine Konvention, ändert sie sich hier.
Authentifizierung
Alle /api/v1/*-Requests werden über einen API-Key im X-API-Key-Header
authentifiziert. Es gibt zwei Key-Typen:
| Präfix | Typ | Wo verwenden | Erlaubte Scopes |
|---|---|---|---|
sk_live_* | Secret | Server-zu-Server | Alle Scopes (inkl. write/delete/*) |
pk_live_* | Publishable | Browser / Embed | Nur die Read-Whitelist (siehe Scopes) |
sk_test_* / pk_test_* existieren für Sandbox-Setups.
- Secret (
sk_) — überall, wo der Key geheim bleiben kann: Backend, Cron-Jobs, CRM-Sync. Darf schreiben/löschen. Wird nur als bcrypt-Hash gespeichert und genau einmal im Klartext ausgegeben. - Publishable (
pk_) — überall, wo der Key im Browser landet: Embed, öffentliches HTML, clientseitiges JS. Darf ausschließlich die Read-Whitelist und braucht zwingend eine Origin-Allowlist.
Die ausführliche Erklärung zu Key-Typen, Origin-/IP-Allowlists und Rotation findest du im Authentifizierungs-Guide.
Scopes
Scopes folgen dem Format resource:action (z. B. listings:read) und sind
hierarchisch:
*→ alle Ressourcen und Aktionen (Full Access).resource:*→ alle Aktionen einer Ressource (z. B.appointments:*).- Implikation:
deleteimpliziertwrite,writeimpliziertread.
listings:*
├── listings:read
├── listings:write (impliziert read)
└── listings:delete (impliziert write + read)
Registry
| Ressource | Scopes |
|---|---|
organizations | organizations:read · :write · :delete |
members | members:read · :write · :delete |
webhooks | webhooks:read · :write · :delete |
api-keys | api-keys:read · :write · :delete |
listings | listings:read · :write · :delete |
embed | embed:read |
appointments | appointments:read · :write · :book · appointments:* |
| Full Access | * |
Publishable-Whitelist
Ein pk_-Key darf ausschließlich diese Read-Scopes tragen:
organizations:readlistings:readembed:readappointments:readappointments:book
Das wird zweifach durchgesetzt (App-Layer und DB-Trigger). Ein fehlender
Scope führt zu 403 INSUFFICIENT_SCOPE.
Antwort-Format
Erfolgreiche Antworten wrappen die Nutzdaten immer in data, ergänzt um ein
meta-Objekt:
{
"data": {
/* Objekt (Detail-Endpoint) oder Array (Listen-Endpoint) */
},
"meta": {
"timestamp": "2026-06-07T10:00:00.000Z"
/* listenspezifisch zusätzlich z. B. "org_id", "filter_applied" */
}
}Cursor-Listen liefern zusätzlich ein pagination-Objekt, Offset-Listen die
Zähler unter meta (siehe Pagination).
Feld-Benennung (camelCase vs. snake_case)
⚠️ Die Feld-Benennung ist nicht durchgängig einheitlich — plane das beim Generieren von Client-Modellen ein.
- Listings & Discovery (Read-DTOs): camelCase —
zipCode,propertyType,livingArea,rentCold,organizationId. - Listings-Write-Body + alle übrigen Ressourcen (Webhooks, Members,
Appointments, Usage, API-Keys, WBS): snake_case —
zip_code,created_at,user_id,start_at.
Ein Listing ist also beim Schreiben snake_case und in der Antwort
camelCase. Validierungsfehler (details.fields) nennen stets die korrekten
(snake_case) Eingabe-Keys.
Fehler-Format
Alle Fehler folgen einem einheitlichen Format. Der Body ist immer in error
gewrappt:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"status": 400,
"details": {
"fields": {
"name": "Name is required"
}
}
}
}code— stabiler, maschinenlesbarer String. Auf diesen Code prüfen, nicht auf denmessage-Text.message— menschenlesbare englische Kurzbeschreibung.status— HTTP-Status (im Body gespiegelt).details— optionales Zusatz-Objekt (z. B.fieldsbei Validierungsfehlern).
Fehler-Code-Tabelle (vollständig)
| Code | HTTP | Bedeutung | Typische Ursache |
|---|---|---|---|
UNAUTHORIZED | 401 | Authentifizierung erforderlich | Header X-API-Key fehlt komplett. |
INVALID_API_KEY | 401 | API-Key ungültig oder abgelaufen | Key unbekannt, revoked oder expires_at überschritten. |
KEY_ROTATED_OUT | 401 | Key wurde rotiert, Overlap-Periode vorbei | Alter Key nach rotation_expires_at weiterverwendet. |
FORBIDDEN | 403 | Zugriff verweigert | Key gültig, aber Ressource gehört einer anderen Organisation. |
INSUFFICIENT_SCOPE | 403 | Key hat nicht den nötigen Scope | Z. B. listings:write angefragt, Key hat nur listings:read. |
PLAN_REQUIRED | 403 | Endpoint erfordert einen höheren Abo-Plan | Premium-gated Endpoint mit Free/Plus-Org aufgerufen. |
CONSENT_REQUIRED | 403 | Org hat API-Zugriff auf Bewerberdaten nicht freigeschaltet | Bewerber-Endpoint ohne gesetzten Consent aufgerufen. |
ORIGIN_REQUIRED | 403 | Origin-Header für pk_-Keys erforderlich | Publishable-Key ohne Origin-Header aufgerufen. |
ORIGIN_NOT_ALLOWED | 403 | Origin nicht in der Allowlist des Keys | Origin passt zu keinem Eintrag der Key-Allowlist. |
IP_NOT_ALLOWED | 403 | Client-IP nicht in der Allowlist des Keys | Request-IP außerhalb der konfigurierten IP/CIDR-Liste. |
NOT_FOUND | 404 | Ressource nicht gefunden | Unbekannte ID oder Pfad; Ressource existiert nicht (mehr). |
METHOD_NOT_ALLOWED | 405 | HTTP-Methode nicht erlaubt | Z. B. PUT auf einem Endpoint, der nur GET/POST kennt. |
ALREADY_EXISTS | 409 | Ressource existiert bereits (Konflikt) | Eindeutigkeits-Verletzung, z. B. Slug bereits vergeben. |
CANNOT_DELETE_SELF | 409 | API-Key kann sich nicht selbst löschen | DELETE /api/v1/api-keys/{id} mit der ID des aufrufenden Keys. |
IDEMPOTENCY_KEY_REUSED | 409 | Idempotency-Key mit abweichendem Body wiederverwendet | Gleicher Idempotency-Key, aber anderer Request-Body. |
INVALID_STATUS_TRANSITION | 409 | Ungültiger Bewerbungs-Status-Übergang | Unzulässiger Übergang (z. B. accepted→new) oder withdrawn-Bewerbung. |
BAD_REQUEST | 400 | Ungültige Anfrage | Fehlerhafte Parameter, zu großer Body, ungültige ID-Form. |
VALIDATION_ERROR | 400 | Eingabe-Validierung fehlgeschlagen | Zod-Schema-Verletzung; Details unter details.fields. |
RATE_LIMITED | 429 | Zu viele Anfragen (DoS-Schutz) | Rate-Limit von 1000 req/h überschritten. Retry-After beachten. |
QUOTA_EXCEEDED | 429 | Monatliche Quota überschritten | Plan-Monatslimit erreicht — nur bei QUOTA_ENFORCEMENT=true. |
INTERNAL_ERROR | 500 | Interner Server-Fehler | Unerwarteter Fehler; serverseitig geloggt. Sicher retrybar. |
SERVICE_UNAVAILABLE | 503 | Dienst vorübergehend nicht verfügbar | Abhängiger Dienst (DB/Redis) down; mit Backoff erneut versuchen. |
Pagination
Die API kennt zwei Pagination-Stile. Welcher gilt, steht in der jeweiligen Endpoint-Doc.
Cursor-Pagination (bevorzugt für große/öffentliche Listen)
Verwendet z. B. von GET /api/v1/listings. Stabil gegenüber gleichzeitigen
Inserts/Deletes.
- Request:
?cursor=<opaque>&limit=<n>— dercursorist ein opaker String aus der vorherigen Antwort. - Response: Die Pagination-Metadaten enthalten
next_cursor(String odernull) undhas_more(boolean):
{
"pagination": {
"next_cursor": "eyJpZCI6...",
"has_more": true,
"limit": 20
}
}Solange has_more === true, den next_cursor als nächsten cursor mitgeben.
Offset-Pagination (klassische Listen)
Verwendet z. B. von GET /api/v1/organizations und GET /api/v1/appointments.
- Request:
?page=<n>&per_page=<n>(per_pageist gedeckelt, typ. max. 100). - Response-
meta:total,page,perPage,hasMore:
{
"data": [],
"meta": {
"total": 42,
"page": 1,
"perPage": 20,
"hasMore": true,
"timestamp": "2026-06-03T10:30:00.000Z"
}
}Idempotenz
Für schreibende Endpoints (POST/PATCH/DELETE) kann ein Client einen
optionalen Header senden, um doppelte Ausführung bei Retries zu verhindern:
Idempotency-Key: <vom Client gewählter, eindeutiger String>
- Fenster: Ein Schlüssel ist 24 Stunden gültig. Innerhalb dieses Fensters liefert ein erneuter Request mit demselben Schlüssel dieselbe Antwort zurück, ohne die Aktion erneut auszuführen.
- Konflikt: Wird derselbe
Idempotency-Keymit abweichendem Body erneut gesendet, antwortet die API mit409 IDEMPOTENCY_KEY_REUSED. Ein Schlüssel ist an genau einen Request-Body gebunden. - Empfehlung: Pro logischer Operation eine UUID v4 erzeugen und bei Netzwerk-Retries unverändert wiederverwenden.
Caching & ETag
Lese-Endpoints (z. B. GET /api/v1/listings*) unterstützen bedingte Requests
über schwache ETags.
- Response: Jede Antwort trägt einen
ETag(weak, FormW/"l<v>-<hash>") und einenCache-Control-Header. - Bedingter Request: Den ETag im Folge-Request als
If-None-Matchmitschicken:
If-None-Match: W/"l3-1a2b3c4d5e6f7a8b"
304 Not Modified: Stimmt der ETag noch, antwortet die API mit304ohne Body — der Client darf seine gecachte Kopie weiterverwenden.- Cache-Bust: Der ETag enthält die DTO-Schema-Version. Eine DTO-Änderung invalidiert automatisch alle Caches.
Rate-Limits
Zum DoS-Schutz gilt für API-Keys ein Limit von 1000 Requests pro Stunde.
Wird es überschritten, antwortet die API mit 429 RATE_LIMITED.
| Header | Bedeutung | Beispiel |
|---|---|---|
X-RateLimit-Limit | Maximale Requests im Fenster | 1000 |
X-RateLimit-Remaining | Verbleibende Requests im aktuellen Fenster | 950 |
X-RateLimit-Reset | ISO-Zeitpunkt, an dem das Fenster zurückgesetzt wird | 2026-06-03T11:00:00.000Z |
Retry-After | Sekunden bis zum nächsten erlaubten Request (nur bei 429) | 42 |
Nutze exponentielles Backoff und respektiere Retry-After.
Quotas
Zusätzlich zum Rate-Limit zählt die API Requests pro Organisation und Monat
(Europe/Berlin). Aktuell tracking-only: Es wird nichts geblockt, solange
QUOTA_ENFORCEMENT=false (Default). Jede Antwort trägt informative Header.
| Header | Bedeutung | Beispiel |
|---|---|---|
X-Quota-Limit | Plan-Monatslimit oder unlimited | 10000 |
X-Quota-Used | Aktueller Monatszähler oder unknown (Redis nicht erreichbar) | 3450 |
X-Quota-Reset | ISO-Zeitpunkt des Monatswechsels (Berlin) | 2026-07-01T00:00:00.000Z |
X-Quota-Status | ok / soft-warning / unavailable | soft-warning |
soft-warningwird ab ≥ 80 % Auslastung gesetzt (kein Block).unavailablebedeutet, dass der Zähler (Redis) gerade nicht erreichbar ist — die API antwortet trotzdem normal.
Wie sich die API über die Zeit weiterentwickelt (additive Änderungen, Deprecation-Fenster, DTO-Versionen), beschreibt die Versionierung & Deprecation-Policy.