Darstellung
Fehler, Statuscodes und Limits
Fehlerformat
Alle Fehler der Public API haben dieselbe Form:
json
{
"error": "insufficient_scope",
"message": "Scope site-reports:read fehlt.",
"details": { "required": "site-reports:read", "scopes": ["core:read"] }
}| HTTP | error | Bedeutung | Was tun |
|---|---|---|---|
| 400 | validation_error | Pflichtfeld fehlt oder Wert ungültig | message lesen, Eingabe korrigieren |
| 401 | unauthorized | Schlüssel fehlt, ungültig oder widerrufen | Header prüfen, ggf. neuen Schlüssel anlegen |
| 403 | insufficient_scope | Schlüssel hat den Scope nicht | Scope beim Anlegen des Schlüssels ergänzen |
| 403 | module_disabled | Bereich für den Betrieb nicht freigeschaltet | Betrieb muss das Modul buchen; Daten bleiben erhalten |
| 403 | FORBIDDEN mit code demo_locked | Der Betrieb ist ein Demo-Account; dort sind Anbindungen gesperrt (seit API 1.23) | Den Schlüssel in einem echten Betrieb anlegen |
| 404 | NOT_FOUND | Datensatz existiert nicht oder gehört einem anderen Betrieb | ID prüfen |
| 409 | CONFLICT | Zustandskonflikt, zum Beispiel Statuswechsel nicht erlaubt | details.code auswerten (siehe unten) |
| 413 | payload_too_large | Body zu groß: JSON über 64 KB bzw. Upload über der Grenze | Daten aufteilen |
| 415 | unsupported_media_type | Schreibender Aufruf mit falschem oder fehlendem Content-Type (kaputtes JSON: 400) | Content-Type: application/json senden |
| 429 | rate_limited | Zu viele Anfragen | Retry-After (Sekunden) abwarten |
| 500 | INTERNAL_ERROR | Unerwarteter Fehler | erneut versuchen, bei Wiederholung melden |
Fehler aus der Geschäftslogik (Validierung, Konflikte, nicht gefunden) tragen zusätzlich code und timestamp. Validierungsfehler kommen seit API 1.21 immer als validation_error, auch wenn sie aus der Geschäftslogik stammen (vorher dort BAD_REQUEST).
Ein Demo-Account (seit API 1.23) antwortet auf jeden Aufruf so:
json
{
"error": "FORBIDDEN",
"message": "Im Demo-Account sind Anbindungen gesperrt.",
"code": "demo_locked",
"timestamp": "2026-11-05T09:00:00.000Z",
"details": { "code": "demo_locked", "feature": "public_api" }
}Validierungsfehler je Feld
Bei schreibenden Aufrufen nennt details das betroffene Feld:
json
{
"error": "validation_error",
"message": "is_active: Unbekanntes Feld.",
"details": {
"field": "is_active",
"errors": [{ "field": "is_active", "message": "Unbekanntes Feld." }]
}
}details.field ist das erste Feld, details.errors listet bis zu 20 Fehler. Unbekannte Felder, kaputtes JSON und ein leerer Body ergeben ebenfalls 400 validation_error.
Konflikte (details.code)
details.code | Bedeutung |
|---|---|
idempotency_conflict | Der Idempotency-Key wurde schon für eine andere Anfrage (anderer Body, Pfad oder Query-String) verwendet |
idempotency_in_progress | Die erste Anfrage mit diesem Idempotency-Key läuft noch; später erneut senden |
idempotency_outcome_unknown | Die erste Anfrage mit diesem Idempotency-Key ist seit mehr als 5 Minuten offen, ihr Ausgang ist unbekannt; vor einem neuen Schlüssel per GET prüfen, ob der Datensatz angelegt wurde |
has_documents | Der Kontakt hat versendete Belege und kann nicht gelöscht werden; stattdessen archivieren |
has_open_items | Archivieren oder Löschen des Kontakts würde andere Daten mitändern oder löschen; details.items zählt sie je Art (siehe unten). In der App bereinigen, dann erneut senden |
duplicate_project_number | Die Projektnummer ist im Betrieb schon vergeben (Projektnummern sind je Betrieb eindeutig, seit API 1.22); andere Nummer wählen oder ohne project_number anlegen |
NUMBER_TAKEN | Nur POST /projects ohne project_number und POST /contacts: Die von Work7 vergebene Projekt- bzw. Kundennummer wurde im selben Moment im Betrieb anders vergeben (gleichzeitige Anlage, von Hand gesetzte Nummer). details.number nennt die Nummer, details.document_type project oder customer. Nichts wurde angelegt; ein erneuter Versuch kann klappen, Work7 überspringt die Nummer dann (seit API 1.22). Auch mit demselben Idempotency-Key, die Antwort wird nicht gespeichert |
PROJECT_STATUS_STALE | Der Projektstatus hat sich seit expected_status_id geändert; details.current_status_id nennt den aktuellen. Gilt auch bei reinen Feldänderungen. Projekt neu lesen und erneut entscheiden |
Kontakt archivieren oder löschen: has_open_items
In der App nimmt das Archivieren oder Löschen eines Kontakts dessen offene Vorgänge mit. Über die API ändern PATCH /contacts/{id} mit status: archived und DELETE /contacts/{id} nur den Kontakt selbst. Gibt es solche Vorgänge, antwortet die API mit 409 CONFLICT, details.code = "has_open_items", und ändert nichts:
json
{
"error": "CONFLICT",
"code": "has_open_items",
"message": "Der Kontakt hat offene Vorgänge, die beim Archivieren mitgeändert oder gelöscht würden (1 Angebotsentwürfe, 2 Anfragen). Bitte zuerst in der App bereinigen.",
"details": { "code": "has_open_items", "items": { "offer_drafts": 1, "inquiries": 2 } }
}Schlüssel in details.items | Archivieren | Löschen |
|---|---|---|
offer_drafts | Angebotsentwürfe | Angebotsentwürfe |
invoice_drafts | Rechnungsentwürfe | Rechnungsentwürfe |
sales_document_drafts | – | Entwürfe von Lieferscheinen und Kostenvoranschlägen |
open_rapports | nicht abgeschlossene Rapporte | nicht abgeschlossene Rapporte |
open_acceptance_protocols | nicht abgeschlossene Abnahmeprotokolle | nicht abgeschlossene Abnahmeprotokolle |
inquiries | offene Anfragen (nicht gewonnen, verloren oder abgelehnt) | alle Anfragen |
projects | laufende Projekte (nicht abgeschlossen oder archiviert) | alle Projekte |
details.items nennt nur Arten mit mindestens einem Eintrag.
Schreibende Aufrufe
- Body nur als JSON mit
Content-Type: application/json(Zusatzcharset=utf-8erlaubt). Anderer oder fehlender Content-Type:415 unsupported_media_type. - Höchstens 64 KB je Body, gezählt beim Empfang (unabhängig vom Header
Content-Length). Darüber:413 payload_too_largemitdetails.maxBytes. - Ausnahme
POST /inquiries: Der Body wird auch ohneContent-Type: application/jsonals JSON gelesen (für Website-Formulare), Größengrenze und Feldprüfung gelten trotzdem; unbekannte Felder werden dort ignoriert.
Idempotency-Key
Schreibende POST-Aufrufe nehmen den optionalen Header Idempotency-Key an, damit ein Netzwerkfehler beim Wiederholen keinen zweiten Datensatz erzeugt:
http
POST /api/v1/<bereich>
Content-Type: application/json
Idempotency-Key: 6f1c2a9e-3b7d-4c55-9a43-0d2e8f61b7aa- Format: 16 bis 128 druckbare ASCII-Zeichen ohne Leerzeichen, zum Beispiel eine UUID. Anderes Format:
400 validation_errormitdetails.field = "Idempotency-Key". - Gleicher Schlüssel mit gleichem Body und Pfad: Die API schreibt nicht erneut, sondern liefert die gespeicherte Antwort (Status, Body und
Location) mit dem HeaderIdempotent-Replay: true. - Gleicher Schlüssel mit anderem Body, Pfad oder Query-String:
409 CONFLICT,details.code = "idempotency_conflict". - Läuft die erste Anfrage noch:
409 CONFLICT,details.code = "idempotency_in_progress". - Ist die erste Anfrage seit mehr als 5 Minuten offen (zum Beispiel nach einem Absturz), ist ihr Ausgang unbekannt:
409 CONFLICT,details.code = "idempotency_outcome_unknown". Die API führt sie nicht erneut aus. Vor einem neuen Schlüssel per GET prüfen, ob der Datensatz schon angelegt wurde. - Gespeichert werden Antworten mit 2xx und 4xx. Nach einem 5xx oder einem
409mitdetails.code = "NUMBER_TAKEN"darf derselbe Schlüssel erneut gesendet werden. - Ein Schlüssel gilt je API-Schlüssel und 24 Stunden; danach wird er wie ein neuer behandelt. Gespeicherte Antworten werden 24 Stunden aufbewahrt und danach gelöscht: beim nächsten Aufruf des Betriebs mit
Idempotency-Key, sonst beim täglichen Aufräumen (spätestens rund einen Tag nach Ablauf). - Wird ein Kontakt über die API archiviert oder gelöscht, löscht Work7 die gespeicherten Antworten, die auf ihn zeigen (
Location: /api/v1/contacts/{id}). Eine spätere Wiederholung mit einem solchen Schlüssel läuft wie ein neuer Aufruf. - Ohne Header verhält sich jeder Aufruf wie bisher.
Welche Endpunkte den Header auswerten, steht in der API-Referenz beim jeweiligen Endpunkt.
Rate-Limit
600 Anfragen pro Minute je Schlüssel, Lesen und Schreiben zusammen. Davon dürfen höchstens 60 pro Minute schreibend sein (POST, PATCH, DELETE); Lesen, mit 403 abgewiesene Aufrufe und Uploads der Datenablage (POST /inbound, dort gilt nur das Gesamtlimit) zählen dort nicht mit. Bei Überschreitung eines der beiden Limits antwortet die API mit 429 rate_limited und dem Header Retry-After (Sekunden); beim Schreib-Limit steht in details zusätzlich scope: "write" und limit: 60. Für Massenabgleiche: Listen mit limit=200 holen, statt Einzelobjekte in Schleifen zu laden.
Pagination
Listen liefern data und pagination:
json
{
"data": [ { "id": 1 } ],
"pagination": { "limit": 50, "offset": 0, "total": 137, "hasMore": true }
}limitmaximal 200, Standard 50.- Nächste Seite:
offset = offset + limit, solangehasMorewahr ist. /time-entriesverwendetpageundpageSize(maximal 100) und liefertpageInfoimdata-Objekt.
Datumsformate
- Zeitstempel: ISO 8601 mit Zeitzone, zum Beispiel
2026-09-12T10:00:00+02:00. - Tagesdaten (
from,to, Termine):YYYY-MM-DD, interpretiert inEurope/Vienna.
Versionierung
Der Pfad trägt die Hauptversion (/api/v1). Innerhalb von v1 kommen Felder und Endpunkte hinzu, bestehende Felder werden nicht entfernt oder umbenannt. Was in welcher API-Version hinzugekommen ist, steht unter API-Änderungen; dort liegen auch ältere Fassungen der Spezifikation zum Herunterladen.
Einzelne Pfadsegmente werden ins Englische umbenannt (/anfragen → /inquiries, /kontakte → /contacts, /rapports → /site-reports); die alten Segmente bleiben als Aliase dauerhaft erreichbar und liefern identische Antworten — kein Redirect, keine Frist zum Umstellen.
Support
Fragen zur Integration: office@work7.net. Bitte Betrieb, Schlüssel-Prefix (w7_live_<prefix>_…) und den Wert des Antwort-Headers x-request-id mitschicken, nie den vollständigen Schlüssel.