Termine abrufen
Gibt eine paginierte Liste aller Termine des Unternehmens zurück.
Breaking Change
Der Kurs eines Kurs-Termins (data[].course) ist jetzt eine UUID statt eines eingebetteten Kurs-Objekts. Kursdaten und Teilnehmer lädst du über GET /courses/{uuid} nach.
Ohne den neuen Parameter status liefert diese Liste jetzt auch stornierte Termine. Nur aktive Termine bekommst du mit status=active.
Ohne Zeitstempel-Filter sortiert die Liste jetzt nach interner ID aufsteigend statt nach Startzeit absteigend.
Request
GET /api/oauth/v2/resource/appointments
Header
| Header | Wert |
|---|---|
Authorization | Bearer {access_token} |
Query-Parameter
| Parameter | Typ | Pflicht | Beschreibung |
|---|---|---|---|
offset | integer | Nein | Startposition (Standard: 0) |
limit | integer | Nein | Anzahl Ergebnisse (Standard: 50, Max: 200) |
from | string (ISO 8601) | Bedingt | Zeitraum-Start – als Datum (2026-03-01) oder ISO 8601 (2026-03-01T00:00:00+01:00). Pflicht, solange weder updatedSince noch deletedSince gesetzt ist – siehe Hinweis unter der Tabelle. |
until | string (ISO 8601) | Bedingt | Zeitraum-Ende – als Datum (2026-03-31) oder ISO 8601 (2026-03-31T23:59:59+01:00). Pflicht, solange weder updatedSince noch deletedSince gesetzt ist – siehe Hinweis unter der Tabelle. |
type | string | Nein | Kommaseparierte Liste von Termintypen (Standard: admin,customer,course). Erlaubte Werte: admin, customer, course, pause, private, vacation, illness, official_holiday, external. Sonderwert all gibt alle Typen zurück. |
customerId | string (UUID) | Nein | Filtert Termine auf einen bestimmten Kunden. Gibt nur Termine zurück, bei denen der Kunde direkt zugewiesen ist oder in der Attendee-Liste eines Kurs-Termins vorkommt. |
status | string | Nein | Kommaseparierte Liste (Standard: all). Erlaubte Werte: active, deleted. active liefert nur nicht stornierte Termine. Ungültiger Wert → 400 validation_error. Details siehe Konventionen. |
updatedSince | string (ISO 8601) | Nein | Nur Termine, die ab diesem Zeitpunkt geändert wurden (updatedAt >=). Neu angelegte Termine kommen mit, weil der Änderungszeitpunkt beim Anlegen gesetzt wird. |
updatedUntil | string (ISO 8601) | Nein | Nur Termine, die bis zu diesem Zeitpunkt geändert wurden (updatedAt <=) |
deletedSince | string (ISO 8601) | Nein | Nur stornierte Termine mit Löschzeitpunkt ab diesem Wert (deletedAt >=). Aktive Termine liefert dieser Filter nicht. |
view | string | Nein | default (Standard) oder slim. slim liefert pro Termin nur die wichtigsten Felder – Details siehe Konventionen. Ungültiger Wert → 400 validation_error. |
Hinweis: Du fragst Termine auf zwei Wegen ab. Mit
fromunduntilholst du ein Terminfenster – die API gibt nur Termine in diesem Zeitraum zurück. MitupdatedSince(oderdeletedSince) holst du stattdessen Änderungen unabhängig vom Termindatum.Eines von beiden brauchst du immer. Fehlt
fromoderuntil, und ist wederupdatedSincenochdeletedSincegesetzt, antwortet die API mit400 validation_error.
Wichtig: Schicke beim inkrementellen Abgleich kein Terminfenster mit.
from/untilundupdatedSincewirken sonst zusammen – du bekommst nur Änderungen innerhalb dieses Zeitfensters und verpasst den Rest. Für einen vollständigen Abgleich lässt dufromunduntilweg.
Tipp: Bei Angabe ohne Uhrzeit (z.B.
2026-03-01) wirdfromauf00:00:00unduntilauf23:59:59gesetzt – ideal für tagesgenaue Abfragen.
Hinweis: Dieser Endpoint erfordert immer die Scopes
appointmentsundusers. Für Kurs-Termine (Typcourseoderall) ist zusätzlich der Scopecustomerserforderlich.
Hinweis: Wird
customerIdangegeben, ist zusätzlich der Scopecustomerserforderlich – unabhängig vom gewählten Termintyp.
Hinweis zu Kurs-Terminen: Bei Kurs-Terminen (
type=course) steht incoursenur die UUID des Kurses. Kursdaten, Teilnehmer und Kursleiter lädst du überGET /courses/{uuid}undGET /courses/{uuid}/attendeesnach. Für Kurs-Termine ist trotzdem der Scopecustomerserforderlich. Fragst du explizit?type=course(oder?type=all) an, ohne diesen Scope zu besitzen, antwortet die API mit403 insufficient_scope.
Hinweis: Für einen Abgleich genügt
updatedSince– der Wert deckt neue und geänderte Termine ab. Stornierte Termine holst du in einem zweiten Request mitdeletedSince.
Hinweis:
deletedSincelässt sich nicht mitupdatedSinceoderupdatedUntilkombinieren. Die API antwortet dann mit400 validation_error, weildeletedSincedie Ergebnismenge auf stornierte Termine umschaltet. Frage Änderungen und Stornierungen deshalb in zwei getrennten Requests ab.
Hinweis: Formate, Zeitzonen-Verhalten, Sortierung im Filter-Modus, Cursor-Strategie und die
meta.total-Falle gelten für alle Zeitstempel-Filter gleich – siehe Inkrementeller Abgleich.Beim Lösch-Abgleich mit
deletedSincekommt ein weiterer Fall des Restrisikos hinzu. Ein während des Durchlaufs wiederhergestellter Termin behält seinen alten Zeitstempel und rutscht unter den neuen Cursor.
Hinweis zu wiederhergestellten Terminen: Ein stornierter Termin kann wiederhergestellt werden. Dabei fällt der Löschzeitpunkt weg. Der Termin verschwindet damit aus dem Lösch-Abgleich und erscheint stattdessen im Änderungs-Abgleich mit
deleted=false.Hast du den Termin lokal auf die Storno-Meldung hin entfernt, holst du ihn dir so zurück. Voraussetzung: dein Client wertet
deletedaus. Fahre deshalb immer beide Abgleiche, nicht nur den Lösch-Abgleich.
Hinweis zu Stornierungen aus dem Altbestand:
deletedSincefindet nur Termine mit gesetztem Löschzeitpunkt. Termine, die davor storniert wurden, erreichst du damit nicht – auch nicht mit einem sehr frühen Datum.
Hinweis: Termine unterstützen kein
createdSinceund keincreatedUntil. Das FeldcreatedAtliefert am Termin den Buchungszeitpunkt, nicht den Erstellungszeitpunkt des Datensatzes. Für den inkrementellen Abgleich über Termine verwendest du deshalbupdatedSince.
Response
Erfolg (200 OK)
Beispiel: Einzeltermin (type: "customer")
{
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Erstgespräch",
"start": "2026-03-15T10:00:00+01:00",
"end": "2026-03-15T11:00:00+01:00",
"allDay": false,
"status": "active",
"type": "customer",
"course": null,
"note": "Bitte nüchtern erscheinen",
"customer": {
"id": "f0e1d2c3-b4a5-6789-fedc-ba9876543210",
"firstName": "Anna",
"lastName": "Muster"
},
"service": {
"id": "c3d4e5f6-a7b8-9012-cdef-012345678901",
"name": "Erstgespräch"
},
"location": {
"id": "e5f6a7b8-c9d0-1234-efab-234567890123",
"name": "Studio 1",
"street": "Hauptstraße 1",
"zip": "1010",
"city": "Wien",
"country": "AT"
},
"user": {
"id": "d4e5f6a7-b8c9-0123-defa-123456789012",
"firstName": "Max",
"lastName": "Muster",
"abbreviation": "MM"
},
"source": "oauth",
"deleted": false,
"deletedAt": null,
"stornoReason": null,
"cancelableUntil": null,
"createdAt": "2026-03-01T09:00:00+01:00",
"updatedAt": "2026-03-01T09:00:00+01:00"
}
],
"meta": {
"total": 500,
"offset": 0,
"limit": 50
}
}Beispiel: Kurs-Termin (type: "course")
{
"data": [
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"title": "Yoga Anfänger",
"start": "2026-03-20T09:00:00+01:00",
"end": "2026-03-20T10:00:00+01:00",
"allDay": false,
"status": "active",
"type": "course",
"course": "c3d4e5f6-a7b8-9012-cdef-012345678901",
"note": null,
"customer": null,
"service": null,
"location": {
"id": "e5f6a7b8-c9d0-1234-efab-234567890123",
"name": "Studio 1",
"street": "Hauptstraße 1",
"zip": "1010",
"city": "Wien",
"country": "AT"
},
"user": {
"id": "d4e5f6a7-b8c9-0123-defa-123456789012",
"firstName": "Max",
"lastName": "Muster",
"abbreviation": "MM"
},
"source": "oauth",
"createdAt": "2026-03-01T09:00:00+01:00",
"updatedAt": "2026-03-01T09:00:00+01:00"
}
],
"meta": {
"total": 500,
"offset": 0,
"limit": 50
}
}Response-Felder
| Feld | Typ | Beschreibung |
|---|---|---|
data | Array | Liste der Termine |
data[].id | string (UUID) | Eindeutige Termin-ID |
data[].title | string | Titel des Termins |
data[].start | string (ISO 8601) | null | Startzeit |
data[].end | string (ISO 8601) | null | Endzeit |
data[].allDay | boolean | Ganztägiger Termin |
data[].status | string | Status des Termins (active oder request) |
data[].type | string | Termintyp (admin, customer, course, pause, private, vacation, illness, official_holiday, external) |
data[].course | string (UUID) | null | Kurs-Referenz – nur bei type: "course" befüllt, sonst null. Kursdaten und Teilnehmer lädst du über GET /courses/{uuid} nach |
data[].note | string | Interne Notiz |
data[].customer | object | null | Verknüpfter Kunde |
data[].customer.id | string (UUID) | Kunden-ID |
data[].customer.firstName | string | Vorname |
data[].customer.lastName | string | Nachname |
data[].service | object | null | Verknüpfte Leistung |
data[].service.id | string (UUID) | Leistungs-ID |
data[].service.name | string | Name der Leistung |
data[].location | object | null | Ausführungsort des Termins |
data[].location.id | string (UUID) | Standort-ID |
data[].location.name | string | Standortname |
data[].location.street | string | null | Straße |
data[].location.zip | string | null | PLZ |
data[].location.city | string | null | Ort |
data[].location.country | string | null | Land |
data[].user | object | null | Dem Termin zugeordnete:r Mitarbeiter:in |
data[].user.id | string (UUID) | Mitarbeiter-ID |
data[].user.firstName | string | Vorname |
data[].user.lastName | string | Nachname |
data[].user.abbreviation | string | null | Kürzel |
data[].source | string | Quelle des Termins |
data[].deleted | boolean | true wenn der Termin storniert ist, sonst false |
data[].deletedAt | string (ISO 8601) | null | Zeitpunkt der Stornierung, null wenn nicht storniert |
data[].stornoReason | string | null | Storno-Grund, null wenn nicht storniert oder kein Grund angegeben |
data[].cancelableUntil | string (ISO 8601) | null | Zeitpunkt, bis zu dem der Termin online storniert werden kann, null wenn keine Frist gesetzt ist |
data[].createdAt | string (ISO 8601) | null | Buchungszeitpunkt des Termins – nicht der Erstellungszeitpunkt des Datensatzes |
data[].updatedAt | string (ISO 8601) | null | Letzter Änderungszeitpunkt |
meta.total | integer | Gesamtanzahl der Termine |
meta.offset | integer | Aktuelle Startposition |
meta.limit | integer | Anzahl der zurückgegebenen Einträge |
Hinweis: Die Objekte
customerundserviceenthalten nur eine Kurzform mit den wichtigsten Feldern (siehe Tabelle oben).
Hinweis: Kurse sind auch eigenständig über
GET /coursesabrufbar und paginierbar, ohne über Termine zu gehen.
curl -X GET "https://my.offisy.at/api/oauth/v2/resource/appointments?offset=0&limit=50&from=beispiel&until=beispiel&type=admin%2Ccustomer%2Ccourse&customerId=b2c3d4e5-f6a7-8901-bcde-f01234567890&status=all&updatedSince=2026-04-15T10%3A00%3A00&updatedUntil=2026-04-15T10%3A00%3A00&deletedSince=beispiel&view=default" \
-H "Authorization: Bearer {access_token}"Mögliche Fehler
| Status | Code | Beschreibung |
|---|---|---|
| 400 | validation_error | from und/oder until fehlt, obwohl weder updatedSince noch deletedSince gesetzt ist – oder der Wert ist ungültig |
| 400 | validation_error | from liegt nach until (error_description: "from" must be before or equal to "until".) |
| 400 | validation_error | Ein Zeitstempel-Filter enthält einen ungültigen Wert, oder updatedSince liegt nach updatedUntil |
| 400 | validation_error | deletedSince wurde mit updatedSince oder updatedUntil kombiniert |
| 400 | validation_error | status enthält einen ungültigen Wert |
| 400 | validation_error | view ist weder default noch slim |
| 401 | invalid_client | Ungültiger oder fehlender Access Token |
| 403 | insufficient_scope | Scope appointments oder users fehlt (beide immer erforderlich) |
| 403 | insufficient_scope | ?type=course/?type=all angefragt, aber Scope customers fehlt |