Listen, Filter und Referenzen
Diese Regeln gelten für die OAuth-API. Status-Filter, Referenzen, Paginierung und Sortierung betreffen die Listen-Endpoints, Datums- und Zeitparameter gelten endpointübergreifend – sofern der jeweilige Endpoint-Artikel nichts anderes sagt.
Status-Filter
Der Parameter status filtert einen Listen-Endpoint nach dem Status seiner Datensätze. Der Standardwert ist all – ohne den Parameter liefert die Liste also alle Werte. Du kannst mehrere Werte kommagetrennt angeben, z.B. status=active,archived. Ein ungültiger Wert liefert 400 validation_error.
| Endpoint | Werte | active bedeutet |
|---|---|---|
| Waren abrufen | active, archived, deleted | weder archiviert noch gelöscht |
| Kurse abrufen | active, archived | nicht archiviert |
| Kursteilnehmer abrufen | active, deleted | nicht storniert |
| Aufgaben abrufen | active, deleted | nicht gelöscht |
| Status abrufen | active, deleted | nicht gelöscht |
| Lagerorte abrufen, Lieferanten abrufen | active, deleted | nicht gelöscht |
| Mahnstufen abrufen | active, deleted | nicht gelöscht |
| Mahnungen abrufen | active, replaced, canceled | nicht gelöscht und Mahnstatus active, done oder leer |
| Kunden abrufen | active, archived, deleted | weder archiviert noch gelöscht |
| Termine abrufen, Mitarbeiter abrufen, Rechnungen abrufen, Honorarnoten abrufen | active, deleted | nicht gelöscht bzw. nicht storniert |
| Tags, Referenzen, Warenkategorien, Lagerbewegungen, Bestellungen, Kundengruppen, Auswertungen | – | kein status-Parameter, siehe unten |
Bei den Mahnungen kommt ein zweiter Parameter dazu: dunningStatus (active, replaced, canceled, done) filtert nach dem fachlichen Mahnstatus und wirkt UND-verknüpft mit status. Bei status bedeutet canceled dort: Mahnstatus canceled oder gelöscht – eine gelöschte Mahnung ist immer canceled, unabhängig vom Mahnstatus.
Endpoints ohne Status-Filter
Folgende Listen kennen keinen status-Parameter, weil ihre Datensätze keinen Löschzeitpunkt oder Endzustand tragen: Tags, Referenzen, Warenkategorien, Lagerbewegungen, Bestellungen, Kundengruppen und Auswertungen. Sie liefern immer den vollständigen, ungefilterten Bestand.
Einzelabruf
Der Einzelabruf eines Datensatzes (GET /{resource}/{uuid}) antwortet immer mit 200 OK, auch für einen Datensatz im Endzustand – gelöscht, storniert, archiviert oder ersetzt. Ein Datensatz, der nicht mehr aktiv ist, liefert also keinen 404.
Ein Einzelabruf antwortet dagegen mit 404, wenn der Datensatz für den Besitzer-Benutzer deines API-Zugangs nicht erreichbar ist: bei Rechnungen, Honorarnoten, Bareinnahmen und Mahnungen, wenn der Mandant des Datensatzes nicht zugänglich ist (siehe Zugriff mit dem Add-on Mandanten). Die Antwort ist in diesem Fall exakt dieselbe wie bei einer unbekannten UUID, damit sie nichts über die Existenz des Datensatzes verrät:
{"error":"not_found","error_description":"The requested resource was not found."}Beim Kursteilnehmer gilt das nicht: Ist der Kurs für den Besitzer-Benutzer nicht lesbar, antwortet der Einzelabruf mit 403 access_denied.
Löschzustand im Response
Jeder Datensatz mit einem Löschzustand trägt das Feld deleted (boolean). Hat die zugehörige Entity einen Löschzeitpunkt, kommt zusätzlich deletedAt (ISO 8601, null wenn nicht gelöscht) dazu. Wo ein Datensatz zusätzlich archiviert werden kann, kommt das Feld archived (boolean) dazu. Archiviert und gelöscht sind dabei unabhängige Zustände.
Referenzen auf andere Datensätze
Eine Referenz auf einen anderen Datensatz kommt als UUID-String, z.B. "customer": "<uuid>". Eine Liste von Referenzen kommt als Array von UUID-Strings, z.B. "instructors": ["<uuid>"]. Fehlt die Referenz, ist der Wert null (bei Listen []). Es gibt keine eingebetteten Objekte und keine Namen mehr in diesen Feldern.
Um die Daten hinter einer Referenz zu laden, rufst du den zugehörigen Endpoint mit der UUID auf. Dafür brauchst du denselben Scope wie für die Liste, aus der die Referenz stammt.
| Relation | Endpoint |
|---|---|
customer | GET /customers/{uuid} |
course | GET /courses/{uuid} |
invoice | GET /invoices/{uuid} |
dunningType | GET /invoices/dunnings/types/{uuid} |
tenancy / tenancies | GET /tenancies/{uuid} |
assignedTo, treatingUser, instructors, consumer, createdBy, updatedBy | GET /users/{uuid} |
status (Aufgaben) | GET /todos/statuses/{uuid} |
tags (Aufgaben) | GET /todos/tags/{uuid} |
attendees (Kurse) | GET /courses/{uuid}/attendees/{attendeeUuid} |
category (Kurse, Leistungen) | GET /services/categories/{uuid} |
category (Waren) | GET /wares/categories/{uuid} |
ware (Lagerbewegungen, Bestellungen) | GET /wares/{uuid} |
Referenzen ohne eigenen Endpoint
Manche Referenzen haben keinen eigenen Endpoint zum Nachladen. In den Auswertungen betrifft das resource und insuranceService. Ein Zieltyp ohne eigenen OAuth-Endpoint bleibt ebenfalls eine reine UUID ohne Nachlademöglichkeit: MedicalExternalLetter, MotherChildPass, Psychotherapy, Receipt, DVPReportEntry und Transmission. Bei Waren bleibt groups eine Ausnahme von der UUID-Form: Eine Warengruppe hat keine UUID, das Feld liefert weiterhin [{ "name": "..." }].
Ausnahmen im Bestand
Eingebettete Objekte in Terminen (customer, service, location, user), Kunden und Rechnungen bleiben vorerst unverändert – sie werden nicht Teil dieser Umstellung. Ausgenommen ist course im Termin: Dort steht nur die UUID des Kurses.
Zugriff mit dem Add-on Mandanten
Arbeitet dein Unternehmen mit dem Add-on Mandanten, prüfen Rechnungen, Honorarnoten, Bareinnahmen und Mahnungen den Mandanten jedes Datensatzes. Ein Datensatz ist nur erreichbar, wenn der Besitzer-Benutzer deines API-Zugangs Zugriff auf seinen Mandanten hat und dort das passende Recht besitzt:
| Endpoint | Recht je Mandant |
|---|---|
GET /invoices, GET /fee-notes, GET /cash-revenues und die jeweiligen Einzelabrufe | Rechnungen: Anzeigen |
GET /invoices/dunnings und Einzelabruf | Mahnwesen: Anzeigen |
POST /invoices/dunnings | Mahnwesen: Erstellen |
Daraus folgt:
Eine Liste enthält nur Datensätze aus Mandanten, die der Besitzer-Benutzer sehen darf.
meta.totalzählt nur diese Datensätze.Datensätze ohne Mandant sind nicht erreichbar.
Der Einzelabruf eines Datensatzes außerhalb dieser Mandanten antwortet mit
404 not_found, exakt wie bei einer unbekannten UUID.POST /invoices/dunningsfür eine Rechnung eines nicht zugänglichen Mandanten, oder ohne das Recht Mahnwesen: Erstellen in diesem Mandanten, antwortet mit404 not_found(error_description:Invoice not found.) – wie bei einer unbekannten Rechnung.GET /fee-note-services/turnoverundGET /fee-note-services/productivityantworten mit403 access_denied, wenn dem Besitzer-Benutzer kein Mandant zugänglich ist. Eine Auswertung nur über Datensätze ohne Mandant würde sonst vollständig wirken.
Ohne das Add-on Mandanten ändert sich nichts.
Schlanke Antworten mit view
Sieben Listen-Endpoints nehmen den Query-Parameter view an: GET /appointments, GET /customers, GET /executions, GET /invoices, GET /locations, GET /services und GET /users. Erlaubt sind default (Standard) und slim. Mit slim liefert die Liste pro Datensatz nur die wichtigsten Felder. Verweise auf andere Datensätze kommen dabei als { "id": "<uuid>" }.
Ein anderer Wert oder ein Array ergibt 400 validation_error mit Parameter "view" must be "default" or "slim". Einzelabrufe lesen den Parameter nicht. Wird view mehrfach angegeben, gilt die letzte Angabe – ?view=default&view=slim liefert also die schlanke Antwort.
| Endpoint | Felder mit view=slim |
|---|---|
GET /appointments | id, title, start, end, allDay, status, type, source, deleted, deletedAt, createdAt, updatedAt. customer, service, location und user als { "id" }, course als Objekt mit id, name, maxAttendees und currentAttendees |
GET /customers | id, title, firstName, lastName, companyName, sex, birthday, email, phone, street, zip, city, country, customerNumber, archived, deleted, createdAt, updatedAt, deletedAt |
GET /executions | id, user, location, service (jeweils als { "id" }) |
GET /invoices | id, sequence, formattedSequence, status, net, subTotal, gross, discount, outstandingBalance, invoicingDate, paidAt, isFullyPaid, deleted, createdAt, updatedAt. customer als { "id" } |
GET /locations | id, name, abbreviation, street, zip, city, country, bookable, sort |
GET /services | id, name, duration, price, type, sort. category als { "id" } |
GET /users | id, title, firstName, lastName, abbreviation, isDoctor, deleted, createdAt, updatedAt |
Paginierung
Eine paginierte Liste verwendet die Parameter offset (Standard: 0) und limit (Standard: 50, maximal 200) und liefert die Gesamtanzahl in meta.total. Zwei Endpoints liefern ihre Ergebnisse ohne Paginierung: GET /todos/{uuid}/references liefert alle Referenzen einer Aufgabe ohne Datenbank-Paginierung, und GET /customers/{uuid}/groups liefert alle Kundengruppen eines Kunden ohne offset/limit.
offset, limit und maxDays (bei Nächste freie Slots) müssen ganze Zahlen sein. Ein anderer Wert (abc, 5.5, 1e3, Leerzeichen) ergibt 400 validation_error mit Parameter "limit" must be an integer. (bzw. dem jeweiligen Parameternamen). Früher wurde so ein Wert stillschweigend in eine Zahl umgewandelt. Ein Wert über oder unter der Grenze wird weiterhin auf die Grenze gesetzt, ein fehlender Parameter verwendet den Standardwert.
meta.total zählt in der Datenbank, bevor die Berechtigungsprüfung greift. Die Mandanten-Prüfung bei Rechnungen, Honorarnoten, Bareinnahmen und Mahnungen ist davon ausgenommen: Sie wirkt schon in der Abfrage, siehe Zugriff mit dem Add-on Mandanten. Was das für die Paginierung bedeutet, steht unter Inkrementeller Abgleich.
Standard-Sortierung
Ohne einen Sortier- oder Zeitstempel-Filter sortiert eine Liste nach interner ID aufsteigend. Die interne ID ist dabei die Datenbank-ID des Datensatzes, nicht das Response-Feld id (das ist die UUID). Diese Reihenfolge ist stabil über die gesamte Paginierung. Das gilt für alle umgestellten Listen, ausdrücklich auch für Mahnstufen, Kundengruppen, Aufgaben, Kurse, Kursteilnehmer, Termine, Kunden, Mitarbeiter, Honorarnoten und Rechnungen.
Drei Ausnahmen bleiben bestehen:
GET /waressortiert bei gesetztem Parametertermnach Relevanz statt nach interner ID.- Lagerbewegungen und Bestellungen behalten im Filter-Modus ihre bisher dokumentierte Sortierung.
GET /fee-note-servicessortiert nachdateabsteigend, bei gleichem Datum nachidabsteigend.
Datums- und Zeitparameter
Die OAuth-API parst die Datums- und Zeit-Parameter der Query strikt, ebenso start beim Termin buchen. Das betrifft u.a. from/until bei den Auswertungen, dueDateFrom/dueDateTo bei Aufgaben, startDate/endDate bei freien Slots, from/until bei Terminen und start beim Termin buchen.
Angenommen werden:
- ein reines Datum
YYYY-MM-DD(z.B.2026-08-24) – nur an Endpoints, die keine Uhrzeit verlangen - Datum und Uhrzeit mit Minutenpräzision, ohne Sekunden (
2026-08-24T10:00) – die Sekunden werden dabei als00angenommen - Datum und Uhrzeit mit Sekunden (
2026-08-24T10:00:00) oder mit Millisekunden (2026-08-24T08:00:00.000Z) - jeweils mit Zeitzonen-Offset (
2026-08-24T10:00+02:00,2026-08-24T10:00:00+02:00) oder als UTC mitZ(2026-08-24T10:00Z,2026-08-24T08:00:00.000Z)
Alle Felder sind zweistellig (2026-08-04, nicht 2026-8-4), das Jahr liegt zwischen 1000 und 9999. Ein Offset hat die Form Z, ±HH:MM oder ±HHMM und liegt höchstens bei ±14:00. Die Kleinbuchstaben t und z sind erlaubt. Das gilt für die Query-Parameter und für start.
Mit 400 validation_error abgelehnt werden Zonennamen (Europe/Vienna, CEST, UTC), ein Offset ohne Minuten (+02), ein Offset mit Sekunden, einstellige Monats-, Tag- oder Stundenangaben und ein Leerzeichen vor dem Offset.
Alle Body-Felder, die nur ein Datum erwarten (z.B. date, birthday, customDate, customStartDate), nehmen ausschließlich YYYY-MM-DD als echtes Kalenderdatum an, mit einem Jahr zwischen 1000 und 9999. Ein Zeitstempel, einstellige Angaben oder ein unmögliches Datum ergeben 400 validation_error mit Invalid <Feld>. Expected a date (YYYY-MM-DD).; null oder ein fehlendes Feld bleibt unverändert. Body-Felder mit Zeitangabe (start beim Termin buchen, dueDate bei Aufgabe erstellen und Aufgabe bearbeiten) akzeptieren die oben genannten ISO-Formate.
Ein Zeitzonen-Offset im Wert wird intern auf die Server-Zeitzone normalisiert. Ein unmögliches Kalenderdatum wie 2026-02-31 ergibt 400 validation_error, keine stille Umrechnung auf einen Nachbartag. Eine Zeitangabe mit Leerzeichen statt T (2026-08-24 10:00) oder ein relatives Format (tomorrow) werden ebenfalls abgelehnt.
Ist der Parameter gesetzt, aber ungültig, antwortet der Endpoint mit dem Format-Fehler des jeweiligen Parameters – nicht mit einer Meldung, dass der Parameter fehlt oder required ist, zum Beispiel:
Parameter "from" must be an ISO 8601 date or date/time (e.g. "2026-08-24", "2026-08-24T10:00", "2026-08-24T10:00:00", "2026-08-24T10:00:00+02:00" or "2026-08-24T08:00:00.000Z").
Ein + im Zeitzonen-Offset muss in der URL als %2B prozent-kodiert werden.
Bei einem Zeitraum aus zwei Parametern darf der Beginn nicht nach dem Ende liegen: createdSince nach createdUntil, updatedSince nach updatedUntil und dueDateFrom nach dueDateTo ergeben 400 validation_error. Bei Terminen gilt dasselbe für from und until.
Zusammenspiel mit Zeitstempel-Filtern
Der Parameter deletedSince liefert nur gelöschte bzw. stornierte Datensätze und wirkt UND-verknüpft mit status. Kombinierst du deletedSince mit status=active, widersprechen sich beide Bedingungen – die Antwort ist eine leere Liste, kein Fehler. Mit status=deleted (oder ohne status) liefert deletedSince wie erwartet die gelöschten Datensätze ab dem angegebenen Zeitpunkt.
deletedSince lässt sich an vielen Endpoints außerdem nicht mit createdSince, createdUntil, updatedSince oder updatedUntil kombinieren – das ist eine eigene Regel und unabhängig von status. Details dazu, zum Cursor über mehrere Seiten und zum Restrisiko der Paginierung stehen unter Inkrementeller Abgleich.