Fehlercodes

Alle HTTP-Statuscodes der i-Planner REST API v3 mit ihren JSON-Response-Bodies, möglichen `error`-Werten und wann sie ausgelöst werden. Konsolidierte Referenz für defensive Client-Implementierungen.

Zuletzt geprüft: 14. September 2026

Überblick

Reguläre API-Fehler enthalten den HTTP-Status und die Fehlerdetails im Feld data. Beispiel eines abgelaufenen Tokens:

json
{
  "statusCode": 401,
  "statusMessage": "Token expired",
  "data": {
    "error": "token_expired",
    "message": "The API token has expired."
  }
}

Defensive Clients sollten beide Formen verarbeiten: const details = body.data ?? body. Die Beispiele bei den einzelnen Statuscodes zeigen anschließend die Fehlerdetails aus data, nicht den gesamten Response. Bei einem allgemeinen Serverfehler können die Details fehlen; nutze dann den HTTP-Status und die Request-ID. Fehler-Responses enthalten keine Stack-Traces.

Konventionen:

  • error ist ein stabiler Machine-Code — defensive Clients sollten auf diesen Wert matchen, nicht auf den message-Text.
  • message ist menschenlesbar (englisch) und kann zwischen Versionen leicht variieren.
  • Zusätzliche Felder je nach Fehler-Typ — z. B. required_scope/hint bei 403, retry_after bei 429.
  • HTTP-Statuscode trägt die grobe Kategorie (4xx Client, 5xx Server). Der error-String präzisiert.

400 Bad Request

Der Request ist syntaktisch oder logisch unzulässig — typisch bei fehlendem/ungültigem Body, falschen IDs oder Limit-Überschreitungen.

json
// Request-Body fehlt oder ist kein Objekt (POST/PATCH).
{
  "error": "empty_body",
  "message": "Request body must contain at least one customer object."
}

401 Unauthorized

Token-Probleme. Detail-Doku zu den möglichen Codes in Authentication > Fehler bei Authentifizierung.

json
{
  "error": "missing_bearer_token",
  "message": "Authorization header must contain a Bearer token."
}

Auch token_revoked, token_not_found, wrong_token_kind, invalid_token_type, token_mismatch, invalid_expiry und missing_cid sind mögliche 401-Codes. Ein Token-Wechsel ist nur bei einem ungültigen oder ersetzten String erforderlich; ein deaktivierter Token kann aktiviert und ein abgelaufener v5-Token verlängert werden. Siehe Token-Lebenszyklus.

403 Forbidden

Bei fehlendem Scope nennt missing_required_scope die benötigte Berechtigung. Weitere mögliche Ursachen sind ip_not_allowed (Client-IP nicht freigegeben), contract_inactive (Organisationsvertrag nicht aktiv) und trial_expired (Testzeitraum abgelaufen). Eine geschützte Ressource kann trotz passendem Scope ebenfalls 403 liefern.

json
{
  "error": "missing_required_scope",
  "message": "The endpoint \"/v3/customers\" requires the \"v3:customers:read\" scope.",
  "required_scope": "v3:customers:read",
  "provided_scope": "v3:customers:read = 0",
  "hint": "Ask your organization admin to enable 'v3:customers:read'."
}

404 Not Found

Zwei semantisch unterschiedliche Fälle — beide HTTP 404:

json
// URL-Parameter `<id>` passt nicht zum Integer-Regex (z. B. UUID
// oder Buchstaben übergeben). Die Route existiert für diesen Pfad
// nicht — nicht der DATENSATZ fehlt, der PFAD selbst.
{
  "error": "endpoint_not_found",
  "message": "This API endpoint does not exist."
}

405 Method Not Allowed

Eine bekannte URL wurde mit der falschen HTTP-Methode aufgerufen — z. B. PUT /v3/customers/12345 (es gibt nur GET/PATCH/DELETE) oder GET /v3/customers/12345/merge (merge existiert nur als POST).

json
// Die Route existiert für andere Methoden, aber nicht für diese.
// Welche Methoden zulässig sind, listet die [Endpoints-Übersicht](/rest/v3/endpoints).
{
  "error": "method_not_allowed",
  "message": "The HTTP method PUT is not supported on /v3/customers/[id]. Consult the API documentation for allowed methods on this resource."
}

409 Conflict

Race-Condition bei konkurrenten Updates oder Duplikate auf Annotation-Sub-Ressourcen (Links, Followers, Tags) — kein Retry sinnvoll, weil die Ziel-Beziehung bereits existiert.

json
// Soft-Delete UPDATE hat 0 Rows betroffen, obwohl der Read direkt
// davor 1 Row fand. Retry ist sicher, weil keine Daten verändert
// wurden.
{
  "error": "delete_failed",
  "message": "The record could not be deleted, possibly due to a concurrent update."
}

413 Content Too Large

Dokument-Uploads sind auf 25 MiB decodierten Dateiinhalt begrenzt (file_too_large). Für Upload-Requests gilt zusätzlich eine 40-MiB-Grenze für den JSON-Body (payload_too_large), da Base64 den Inhalt vergrößert. Teile Dateien nicht in beliebige JSON-Felder auf, um die Grenze zu umgehen.

422 Unprocessable Content

Beim Erstellen von Kunden können invalid_object oder no_valid_fields auftreten, wenn ein einzelnes Item nicht angelegt werden kann. Bei Kunden-Batches beachte zusätzlich die inserted-/failed-Zusammenfassung: vollständiger Erfolg liefert 200, teilweiser Erfolg 207; bei vollständigem Fehlschlag richtet sich der Fehlerstatus nach der Ursache. Wiederhole nur die fehlgeschlagenen Items. Eine vorgelagerte Feldvalidierung mit 400 schreibt kein Item.

429 Too Many Requests

Rate-Limit überschritten: gemeinsames Budget aller REST-Tokens einer Organisation oder Schutz gegen fehlgeschlagene Authentifizierung pro IP — siehe Rate Limits in der Overview. Der Response setzt immer den Retry-After-Header (Sekunden).

json
// Organisationsbudget ausgeschöpft. Fehlerdetails-`retry_after` und
// HTTP-Header `Retry-After` sind identisch.
{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Try again in 12 seconds.",
  "retry_after": 12
}

500 Internal Server Error

Unerwarteter Backend-Fehler. Body enthält keine internen Details (Stack-Traces, SQL-Errors) — die landen im Server-Log und sind über die x-request-id referenzierbar.

json
{
  "error": "internal_error",
  "message": "Unexpected server error in /v3/customers."
}

502 Bad Gateway

Der für den Aufruf benötigte Datei- oder Suchdienst ist vorübergehend nicht verfügbar, z. B. s3_upload_failed beim Dokument-Upload. Beachte die Request-ID und prüfe bei Schreibaufrufen zunächst, ob ein Datensatz bereits angelegt wurde, bevor du erneut sendest.

503 Service Unavailable

Die Limit-Prüfung ist nicht verfügbar oder das aktuelle Organisationslimit kann nicht geladen werden. Die API antwortet fail-closed, weil ohne funktionierendes Rate-Limiting kein ungedrosselter Traffic durchlaufen soll. Retry-After: 10 ist Standard, ein interner Circuit-Breaker probiert Redis regelmäßig.

json
{
  "error": "service_unavailable",
  "message": "Rate limiting service is temporarily unavailable. Please retry shortly.",
  "retry_after": 10
}

Standard-Headers in Fehler-Responses

Die Verfügbarkeit der Header hängt davon ab, wie weit die Anfrage verarbeitet wurde:

  • x-request-id — Request-ID für Support-Anfragen.
  • X-Token-Expires-At / X-Token-Expires-In-Days — nach vollständig erfolgreicher Token-Prüfung, auch bei späteren Scope- oder Eingabefehlern. Bei 401, IP-Ablehnung oder einer früheren Drosselung fehlen sie. Expires-In-Days erscheint nur bei höchstens 30 vollen Tagen Restlaufzeit. Siehe Gültigkeit.
  • Retry-After — bei 429 und bei 503 aufgrund der Limit-Prüfung. Wartezeit in Sekunden.
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Layer — sobald das Organisationsbudget geprüft wurde, bzw. bei 429 des Authentifizierungsschutzes. Der Layer ist organization oder authFail; separate Tenant-Header gibt es nicht. Siehe Rate Limits.

Robuster Error-Handler

js
async function callApi(path, options = {}) {
  const res = await fetch(`https://www.api.i-planner.app${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${process.env.IPLANNER_API_TOKEN}`,
      ...options.headers,
    },
  })

  if (res.ok) return res.json()

  const responseBody = await res.json().catch(() => ({}))
  const body = responseBody.data ?? responseBody
  const error = body.error ?? 'unknown'

  switch (res.status) {
    case 400:
      throw new BadRequestError(error, body.message)
    case 401:
      // Token-Status und Gültigkeit prüfen; bei ersetztem String Integration aktualisieren
      throw new AuthError(error, body.message)
    case 403:
      // Scope, IP-Freigabe und Organisationszugang prüfen; kein unveränderter Retry
      throw new ScopeError(error, body.required_scope)
    case 404:
      // Unterscheidung: endpoint_not_found (URL falsch) vs. not_found (ID existiert nicht)
      throw error === 'endpoint_not_found' ? new RouteError(path) : new NotFoundError(path)
    case 409:
      // delete_failed — Retry ist sicher, max. 2-3 Versuche
      throw new ConflictError(error, body.message)
    case 429:
      // Retry-After-Header lesen, backoff respektieren
      const retryAfter = Number(res.headers.get('Retry-After') ?? body.retry_after ?? 10)
      throw new RateLimitError(retryAfter)
    case 500:
    case 503:
      // Request-ID für Support; Schreibaufrufe vor Wiederholung auf Teilerfolg prüfen
      throw new ServerError(error, res.headers.get('x-request-id'))
    default:
      throw new Error(`Unexpected HTTP ${res.status}: ${error}`)
  }
}

Dieses Pattern normalisiert beide Fehler-Envelope-Formen. Weitere HTTP-Statuscodes werden vom allgemeinen Fehlerzweig verarbeitet. Spezifische Exceptions pro HTTP-Status erlauben dem Caller, gezielt zu reagieren — Retry-Logik pro Exception-Klasse, kein generischer try/catch-Catch-all.