Zum Inhalt springen

Open Beta – hilf uns beim Testen! Alle Inserate sind nur Beispiele.

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äfixTypWo verwendenErlaubte Scopes
sk_live_*SecretServer-zu-ServerAlle Scopes (inkl. write/delete/*)
pk_live_*PublishableBrowser / EmbedNur 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: delete impliziert write, write impliziert read.
listings:*
├── listings:read
├── listings:write    (impliziert read)
└── listings:delete   (impliziert write + read)

Registry

RessourceScopes
organizationsorganizations:read · :write · :delete
membersmembers:read · :write · :delete
webhookswebhooks:read · :write · :delete
api-keysapi-keys:read · :write · :delete
listingslistings:read · :write · :delete
embedembed:read
appointmentsappointments:read · :write · :book · appointments:*
Full Access*

Publishable-Whitelist

Ein pk_-Key darf ausschließlich diese Read-Scopes tragen:

  • organizations:read
  • listings:read
  • embed:read
  • appointments:read
  • appointments: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 den message-Text.
  • message — menschenlesbare englische Kurzbeschreibung.
  • status — HTTP-Status (im Body gespiegelt).
  • details — optionales Zusatz-Objekt (z. B. fields bei Validierungsfehlern).

Fehler-Code-Tabelle (vollständig)

CodeHTTPBedeutungTypische Ursache
UNAUTHORIZED401Authentifizierung erforderlichHeader X-API-Key fehlt komplett.
INVALID_API_KEY401API-Key ungültig oder abgelaufenKey unbekannt, revoked oder expires_at überschritten.
KEY_ROTATED_OUT401Key wurde rotiert, Overlap-Periode vorbeiAlter Key nach rotation_expires_at weiterverwendet.
FORBIDDEN403Zugriff verweigertKey gültig, aber Ressource gehört einer anderen Organisation.
INSUFFICIENT_SCOPE403Key hat nicht den nötigen ScopeZ. B. listings:write angefragt, Key hat nur listings:read.
PLAN_REQUIRED403Endpoint erfordert einen höheren Abo-PlanPremium-gated Endpoint mit Free/Plus-Org aufgerufen.
CONSENT_REQUIRED403Org hat API-Zugriff auf Bewerberdaten nicht freigeschaltetBewerber-Endpoint ohne gesetzten Consent aufgerufen.
ORIGIN_REQUIRED403Origin-Header für pk_-Keys erforderlichPublishable-Key ohne Origin-Header aufgerufen.
ORIGIN_NOT_ALLOWED403Origin nicht in der Allowlist des KeysOrigin passt zu keinem Eintrag der Key-Allowlist.
IP_NOT_ALLOWED403Client-IP nicht in der Allowlist des KeysRequest-IP außerhalb der konfigurierten IP/CIDR-Liste.
NOT_FOUND404Ressource nicht gefundenUnbekannte ID oder Pfad; Ressource existiert nicht (mehr).
METHOD_NOT_ALLOWED405HTTP-Methode nicht erlaubtZ. B. PUT auf einem Endpoint, der nur GET/POST kennt.
ALREADY_EXISTS409Ressource existiert bereits (Konflikt)Eindeutigkeits-Verletzung, z. B. Slug bereits vergeben.
CANNOT_DELETE_SELF409API-Key kann sich nicht selbst löschenDELETE /api/v1/api-keys/{id} mit der ID des aufrufenden Keys.
IDEMPOTENCY_KEY_REUSED409Idempotency-Key mit abweichendem Body wiederverwendetGleicher Idempotency-Key, aber anderer Request-Body.
INVALID_STATUS_TRANSITION409Ungültiger Bewerbungs-Status-ÜbergangUnzulässiger Übergang (z. B. acceptednew) oder withdrawn-Bewerbung.
BAD_REQUEST400Ungültige AnfrageFehlerhafte Parameter, zu großer Body, ungültige ID-Form.
VALIDATION_ERROR400Eingabe-Validierung fehlgeschlagenZod-Schema-Verletzung; Details unter details.fields.
RATE_LIMITED429Zu viele Anfragen (DoS-Schutz)Rate-Limit von 1000 req/h überschritten. Retry-After beachten.
QUOTA_EXCEEDED429Monatliche Quota überschrittenPlan-Monatslimit erreicht — nur bei QUOTA_ENFORCEMENT=true.
INTERNAL_ERROR500Interner Server-FehlerUnerwarteter Fehler; serverseitig geloggt. Sicher retrybar.
SERVICE_UNAVAILABLE503Dienst vorübergehend nicht verfügbarAbhä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> — der cursor ist ein opaker String aus der vorherigen Antwort.
  • Response: Die Pagination-Metadaten enthalten next_cursor (String oder null) und has_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_page ist 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-Key mit abweichendem Body erneut gesendet, antwortet die API mit 409 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, Form W/"l<v>-<hash>") und einen Cache-Control-Header.
  • Bedingter Request: Den ETag im Folge-Request als If-None-Match mitschicken:
If-None-Match: W/"l3-1a2b3c4d5e6f7a8b"
  • 304 Not Modified: Stimmt der ETag noch, antwortet die API mit 304 ohne 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.

HeaderBedeutungBeispiel
X-RateLimit-LimitMaximale Requests im Fenster1000
X-RateLimit-RemainingVerbleibende Requests im aktuellen Fenster950
X-RateLimit-ResetISO-Zeitpunkt, an dem das Fenster zurückgesetzt wird2026-06-03T11:00:00.000Z
Retry-AfterSekunden 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.

HeaderBedeutungBeispiel
X-Quota-LimitPlan-Monatslimit oder unlimited10000
X-Quota-UsedAktueller Monatszähler oder unknown (Redis nicht erreichbar)3450
X-Quota-ResetISO-Zeitpunkt des Monatswechsels (Berlin)2026-07-01T00:00:00.000Z
X-Quota-Statusok / soft-warning / unavailablesoft-warning
  • soft-warning wird ab ≥ 80 % Auslastung gesetzt (kein Block).
  • unavailable bedeutet, 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.